2008年(909)
分类:
2008-05-06 22:34:05
原著:Paul DiLascia
翻译:NorthTibet
下载源代码:CAtWork0512.exe (493KB)
原文出处:Layered
Windows, Blending Images
有没有方法创建一个半透明的窗口,并将该窗口上发生的所有鼠标事件都传递到桌面或另一个应用窗口处理?
int CMainFrame::OnCreate(...) { ... ModifyStyleEx(0, WS_EX_LAYERED|WS_EX_TRANSPARENT); }
ModifyStyle 和 ModifyStyleEx 是专用的 MFC CWnd 方法,其作用顾名思义。如果你用 C
语言编写,那么得调用 GetWindowLong(GWL_EXSTYLE) 来获取扩展式样,然后必须调用
SetWindowLong(GWL_EXSTYLE)来设置式样。其效果与 ModifyStyle(Ex)一样。当然,你也可以在创建窗口的时候使用此式样。
一旦创建了分层窗0口,你便可以调用 SetLayeredWindowAttributes 来设置透明度。可用的分层窗口属性之一是
LWA_ALPHA,它就是用来调整透明度的,取值范围从 0(完全透明)到 255(不透明)。要得到半透明的效果,可以这样调用
SetLayeredWindowAttributes:
// in CMainFrame::OnCreate SetLayeredWindowAttributes(0, 255 * 0.50, LWA_ALPHA);
这里我用乘法来表示一般公式;你可以仅用 128,因为那是 255 的一半(四舍五入)。你还可以用专门的颜色作为透明色。此时,你得用
LWA_COLORKEY 作为属性,在第一个参数中指定 COLORREF。Windows 会让所有像素颜色都呈透明。注意前面的代码段假设你是从
CWnd 派生对象中调用。如果用 C 语言,你得使用 ::SetLayeredWindowAttributes,它带有一个额外的参数 HWND。
你可以用分层窗口来进行动画和其它转换效果的处理;详细细节请参考文档中的“分层窗口”部分。
我正在写一个幻灯显示程序,该程序要显示JPEG图像序列。我使用了 2002年三月刊专栏文章中的 CPicture 类来绘制图像(参见:C
Q&A: Do You Have a License for that GIF? PreSubclassWindow, EOF in MFC,
and
More)。那个程序运行得很好。但我现在想添加从某一张图像到下一张图像的渐变特性。我在网页中用转换效果可以做到。那么是否有办法从程序代码中实现图像渐变特性?
[R,G,B]blended = ?[R,G,B]image (1-?? [R,G,B]background
当 alpha 为 0 时,你得到的是背景(图像完全透明);当 alpha 为 1 时,你得到非透明图像(完全不透明)。实际有透明效果的
alpha 值是一个 8 位的字节表示的值,范围从0-255,0 和 1 只是表示透明和非透明两个极端。它们都是可用的 alpha
值,但大多数应用程序不需要;多数应用程序使用一个常量 alpha
值来处理整个对象,如一幅图像。例如,你可能想让一幅特定的图像以25%的透明度显示。
AlphaBlend 函数类似老的 BitBlt 和 StretchBlt,但它仅仅实现渐变。发音为“blit”,这个术语是从古老的
PDP-10 BLT (块转移)指令派生而来,这个指令用于将大块内存从一个位置转移到另一个位置。AlphaBlend 的细节如
Figure 1
所示,参数简单明了,但是用 AlphaBlend 来实现渐变很繁琐,因为只调用一次是不行的,必须重复调用来产生渐变效果,用一个定时器和
0-255 之间不同的 alpha 值来控制。
为了展示 AlphaBlend 实际的工作过程,我编写了一个程序 BlendView,该程序基于我的一个图像查看程序,参见 2002 年 3
月刊的专栏文章。BlendView 可以查看各种图像文件(BMP、JPG、GIF 以及其它任何 GDI
支持的格式),但是当你打开一幅新图像时,原来的图像会渐变成新图像,如 Figure 2 所示。
Figure 2 原图像
Figure 2 渐变的图像
Figure 2 最终的图像
为了将一幅图像渐变为另一幅,你需要两幅图像,当用户打开一个新文档时,MFC 要做的第一件事情是销毁旧的那个对象。所以你考虑在 MFC
加载新图像前将旧图像保存在某个地方。因为渐变效果概念上属于视图处理(绘制图像范畴),所以我把处理过程放在在视图(View)中。也就是说在视图中保存旧图像。但视图如何知道何时要保存图像呢?你当然得告诉它。幸运的是,CDocument
具备一个方法,你可以用它来随时通知视图发生了什么。这个方法就是 CDocument::UpdateAllViews:
// in Doc.cpp: BOOL CPictureDoc::OnOpenDocument(LPCTSTR lpszPathName) { UpdateAllViews(NULL, PREOPENDOC, this); return m_pict.Load(lpszPathName); }
PREOPENDOC 是我自己的枚举代码,在 doc.h 中定义。当你调用 UpdateAllViews 时,将自己的“提示代码”(一个32位整数)随一个指针传递到“提示对象”,该对象可以是任何 CObject 派生的 MFC 类。这里我传的是文档本身。注意我是在加载新图像之前调用 UpdateAllViews,而旧图像仍然有效。视图处理通知消息保存该图像:
void CPictureView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { if (lHint==CPictureDoc::PREOPENDOC) { SaveDocImage((CPictureDoc*)pHint); } }
相同的 OnUpdate
函数处理所有文档的通知消息,所以你得检查发送了哪个通知消息。一般情况下,提示代码和提示对象背后的工作原理是文档以提示方式提供信息,告诉视图它需要更新屏幕的哪一部份。对于 CPictureView
来说,如果提示代码是 PREOPENDOC,那么 CPictureView则调用一个辅助函数 SaveDocImage
来保存当前图像。Figure 3 是 SaveDocImage
的代码,它创建一个位图和内存设备上下文(DC),然后在内存设备上下文中呈现图像,在文档摧毁原来图像后有效地进行渐变拷贝。
现在,当用户打开一个新文件,文档通知视图以及 OnUpdate 处理例程以位图形式保存图像。渐变是怎么做出来的呢?它需要重复调用 AlphaBlend
从老图像渐变成新图像。最显而易见的方法是设置一个定时器。假设你想用三秒来渐变。为了用 100 步来实现渐变,你可以将定时期设置成
3000/100=30毫秒。但问题是 AlphaBlend实际上花了大量的时间来处理渐变。而且,所花的时间依赖于图像的大小。较大的图像渐变的时间较长。如果你使用定时器来做,最后得到的幻灯效果是较小的图像更快,较大的图像更慢——可能不是你想要的结果。
保持渐变时间为常量的方法是固定持续时间——假设为 3,000 毫秒——然后根据实际逝去的时间计算 alpha 值,假设第一次迭代发生在
t 20 毫秒。那么你可以用的 alpha 值为 20*255/3000 = 1 (取最近似的一个整数)。然后根据当前时间计算的 alpha
值立即进行另一次渐变。如果逝去的时间超过一半,你最终的 alpha 值是 .5。通过用实际逝去的时间计算 alpha
值,你可以保证渐变总是按时完成,但缺点是较大的图像无法平滑地完成渐变,因为它们的迭代过程更少,在 AlphaBlend 中花的时间更多。
所有这些实现难易程度不一。Figure 3 和
Figure 4 是详细代码。当 CPictureView::OnUpdate 获得 PREOPENDOC
通知时,保存旧图像之后,它将数据成员 m_iStartTime
置为当前时钟时间。时钟时间是自该进程启动后的“时钟嘀嗒”数。每秒嘀嗒数为 CLOCKS_PER_SEC(通常为 1,000)。当 OnUpdate
返回时,控制传回到文档和 MFC,它调用视图的 OnInitialUpdate 函数,该函数调用 OnUpdate,它重画窗口。最后,Windows
向你的视图发送 MW_PAINT 消息,MFC 通过调用视图的虚拟 OnDraw 方法处理该消息。这是 MFC 的基本常识:在某个视图中,绘图在
OnDraw 进行,而不是 OnPaint 中。CPictureView 是这样绘制的:
void CPictureView::OnDraw(CDC* pDC) { CPicture* ppic = // get current picture if (m_iStartTime) { // do blend } else { // render as normal ppic->Render(pDC,rc); } }
我省略了渐变的细节,主要突出 CPictureView 如何用 m_iStartTime 作为标志来确定是否渐变。以下是实现渐变需要的基本步骤。
在画面以外的内存 DC 中进行渐变然后拷贝到屏幕这一步是很重要的;否则用户将会看到一闪而过的中间图像。因为 AlphaBlend 需要设备上下文,而不是 CPicture 对象,首先绘制新图像(所以我调用 CPicture::Render ),然后在其上渐变旧图像要方便一些。所以我用的 alpha 值与先从旧的图像开始显示所用的 alpha 值相反转(1-alpha) ,换句话说,不是先从旧图像开始,然后在上面以越来越多的效果渐变新图像。我是先从新图像开始,然后在上面以越来越少的效果渐变旧图像。很聪明,不是吗?网格效果处理方法一样。以下是计算 AlpahBlend alpha 值的关键代码行:
int alpha = ((clock() - m_iStartTime) * 255) / BLEND_DURATION; alpha = max(255-alpha,0);
渐变之后,如果计算的 alpha 值大于 0,那么就需要处理更多的渐变效果。所以 OnDraw 调用 Invalidate(FALSE)
在不擦除背景的情况下而重画窗口。Windows 发送另一个 WM_PAINT 消息——只是要等到当前消息处理完成。这样一来(使 WM_PAINT
为有效消息),没有阻塞。在渐变期间,用户仍然能使用应用程序。你可以在渐变期间改变窗口大小来证明这一点。CPictureView
在新的窗口尺寸下保持渐变。
如果算出的 alpha 值为 0,渐变完成。计时器停止。这时,OnDraw 调用辅助函数 StopBlending,该函数删除旧图像并将
m_iStartTime 设置为 0,暗示 OnDraw 停止渐变。现在当视图需要绘制时,OnDraw 通过调用 CPicture::Render
进行常规绘制,直接呈现新图像,不发生渐变。
如果你使用活动模板库(ATL)CImage 类来保存图像(而不是用 CPicture,这是我很久以前实现的一个类,当时 CImage
还未出现),你可以用 CImage::AlphaBlend,不过用它来进行渐变会产生一些开销。
如果你使用微软的 .NET 框架,你可以用 Graphics.DrawImage 重载方法函数之一来进行 alpha 渐变,该重载有一个 ImageAttributes
对象参数。ImageAttributes 中的一个方法是 SetColorMatrix。颜色矩阵为一个5x5 矩阵,定义红、绿、蓝颜色映射以及
alpha 加第五个 w 通道,对角线上必须是 1,其它地方必须为
0(学过数学的的人都知道,第五通道被用于实现非线性转换)。为了完成半透明渐变,你得用单位矩阵(对角线上为 1,其余都为 0),然后将 alpha
值(ColorMatrix.Matrix33)置为 .5f 并用它绘制图像。
顺祝编程愉快!
您的提问和评论可发送到 Paul 的信箱:cppqa@microsoft.com.
下载本文示例代码
C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...