C# 中的无闪烁绘图






3.96/5 (23投票s)
2002年4月1日
4分钟阅读

266254

6189
一篇介绍避免绘图时闪烁方法的文章。

引言
无闪烁的动画绘图曾经是 Win32 和 MFC 中一个非常热门的问题。有许多优秀的文章解释了实现无闪烁动画效果的技术。正如许多读者所知,最流行的技术是使用屏幕外 DC(设备上下文)来完成所有复杂的绘图,然后将该屏幕外 DC 直接复制到屏幕 DC。这种技术也称为双缓冲。
微软将 C# 定位为 C++ 程序员的未来。因此,像许多其他 C++ 程序员一样,我利用我的一些业余时间来玩 C#,以感受它。几天前,我正在尝试用 C# 编写一个应用程序来模拟一个模拟时钟。在建立了一个基本框架并看到我的时钟工作(当然有闪烁)后,我很高兴能够使用旧的双缓冲技术让我的时钟流畅地动画。但我的第一个困境是,我找不到像 CreateCompatibleDC
、CreateCompatibleBitmap
和 SelectObject
等函数。于是我开始搜索 MSDN 并研究 Graphics
类。经过一些研究,我找到了两种实现平滑动画效果的方法,我将在下面解释这些技术。
旧式双缓冲技术
我很高兴得知 C# 中有一种方法可以使用旧的 Win32 技术来实现平滑动画。虽然找不到像 CreateCompatibleDC
、CreateCompatibleBitmap
和 SelectObject
这样的函数的直接实现,但有一个间接的方法可以为您的 GDI+ 设备上下文使用这些函数。这个想法是让 C# 知道您将使用非托管 DLL 中的一些函数。您可以使用 DllImport
属性导入 DLL 导出的函数。关于 DllImport
的详细文档可以在 .NET 文档中找到。简而言之,借助 DllImport
,我们可以告诉编译器我们将使用指定 DLL 中指定的函数。例如,
[DllImport("msvcrt.dll")]
public static extern int puts(string c);
上面的声明将使用 static 和 extern 属性声明名为 puts
的函数,该函数的实际实现将从 msvcrt.dll 导入。我使用 DllImport
从 gdi32.dll 导入所有必需的函数。为了保持事物的托管性,我声明了一个单独的类来导入所有这些函数。下面的代码显示了这个类的实际实现
/// <summary>
/// Summary description for Win32Support.
/// Win32Support is a wrapper class that imports all the
/// necessary functions that are used in old
/// double-buffering technique for smooth animation.
/// </summary>
public class Win32Support
{
/// <summary>
/// Enumeration to be used for those Win32 function
/// that return BOOL
/// </summary>
public enum Bool
{
False = 0,
True
};
/// <summary>
/// Enumeration for the raster operations used in BitBlt.
/// In C++ these are actually #define. But to use these
/// constants with C#, a new enumeration type is defined.
/// </summary>
public enum TernaryRasterOperations
{
SRCCOPY = 0x00CC0020, // dest = source
SRCPAINT = 0x00EE0086, // dest = source OR dest
SRCAND = 0x008800C6, // dest = source AND dest
SRCINVERT = 0x00660046, // dest = source XOR dest
SRCERASE = 0x00440328, // dest = source AND (NOT dest)
NOTSRCCOPY = 0x00330008, // dest = (NOT source)
NOTSRCERASE = 0x001100A6, // dest = (NOT src) AND (NOT dest)
MERGECOPY = 0x00C000CA, // dest = (source AND pattern)
MERGEPAINT = 0x00BB0226, // dest = (NOT source) OR dest
PATCOPY = 0x00F00021, // dest = pattern
PATPAINT = 0x00FB0A09, // dest = DPSnoo
PATINVERT = 0x005A0049, // dest = pattern XOR dest
DSTINVERT = 0x00550009, // dest = (NOT dest)
BLACKNESS = 0x00000042, // dest = BLACK
WHITENESS = 0x00FF0062, // dest = WHITE
};
/// <summary>
/// CreateCompatibleDC
/// </summary>
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
/// <summary>
/// DeleteDC
/// </summary>
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern Bool DeleteDC(IntPtr hdc);
/// <summary>
/// SelectObject
/// </summary>
[DllImport("gdi32.dll", ExactSpelling=true)]
public static extern IntPtr SelectObject(IntPtr hDC,
IntPtr hObject);
/// <summary>
/// DeleteObject
/// </summary>
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern Bool DeleteObject(IntPtr hObject);
/// <summary>
/// CreateCompatibleBitmap
/// </summary>
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern IntPtr CreateCompatibleBitmap(
IntPtr hObject, int width, int height);
/// <summary>
/// BitBlt
/// </summary>
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern Bool BitBlt(
IntPtr hObject,
int nXDest, int nYDest,
int nWidth, int nHeight,
IntPtr hObjSource, int nXSrc, int nYSrc,
TernaryRasterOperations dwRop);
}
现在我可以使用这个 Win32Support
类来使用我旧的技术。下面的代码片段展示了如何通过 Win32Support
在您的 Form
类中创建一个内存 DC。
Graphics memDC;
Bitmap memBmp;
memBmp = new Bitmap(this.Width, this.Height);
Graphics clientDC = this.CreateGraphics();
IntPtr hdc = clientDC.GetHdc();
IntPtr memdc = Win32Support.CreateCompatibleDC(hdc);
Win32Support.SelectObject(memdc, memBmp.GetHbitmap());
memDC = Graphics.FromHdc(memdc);
clientDC.ReleaseHdc(hdc);
这里有一个重要的注意事项:对某个 DC 上的 Graphics.GetHdc
函数的每次调用都必须与对 Graphics.ReleaseHdc
的调用配对。MSDN 关于此问题的说法是:“GetHdc 和 ReleaseHdc 方法的调用必须成对出现。在 GetHdc-ReleaseHdc 方法对的作用域内,通常只进行 GDI 函数的调用。在该作用域内对 Graphics 对象(生成 hdc 参数)的 GDI+ 方法的调用会因 ObjectBusy 错误而失败。此外,GDI+ 会忽略在随后的操作中对 hdc 参数的 Graphics 对象所做的任何状态更改。”
一旦您拥有了 memDC
,您就可以使用它进行屏幕外绘图,然后我们将使用 BitBlt
将 memDC 的内容复制到实际的屏幕 DC。
Graphics clientDC = this.CreateGraphics();
// do drawing in memDC
// do drawing in memDC
// do drawing in memDC
IntPtr hdc = clientDC.GetHdc();
IntPtr hMemdc = memDC.GetHdc();
// transfer the bits from memDC to clientDC
Win32Support.BitBlt(hdc, 0, 0, this.Width, this.Height,
hMemdc, 0, 0, Win32Support.TernaryRasterOperations.SRCCOPY);
clientDC.ReleaseHdc(hdc);
memDC.ReleaseHdc(hMemdc);
这将对您一直在尝试实现的动画产生显著影响。
当您单击“屏幕外绘图使用 BitBlt”单选按钮时,示例应用程序将使用此技术。
旧式双缓冲技术(.NET 方式)
幸运的是,我们可以在没有任何 Win32 API 直接帮助的情况下实现相同的目标。与 MFC 相比,.NET 中的图像渲染非常简单高效。Graphics
类中有两个函数用于在屏幕上渲染您的 Image
对象,它们是 DrawImage
和 DrawImageUnscaled
。使这些函数重要的事实是,.NET 在后台始终使用 BitBlt
将图像渲染到 DC。因此,如果我们能够在 Image 对象中进行屏幕外绘图,我们就可以使用这些函数直接将该对象渲染到 DC,并获得平滑的动画效果。
技术是相同的,但实现方式略有不同。在下面的代码片段中,我们使用 Bitmap
对象进行屏幕外绘图。要在某个 Image
对象上进行绘图,它必须附加到一个 Graphics
对象。我们可以使用 Graphics
的静态成员函数 FromImage
从 Image
对象创建一个新的 Graphics
对象。一旦我们从某个 Image
对象获取了一个 Graphics
对象,在该 Graphics
对象上进行的任何绘图实际上都会更改 Image
。
Bitmap offScreenBmp;
Graphics offScreenDC;
offScreenBmp = new Bitmap(this.Width, this.Height);
offScreenDC = Graphics.FromImage(offScreenBmp);
前提是我们已按照上面示例所示创建了 offScreenDC
,我们可以根据下面的代码片段实现双缓冲技术。
Graphics clientDC = this.CreateGraphics();
// do drawing in offScreenDC
// do drawing in offScreenDC
// do drawing in offScreenDC
clientDC.DrawImage(offScreenBmp, 0, 0);
我推荐这种技术,因为它不涉及任何对**非托管**代码的调用,并且本质上更简单。
当您单击“屏幕外绘图使用 Image”单选按钮时,示例应用程序将使用此技术。