1. 问题来源 我们用DirectDraw开发了一个ActiveX播放控件,客户在应用时需要实现画中画效果,这只要将一大一小两个控件叠加即可,但问题是二者相叠会造成画面的剧烈闪烁,因为两个窗口会不断刷新同一块区域。该怎么办呢? 2. 最初方案 我首先想到的是通过DirectDraw裁剪来实现。所谓裁剪,是指对图像的输出区域的限制。因此,我猜想只要限制一下大画面的裁剪区域,将小画面所在范围从大画面中去掉,便能避免闪烁。 DirectDraw表面的裁剪是通过IDirectDrawClipper接口实现的。通常,在采用普通窗口模式时,我们调用SetHWnd方法 将裁剪器与某个窗口相关联,DirectDraw便会自动将图像的显示区域限制在窗口之中。而要制定形状任意的裁剪器,则需使用SetClipList函 数,其原型为: HRESULT SetClipList(
LPRGNDATA lpClipList, DWORD dwFlags ); 其中,参数lpClipList用于指定裁剪区域,而第二个参数dwFlags目前未使用,设为0就行。 如果我们熟悉Windows GDI函数,就不会对RGNDATA结构感到陌生,它用于描述一个区域(region)是由哪些矩形(rectangle)构成,RGNDATA的具体结构如下: typedef struct _RGNDATA {
RGNDATAHEADER rdh; // 头部,用于描述region的主要信息 char Buffer[1]; // 占位符,从Buffer开始为组成region的所有矩形数据 } RGNDATA, *PRGNDATA; RGNDATA的结构图为: ------------------
| RGNDATAHEADER | <- rdh ------------------ | rect 1 | <- Buffer ------------------ | ...... | ------------------ | rect n | ------------------ RGNDATAHEADER的结构为: typedef struct _RGNDATAHEADER {
DWORD dwSize; // 本结构的大小,等于sizeof(RGNDATAHEADER) DWORD iType; // RDH_RECTANGLES DWORD nCount; // 组成region的矩形数量 DWORD nRgnSize; // region数据的大小,等于nCount*sizeof(RECT) RECT rcBound; // 所有矩形的边界矩形 } RGNDATAHEADER, *PRGNDATAHEADER; 现在,我们来考察一下画中画功能的实现。 窗口布局 裁剪窗口
-------------------------- -------------------------- | | | | | | | 小画面 | | | | 裁剪矩形1 | | hWnd1 | | | | rc1 | | rc1 | | | | | |------------- | --> |------------------------| | | | | | 大窗口 | | | | hWnd0 | | 裁剪矩形0 | | rc0 | | rc0 | | | | | -------------------------- -------------------------- 上图中,左边为窗口布局,右边为对应的裁剪窗口。裁剪代码大致为: IDirectDrawClipper* pClipper = NULL; // 定义裁剪器
pDirectDraw->CreateClipper(0, &pClipper, NULL); // 创建裁剪器,pDirectDraw为之前创建好的IDirectDraw接口
const int nCount = 2; // 矩形数量 RGNDATA* pRgn = (RGNDATA*)malloc(sizeof(RGNDATAHEADER) + nCount*sizeof(RECT)); // 分配region所需空间 // 设置裁剪窗口 pClipper->SetClipList(pRgnData, 0); // 注意,在播放期间,pRgn不能释放,如果最后要释放,调用pClipper->SetClipList(NULL);即可 上述代码确实达到了我的预期效果,但另外一个问题又浮出水面:播放画面始终显示在屏幕最上方,就连播放程序最小化也无济于事。为什么通过SetHWnd构造的裁剪器就没有此问题呢?我决定要找出个究竟。 IDirectDrawClipper提供了一个函数用于获取裁剪器的内部裁剪区域,其原型为: HRESULT GetClipList(
LPRECT lpRect, LPRGNDATA lpClipList, LPDWORD lpdwSize ); 我回到SetHWnd窗口裁剪模式,在每次绘图时使用上述函数获得裁剪区域,并将其主要信息显示出来,结果我发现裁剪区域会动态改变!在播放窗口正 常显示情况下,裁剪区域等于窗口区域,当播放窗口被其它窗口遮挡时,裁剪区域变成窗口未被遮挡的部分,而当播放窗口最小化时,裁剪区域中的矩形个数变为 0,也就是说裁剪区域变为空。 原来如此,看似简单的一个SetHWnd函数,结果Windows自动为我们做了大量工作,而一旦使用SetClipList,则表明我们要自己维 护裁剪区域(难怪SetHWnd和SetClipList不能同时使用),这可不是一件容易的事(也许是我不知道容易的实现)。 3. 另选方案 看来,我得另辟途径了。我很快想到了窗口的裁剪,SetWindowRgn便是这样一个函数,其原型为: int SetWindowRgn(
HWND hWnd, // handle to window HRGN hRgn, // handle to region,可以通过CreatRectRgn等API函数创建,也可以使用CRgn类 BOOL bRedraw // window redraw option ); 实际上,SetWindowRgn经常被用于创建异形窗口。很快,我证实了自己的想法。但我对这一方案仍然不满意,因为设置裁剪区域对于用户来讲算是个比较麻烦的事情,有无更好的方法呢? 4. 最终实现 答案当然是肯定的,那就是使用窗口的裁剪属性。还记得吗,窗口的属性中有一个为WS_CLIPSIBLINGS,当一个子窗口具有此属性时,如果它被别的子窗口覆盖,那么它在绘制时不会更新重叠区域里的内容。 设置窗口属性的一种方法是在动态创建窗口时指定,另外一种是使用SetWindowLong函数: LONG SetWindowLong(
HWND hWnd, int nIndex, LONG dwNewLong ); 使用方法为: DWORD dwStyle = ::GetWindowLong(hWnd, GWL_STYLE);
dwStyle |= WS_CLIPSIBLINGS; ::SetWindowLong(hWnd, GWL_STYLE, dwStyle); 现在万事大吉了,不过还有一个问题需要考虑,这就是窗口重叠时会面临着谁在谁上方的问题,也即Z序问题。默认的Z序跟窗口创建顺序相关,如果要动态指定,就不得不请出另外一个API: BOOL SetWindowPos(
HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags ); 如果我们要将某个窗口至于所有子窗口的上面,那么进行如下调用即可: ::SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);
关于以上各API的更多说明请参见MSDN。 5. 后记 最后,总结一下我的这次任务,虽说整个工作花了不到一天时间,但前前后后却经历了几次方案的改进。最初,我由于这是一个跟DirectDraw相关 的项目而按照惯性思维想到了在DirectDraw技术范畴内实现,随着实际应用中问题的暴露,我不得不改用其它方式,进而又想到了最直接也是最简单的实 现方式。 很多项目亦是如此,如果我们能在动手之前仔细分析一下不同的方案,就能少走好多弯路,避免人力物力的浪费,当然,前提是需要我们对相关技术比较熟悉、对问题思考比较深入。 另外,有朋友可能会问,你为啥不用一个控件、通过DirectDraw内部的叠加来实现画中画功能呢?这是由于在我们的应用中,每个播放窗口都是独立的,所谓的画中画只是多个播放窗口的一种布局形式,因此,一路画面使用一个控件具有更大的灵活性。 (zorru) |