Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2096037
  • 博文数量: 909
  • 博客积分: 4000
  • 博客等级: 上校
  • 技术积分: 12260
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-06 20:50
文章分类

全部博文(909)

文章存档

2008年(909)

我的朋友

分类:

2008-05-06 22:34:05

一起学习
C At Work 专栏...
创建分层窗口,实现图像渐变

原著:Paul DiLascia
翻译:NorthTibet

下载源代码:CAtWork0512.exe (493KB)
原文出处:Layered Windows, Blending Images


有没有方法创建一个半透明的窗口,并将该窗口上发生的所有鼠标事件都传递到桌面或另一个应用窗口处理?

Scott Stringham
当然可以,并且相当容易。你只要创建一个“分层窗口”即可。我写了一个小程序叫 lwtest 来示范如何做。你可以下载源代码。为了创建分层窗口,你需要扩展式样 WS_EX_LAYERED,此外,为了能在透明窗口上进行鼠标点击,你还需要 WS_EX_TRANSPARENT 扩展式样。在窗口创建之后,你可以同时设置两个式样,MFC 代码如下:
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)。那个程序运行得很好。但我现在想添加从某一张图像到下一张图像的渐变特性。我在网页中用转换效果可以做到。那么是否有办法从程序代码中实现图像渐变特性?

Bob Kline
借助 COM 确实可以在 IE 中实现转换效果。这些效果包括——渐变、擦除,框入、框出、棒状等等——在DirectX 中都支持。具体细节已经超出了本文的讨论范围,所以我只能让你去看相关文档,其内容参见“Internet Development SDK”中的“Using Transforms in C ”。你需要熟悉 COM 以及一些基本的 DirectX 知识,如:表层(surfaces)和转换(transforms)(DXSurface 和 DXTransform)。
  如果你仅仅是想实现图像到图像的渐变,我可以给你示范如何用 GDI 函数 AlphaBlend 来实现,微软的老大们在 MFC 中已经对之进行了足够友好的包装,CDC::AlphaBlend。AlphaBlend 中的 alpha 是一个图形学术语。它表示位图使用3个字节来说明一个像素:每个字节分别表示 红、绿、蓝的值。由于 32位的 DWORD 有4个字节,多余的这个字节常被用作“alpha channel”,用于指定像素的透明度。这个 alpha 值按照如下的公式来合并像素:
[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 作为标志来确定是否渐变。以下是实现渐变需要的基本步骤。

  1. 创建一个内存 DC;
  2. 在该内存 DC 中绘制位图;
  3. 计算渐变的 alpha 值;
  4. 在该内存 DC 中 AlphaBlend 位图;
  5. 将结果拷贝到屏幕(BitBlt);

在画面以外的内存 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 专栏...

阅读(287) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~