分类: C/C++
2008-08-07 10:32:42
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.