DirectDraw画中画功能的实现

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; // 矩形数量
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提供了一个函数用于获取裁剪器的内部裁剪区域,其原型为:

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内部的叠加来实现画中画功能呢?这是由于在我们的应用中,每个播放窗口都是独立的,所谓的画中画只是多个播放窗口的一种布局形式,因此,一路画面使用一个控件具有更大的灵活性。 

你可能感兴趣的:(windows,工作,struct,api,null,buffer)