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