65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的无闪烁绘图

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.96/5 (23投票s)

2002年4月1日

4分钟阅读

viewsIcon

266254

downloadIcon

6189

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

引言

无闪烁的动画绘图曾经是 Win32 和 MFC 中一个非常热门的问题。有许多优秀的文章解释了实现无闪烁动画效果的技术。正如许多读者所知,最流行的技术是使用屏幕外 DC(设备上下文)来完成所有复杂的绘图,然后将该屏幕外 DC 直接复制到屏幕 DC。这种技术也称为双缓冲。

微软将 C# 定位为 C++ 程序员的未来。因此,像许多其他 C++ 程序员一样,我利用我的一些业余时间来玩 C#,以感受它。几天前,我正在尝试用 C# 编写一个应用程序来模拟一个模拟时钟。在建立了一个基本框架并看到我的时钟工作(当然有闪烁)后,我很高兴能够使用旧的双缓冲技术让我的时钟流畅地动画。但我的第一个困境是,我找不到像 CreateCompatibleDCCreateCompatibleBitmapSelectObject 等函数。于是我开始搜索 MSDN 并研究 Graphics 类。经过一些研究,我找到了两种实现平滑动画效果的方法,我将在下面解释这些技术。

旧式双缓冲技术

我很高兴得知 C# 中有一种方法可以使用旧的 Win32 技术来实现平滑动画。虽然找不到像 CreateCompatibleDCCreateCompatibleBitmapSelectObject 这样的函数的直接实现,但有一个间接的方法可以为您的 GDI+ 设备上下文使用这些函数。这个想法是让 C# 知道您将使用非托管 DLL 中的一些函数。您可以使用 DllImport 属性导入 DLL 导出的函数。关于 DllImport 的详细文档可以在 .NET 文档中找到。简而言之,借助 DllImport,我们可以告诉编译器我们将使用指定 DLL 中指定的函数。例如,

[DllImport("msvcrt.dll")] 
public static extern int puts(string c);

上面的声明将使用 static 和 extern 属性声明名为 puts 的函数,该函数的实际实现将从 msvcrt.dll 导入。我使用 DllImportgdi32.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 对象,它们是 DrawImageDrawImageUnscaled。使这些函数重要的事实是,.NET 在后台始终使用 BitBlt 将图像渲染到 DC。因此,如果我们能够在 Image 对象中进行屏幕外绘图,我们就可以使用这些函数直接将该对象渲染到 DC,并获得平滑的动画效果。

技术是相同的,但实现方式略有不同。在下面的代码片段中,我们使用 Bitmap 对象进行屏幕外绘图。要在某个 Image 对象上进行绘图,它必须附加到一个 Graphics 对象。我们可以使用 Graphics 的静态成员函数 FromImageImage 对象创建一个新的 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”单选按钮时,示例应用程序将使用此技术。

© . All rights reserved.