联芯透明窗体支持方案
更新日期:
需求
- 底下窗体内容会变化。
- 接口简单,使用方便。
- 内存占用少,效率高。
- 透明窗体弹出时,屏幕上所包含的窗体及个数不限定。
- 支持透明主窗体、透明非模态对话框、透明模态对话框。
- 能移动透明窗体和非透明窗体。
- 在创建了透明窗体后能继续创建非透明窗体。
- 能动态设置与撤销双缓冲。
问题
这里先要说明下现有 MiniGUI 框架实现透明窗体的问题。如果光光是要窗体能透明还是比较好办的。在双缓冲回调函数中设置下 memdc 的层 alpha ,然后 BitBlt 到屏幕就可以了。但是这里主要存在的问题是透明的窗体的更新的问题。就是透明窗体下面的窗体更新、或是透明窗体本身更新时怎样才能得到正确的图像。这里分2点:
透明自身窗体更新:由于是透明窗体的,所以透明窗体的图像需要与屏幕做 alpha 混合。非透明窗体直接将自己的图像更新到屏幕就行了。但是如果透明窗体直接这么做的话,会导致与上一次的图像重叠,结果是图像越来越不透明。因此就需要将背景刷新掉。这就需要处在透明窗体下的非透明窗体更新图像。但是现有 MiniGUI 的框架是不会更新下面的窗体的,因为下面的窗体处于透明窗体之下,被挡住了,是会被剪切掉的。
非透明窗体更新:如果处于透明窗体下的非透明窗体更新的话,由于是透明窗体,所以应该能看得到底下的非透明窗体的更新。这就需要透明窗体更新。和上面一点一样, MiniGUI 的现有框架是不会更新透明窗体的。因为透明窗体位于非透明窗体的之上,MiniGUI 认为底下的窗体不会引起上面的窗体的更新。
解决方案
根据上面提到的问题,我们可以在应用层使用 MiniGUI 双缓冲机制来实现。双缓冲提供了一个更新回调函数,窗体有更新时就会调用这个函数。因此我们可以自己设置这个更新函数,然在里面手动更新透明窗体。总结起来分为下面几点:
- 透明窗体和桌面上所有的非透明窗体必须使用双缓冲,透明窗体设置
WS_EX_TRANSPARENT
风格标志。 - 收集桌面中所有的非透明窗体,按照 zorder 保存在链表中,存放在透明窗体的 adddata2 中。
- 在透明窗体的更新函数中,遍历该链表,看其中的非透明窗体是否与透明窗体相交,是则将该非透明窗体的相应区域的图像更新到透明区域(更新透明区域的背景)。
- 在非透明窗体的更新函数,遍历桌面所有其他主窗体,找出具有
WS_EX_TRANSPARENT
风格的透明窗体。判断其更新区域是否与透明窗体相交,如果相交则要强制透明窗体更新。
限制条件
根据如上解决方案,本透明窗体的实现,具有一定的限制性,超出限制可能出现不可预料的问题。
- 屏幕上同时只能创建1个透明主窗体。
- 不要让透明窗体透出桌面(保证其在一个窗体内)。
- 暂时不支持窗口滚动。
接口
提供如下接口:
|
|
实现
数据结构
这里链表使用的是 linux 内核的链表实现。保存主窗体的链表数据结构:
|
|
创建透明主窗体
CreateTansparentMainWindow、CreateTransparentMainWindowIndirectParam 和 TransparentDialogBoxIndirectParam 其实流程都是差不多。都是先调用对应的 MiniGUI 原有的 API,然后初始化私有数据(就是保存桌面主窗体的链表)。这里主要是就是要实现初始化私有数据,主要流程是:
- 初始化链表(申请内存)。
- 使用 GetNextMainWindow 取得当前桌面的所有主窗体(注意要区分透明窗体自己)。使用 GetNextMainWindow(
HWND_NULL
) 做为遍历的开始就能保证 zorder。遍历结束的条件是 GetNextMainWindow 得到的句柄是HWND_NULL
。 - 将遍历得到的主窗体句柄保存到链表中。
- 判断遍历到的主窗体是否有双缓冲风格,如果没有则手动帮其设置上。并设置链表中相应主窗体的 dc flags。
- 遍历完成,将链表保存到透明窗体的 adddata2 中。
这里要注意一点:应该是由于是联芯修改 MiniGUI 源代码的问题,刚开始没办法更新非客户区,所以要在后面手动发送 MSG_NCPAINT
强制更新非客户区。
|
|
销毁透明主窗体
DestroyTransparentMainWindow、DestroyTransparentMainWindowIndirect 和 EndTransparentDialog 的流程也差不多的。都是先销毁私有数据(就是保存桌面主窗体的链表),然后再调用 MiniGUI 对应的 API 销毁。这里主要销毁私有数据,主要流程是:
- 遍历链表,查看主窗体的 dc flags 标志,看主窗体原来是否有双缓冲。如果没有则手动删除双缓冲数据,并还原主窗体设置。
- 逐一删除链表的节点,释放链表节点内存。
- 最后删除链表本身内存。
代码:
|
|
透明窗体更新
由于能够支持在创建透明窗体后继续创建非透明主窗体,以及先销毁非透明主窗体。所以需要时时检测是否有新非透明窗体创建,或是有存在的非透明窗体销毁,然后更新保存非透明主窗体的链表。然后更新方式就和前面解决方案里说的是一样的了。更新一次的流程是:
- 使用 GetNextMainWindow 遍历现有的主窗体,和链表中保存的窗体比较,以检查是否有新窗体创建或是有原有的窗体销毁。
- 如果检测出有新窗体创建或是有原有窗体销毁,则按照 zorder 重新构建链表。
- 遍历链表中的主窗体,判断这些窗体的区域是否与透明窗体的更新区域相交。如果相交则将对应区域的非透明窗体的图像 BitBlt 到透明窗体的区域(这里就解释了为什么需要桌面上所有的非透明窗体都是双缓冲,以及保存必须按照 zorder)。
- 最后根据透明窗体的 alpha 值,设置透明窗体 memdc 的 src alpha,然后 BitBlt 透明窗体的 memdc 。
代码:
|
|
非透明窗体更新
非透明窗体的更新函数除了更新自己的图像以为,还需要通知透明窗体,让其也更新。主要流程是:
- 使用 GetNextMainWindow 遍历现有的主窗体查找到有 WS_EX_TRANSPARENT 的透明窗体(注意区分自己)。
- 判断非透明窗体的更新区域是否与透明窗体相交。如果相交则调用 InvalidateRect 和 SendNotifyMessage 让透明窗体的客户区和非客户区重绘。
- BitBlt 将自己的 memdc 输出到屏幕上。
代码:
|
|
使用范例
使用起来非简单,但有如下约定:
- 必须使用对应的透明主窗体创建接口创建透明主窗体。
- 必须使用对应的透明主窗体销毁接口销毁透明主窗体。
- 只能创建一个透明主窗体。要创建下一个透明窗体时,必须要先销毁之前创建的。
具体例子见 附件 的 main.c 。(哎呦,好像链接失效了,哪天有空去翻翻看代码还不在不)
2011.2.16:改动1——支持逐点 alpha(包括在缓冲dc中)
问题
在之前的实现中是没办法支持逐点 alpha 的(典型的例子使用 png 图片进行贴图)。因为在目前 MiniGUI 的 BitBlt 实现中,如果有开启了层 alpha (透明窗体的透明实现就是应用层 alpha),就会忽略掉逐点 alpha。典型问题就是如果你用 png 图片进行贴图,然后使用 BitBlt 又开启了 MEMDC_FLAGS_SRCALPHA 的话,就会看到你 png 图片本来透明的部分变黑(一般是 memdc 的默认颜色)。
关于应用逐点 alpha 还有一个问题。如果是在 memdc 中使用的话,那么还有一个默认 dc 背景颜色的问题。当把 memdc BitBlt 到屏幕 dc 上的时候,逐点 alpha 会透出 memdc 默认的背景颜色(一般是黑色)。如果是在屏幕 dc 上,可以通过不绘制 MiniGUI 的背景来解决(截获
MSG_ERASEBKGND
,然后直接返回)。
解决办法
从以上的问题直接得到的解决办法是:
多建立一个 memdc ,然后先 BitBlt 进行逐点 alpha 混合,然后再 BitBlt 到屏幕 dc,进行层 alpha 混合。
在 32bit 颜色格式下,可以使用完全透明的颜色,将 memdc 的背景填充一次,这样在 alpha 混合的时候就可以完全透过 memdc 的背景色了。不过这个要求是 32bit 颜色格式的。16bit 色深下十分麻烦,要在 32bit 的memdc 上进行绘制,然后再转到 16bit 的屏幕 dc 上。这个就要求所有的图像加载参考 dc 都要是 32bit 的,并且所有 gdi 相关的参考 dc 都要是 32 bit 的。这个对于联芯来说,改动肯定很大,估计他是不会接受的。
从上面的讨论我们可以看到其实现在透明最大的问题的就是透明的背景问题。为了解决上面的问题可以采用这样的解决办法:
将透明窗体下窗体的图像复制到透明窗体的缓存 dc 中做为透明的背景(这步可以在透明窗体的
MSG_ERASEBKGND
的消息里进行)。然后在缓冲 dc 里进行带逐点 alpha 的绘制。最后再用带层 alpha BitBlt 到屏幕 dc 上。这样缓冲 dc 里已经有正确的背景了,而且也已经正确的透过了,所以就算没了逐点 alpha 也是正确的了。并且这样还能解决 16bit 色下 memdc 透过默认背景的问题。这样在方案实现中增加2个接口:DefaultTransparentMainWinProc 和 DefaultTransparentDialogProc 用来封装
MSG_ERASEBKGND
消息处理。使用透明窗体要使用这2个接口替代 MiniGUI 原来的 DefaultMainWinProc 和 DefaultDialogProc 。
本质问题分析
上面的实现方式算是比较偏的了(也包括最开始的通过双缓冲来更新透明窗体的背景的实现)。其实如果要从正常逻辑实现透明窗体的话,应该提供如下2个条件:
- z 序支持,从底到上,逐步重绘透明窗体的背景。
- 32 bit 颜色格式。
更新下实现代码和示例。最开始写的代码有些地方不正确的。附近里的才是最新的。顺带上个效果图,show 一下,现在的 MiniGUI 也能做到这样的效果哦。 :-D
2011.3.24:改动2——一些小修正
问题
- 很多无需收集的窗体,例如 窗口区域为0,不可见的。(联芯他们的 goku 框架里后台一大堆这样的窗口
-_-||
) - 被隐藏的窗口绘制不正确。
- 给普通窗体动态添加双缓冲,第一次更新双缓冲中的图片,导致闪烁。
- 透明窗体刷新速度慢
解决办法
- 这个可以在透明窗体的更新函数里判断下,如果是不可见的窗体着跳过这个窗体的更新:
代码:
|
|
这个和上面一样的解决办法
这个在第一次更新时图像到双缓冲上的时候,应该不让双缓冲更新到屏幕上,也就是把双缓冲更新函数设置成 DONOTHING :
代码:
|
|
- 这个在联芯的实际使用情况中还是有点明显的。根据他们提出改进建议,在他们的使用情况中,有很大几率的情况会遇到在刷新链表中,某个普通窗体的更新区域(普通窗体与透明窗体相交的区域)正好是透明窗体的更新区域,这样就不需在更新后面的普通窗体的背景到透明窗体中来了。不过这样需要将保存的链表反序遍历,并且比较找到是否存在这样的普通窗体,还要记下这个链表位置(或者说记下这个窗体的句柄??)。仔细分析下,这种思路,在某些情况下应该是能提高效率的。不过我暂时没有实现这个,据说联芯他们在自己改。
代码路径
最开始的代码已经有很多地方不对了。这里就不在上传代码在本文档里了。现在的代码已经在 svn 中(在 3rd-party/transparent 里,devsrv 就是 10.10.0.9):
URL: svn+ssh://devsrv/home/projects/svn/minigui/branches/rel-3-0-arena
2011.4.19:改动3——支持动态设置/去除窗口透明属性
问题
联芯需要新加一系列动态设置透明窗体属性与动态去掉透明窗体属性的接口。他们需要有个需求,是因为想绕过透明窗体实现方案中的同时只能存在一个透明窗体的限制。他们想在透明窗体存在的时候,弹出一个普通窗体,然后去掉原来透明窗体的属性,最后再给弹出来的普通窗体加上透明属性。
解决办法
这个就目前的实现方法来说,并不是很难实现,灵活的运用现有的代码就能很轻松的实现了。新增加一个接口就可以了:
BOOL EnableWindowTransparent(HWND hWnd, BOOL isEnable);
可以通过后一个 BOOL 参数来决定是将普通窗体变成透明窗体,还是把透明窗体变成普通窗体。而这个函数内部可以简单调用内部2个实现函数就可以了:
|
|
至于内部的这个函数实现的流程如下:
enable_win_transparent:
- 如果当前窗体已经具有透明窗体属性,者直接返回。
- 判断当前窗体是否有双缓冲,如果没有,这动态的设置上双缓冲属性。
- 调用
init_transparent_data()
申请并初始化透明窗体数据。 - 更新当前窗体,使其呈现出透明窗体特性。
disable_win_transparent:
- 如果当前窗体
- 去除当前窗口