GDI双缓冲

翻译自Double buffering,原作者Dim_Yimma_H

语言:C

(原文写的是C++,实际上是纯C)

推荐知识:

  • 构建程序
  • 函数
  • 结构体
  • 变量和条件语句
  • switch语句
  • 循环
  • 指针
  • 创建窗口

教程

为了构建这个应用,你需要链接这两个库:User32.lib, Gdi32.lib。

你也需要把开发环境中的字符集设定为多字节,因为如果字符集是Unicode的话,MessageBox和CreateWindow都会有不同的定义。如果你在用Visual C++ 2005,你可以这样做:打开菜单"Project",点击"Properties",点开"Configuration Properties",选择"General",看到"Character Set"右边,把它从"Use Unicode Character Set"改成"Use Multi-Byte Charater Set",点击"OK"。

双缓冲的优势是消除闪烁(flickering)。当持续的直接绘制东西到一个窗口上,并且在绘制完成前就更新窗口,导致当前一次更新后有一部分画面不可见,就产生了闪烁。在下一次更新期间,另一部分又会不可见。为了得到一个更稳定的更新,我们把需要绘制的全部内容都绘制到一个备用缓冲(backbuffer)上,然后backbuffer会被绘制到窗口上。闪烁问题于是被解决:因为不再有部分画面不可见的问题了。同时也带来了另一个问题:水平撕裂(horizontal tearing)。当窗口的一半是现在更新的,而另一半是上次更新的,就出现了horizontal tearing。不过这通常来说是可以接受的。

本教程包括基本的Win32图形编程(Win32 graphics programming)以及如何使用backbuffer。我会分段解释相关代码,文末则给出完整代码。其中一部分是Win32 API,它的目的是处理图形设备(graphics devices) - GDI (graphics device interface)。每个窗口有一个设备上下文(device context),它描述了窗口属性(例如像素格式)。在调用绘制函数时,device context也被用作窗口和位图的标识符。因此,如果我们想在窗口中绘制像素、形状或者位图,我们需要先找到它的device context。通过调用GetDC函数,它返回的是一个HDC(handle to a device context),可以得到这个窗口的device context。GetDC函数需要的参数:一个HWND,也就是我们想要获取context device的那个窗口的HWND。在我们的函原型部分,我们会声明一个函数,后面会用到它,现在则不用想太多。我们同时定义两个宏,表示窗口的宽度和高度,后面用起来会很方便。

  1. #define WINDOW_WIDTH 640
  2. #define WINDOW_HEIGHT 480
  3. // function prototype - PaintRectangle
  4. int PaintRectangle(SHORT left,
  5. SHORT top,
  6. USHORT width,
  7. USHORT height,
  8. COLORREF color,
  9. HDC hDstDC);

我们的backbuffer是一个位图。CreateCompatibleBitmap()函数返回一个HBITMAP,因此通过调用CreateCompatibleBitmap()来获取位图。CreateCompatibleBitmap 函数有三个参数:第一个是HDC类型,描述了我们想要bitmap兼容到的窗口。另外两个参数是位图的宽度和高度。我们现在声明这些变量,以及一个rectangle,这个rectangle后续会被backbuffer绘制出来。

  1. // GDI related variables
  2. HWND hWnd; // handle to the main window
  3. HBITMAP hBackbuffer; // handle to the backbuffer bitmap
  4. HDC hWndDC; // handle to the device context of the main window
  5. HDC hBackbufferDC; // handle to the device context of the backbuffer
  6. // rectangle variables
  7. int left = 0, top = 0; // upper left corner
  8. USHORT width=32, height=24; // size of the rectangle
  9. COLORREF color=RGB(255, 255, 255); // create a 32-bit color using the RGB macro

我们将对主窗口类的style成员做一点小的调整:要求这个窗口指定唯一的device context。

  1. wc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;

当窗口被创建后,一个好的做法是获取它的device context,并创建一个backbuffer和相应的device context。这个backbuffer位图并不会自动有相应的device context,而是要通过CreateCompatibleDC来创建。然后通过调用SelectObject()来描述这个backbuffer。SelectObject()函数的第一个参数是这个HDC,第二个参数是一个GDI对象,例如HBITMAP。

  1. /*
  2. Retrieve the window device context,
  3. and create a device compatible with it.
  4. Then select it to describe hBackbuffer.
  5. */
  6. hWndDC = GetDC(hWnd);
  7. hBackbuffer = CreateCompatibleBitmap(hWndDC, WINDOW_WIDTH, WINDOW_HEIGHT);
  8. hBackbufferDC = CreateCompatibleDC(hWndDC);
  9. SelectObject(hBackbufferDC, hBackbuffer);

为了真的用上double buffering,我们的程序将会是一个实时应用(real time application)。因而,需要修改我们的消息循环相应代码:GetMessage()会休眠,直到获取到一个message;我们换用PeekMessage(),它如果没有得到一个message,就立即返回。注意到也有可能一连串message被一起发送过来,因此Windows有过一个消息队列(message queue)。我们在调用PeekMessage()时指定最后一个参数为PM_REMOVE,作用是删除那些已经被接收到的消息,而不是处理它们两次。

  1. // message loop
  2. while (true)
  3. {
  4. // peek messages for all windows belonging to this thread
  5. // PM_REMOVE specifies that
  6. if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
  7. {
  8. if (msg.message === WM_QUIT) // the window has been destroyed
  9. break;
  10. DispatchMessage(&msg); // send the message to the window procedure
  11. }
  12. else
  13. {
  14. // idle process - do the drawing here

消息循环的这一部分被叫做idle process(空闲进程),因为当前没有消息被接收到。我们知道,窗口最上面的像素数量最少,越往下越多。第一个if条件语句判断的是,我们创建的矩形(前面有提到过一次)是否为挨着窗口右边,如果是的话就随机生成一个位置、尺寸和颜色;而如果矩形还在窗口内,则通过不断的增加“left”变量的值来让矩形向右滑动。

  1. if (left >= WINDOW_WIDTH)
  2. {
  3. // randomize the color of the rectangle
  4. color = RGB(rand() % 255, rand() % 255, rand() % 255);
  5. // randomize the width and height of the rectangle
  6. width = rand() % WINDOW_WIDTH
  7. height = rand() % WINDOW_HEIGHT
  8. // randomize the positioon of the rectangle
  9. left = - width;
  10. top = rand() % (WINDOW_HEIGHT - height);
  11. }
  12. else
  13. {
  14. left += 2; // move rectangle to the right
  15. }
  16. // clear the backbuffer by painting a blank (0) rectangle over it
  17. PaintRectangle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, hBackbufferDC);
  18. // paint the moving rectangle onto the backbuffer
  19. PaintRectangle(left, top, width, height, color, hBackbufferDC);

GDI提供了一个叫做BitBlt的函数,它做的是bit-block transfer(拷贝一个bitmap到另一个)。我们要做的是拷贝backbuffer到窗口,BitBlt函数对此来说是完美的。BitBlt的函数参数如下:目标device context,left, top, width, height, 源device context, 源left, 源top, 光栅操作标识符(raster operation flag)。我们把光栅操作标识符设定为SRCCOPY来表示说我们只想把源BITMAPk(backbuffer)拷贝到窗口。此外还有其他的光栅操作标识符类型,都是使用位操作来结合位图的像素,从而创建不同的效果。

  1. // copy the whole backbuffer to the main window
  2. BitBlt(hWndDC, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
  3. hBackbufferDC, 0, 0,
  4. SRCCOPY);
  5. // drawing is complete, pause the program for 30 ms by calling Sleep
  6. // thie limits the speed and lowers the CPU usage
  7. Sleep(30);
  8. }
  9. // end of message loop
  10. }

我们创建的对象,一旦用完就应该删掉,因此当结束程序之前我们删掉backbuffer和相应的device context,因为它们是我们手动创建的。窗口的device context则应该被释放而不是删除,因为它是被找到的(retrieved)而不是被创建的(created)。

  1. // this should be done before exiting the program:
  2. ReleaseDC(hWnd, hWndDC); // retrieved device contexts are just released
  3. DeleteDC(hBackbufferDC); // created device contexts must be deleted
  4. DeleteObject(hBackbuffer); // created objects must be deleted

最后,我们过一遍PaintRectangle函数。之所以现在才看这个函数,是因为觉得对应该对device context有一个整体的印象。PaintRectangle函数有两个位置参数:left和top。如果它们俩都等于0,那么会在左上角绘制。width和height则表示了矩形框的尺寸,颜色参数表示整个框的颜色;hDstDC参数则是目标device context的句柄。因此如果它是backbuffer的device context的话,那么就绘制矩形框到backbuffer上。使用了两种新的数据类型:一个是RECT,是矩形的left、top、right、bottom的简单封装的结构体;另一个是HBRUSH,它是brash的句柄。brush意思是刷子,是按指定的颜色来填充指定区域的。HBRUSH是一个GDI对象,我们通过CreateSolidBrush来创建,而当用完的时候,通过DeleleObject来释放它。为了绘制真正的矩形,需要调用FillRect函数。FillRect函数的第一个参数是目标device context,第二个参数则是RECT的地址,RECT表示了需要绘制的区域;最后一个参数则是brush句柄。FillRect返回了一个int变量,随后被PaintRectangle返回,如果成功的话它的值非0。

  1. int PaintRectangle(SHORT left, SHORT top, USHORT width, USHORT height, COLORREF color, HDC hDstDC)
  2. {
  3. if (hDstDC == NULL)
  4. {
  5. return -1; // no device context specified - return
  6. }
  7. int returnValue;
  8. // RECT struct instance to paint
  9. RECT rect = {left, top, left+width, top+height};
  10. HBRUSH hBrush;
  11. hBrush = CreateSolidBrush(color);
  12. returnValue = FillRect(hDstDC, &rect, hBrush);
  13. DeleteObject(hBrush);
  14. return returnValue;
  15. }

搞定!完整代码如下

(简单起见,我把入口函数从WINMAIN改成main,因此直接创建VS的控制台程序即可)

  1. //from http://www.geocities.ws/dim_yimma_h/cdoublebuffering.htm
  2. #include <stdio.h>
  3. #include <stdbool.h>
  4. #include <windows.h>
  5. #define WINDOW_WIDTH 640
  6. #define WINDOW_HEIGHT 480
  7. //function prototype - PaintRectangle
  8. int PaintRectangle(SHORT left,
  9. SHORT top,
  10. USHORT width,
  11. USHORT height,
  12. COLORREF color,
  13. HDC hDstDC);
  14. //declare window procedure
  15. LRESULT WINAPI MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
  16. int main()
  17. {
  18. HINSTANCE hInstance = GetModuleHandle(0);
  19. MSG msg; //a message struct instance
  20. WNDCLASS wc; //a window class instance
  21. //GDI related variables
  22. HWND hWnd; //handle to the main window
  23. HBITMAP hBackbuffer; //handle to the backbuffer bitmap
  24. HDC hWndDC; //handle to the device context of the main window
  25. HDC hBackbufferDC; //handle to the device context of the backbuffer
  26. //rectangle variables
  27. int left = 0, top = 0; //upper left corner
  28. USHORT width = 32, height = 24; //size of the rectangle
  29. COLORREF color = RGB(255, 255, 255); //create a 32-bit color using the RGB macro
  30. //set the member variables of the main window class instance
  31. wc.lpszClassName = "MainWindowClass"; //string identifier for this class instance
  32. wc.lpfnWndProc = MainWndProc; //the name (address) of the window procedure
  33. /* CS_HREDRAW and CS_VREDRAW specifies the window should be redrawn,
  34. both when resized horizontally and vertically.
  35. hInstance specifies program instance a created window belongs to. */
  36. wc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
  37. wc.hInstance = hInstance;
  38. /* Load the icon displayed in the corner of the window,
  39. the first argument specifies the HINSTANCE of the icon resource,
  40. since IDI_WINLOGO is a standard icon ID no HINSTANCE is specified.
  41. Also load the mouse cursor displayed when hovering over the window. */
  42. wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
  43. wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  44. //brushes are used to specify fill color (in this case the background)
  45. //the the color of a window is explicitly converted into a HBRUSH
  46. wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  47. wc.lpszMenuName = NULL; //name of window menu, we won't be creating one so it's NULL
  48. //the following two members specifies extra bytes to allocate for the window instance
  49. wc.cbClsExtra = 0;
  50. wc.cbWndExtra = 0;
  51. if (RegisterClass(&wc) == 0)
  52. {
  53. //we have no window yet thus the HWND for the messagebox is set to NULL
  54. //MB_ICONERROR specifies an error icon should be displayed in the messagebox
  55. MessageBox(NULL, "RegisterClass failed.", "Double buffering", MB_ICONERROR);
  56. return 0; //no window has been created so return without destroying it
  57. }
  58. /* create the window using the registered class,
  59. the first message for this window is sent now,
  60. it was the WM_CREATE message (see MainWndProc) */
  61. hWnd = CreateWindow("MainWindowClass", //our window class identifier
  62. "Double buffering", //caption
  63. WS_OVERLAPPEDWINDOW | WS_VISIBLE,
  64. 0, //left edge position
  65. 0, //top edge position
  66. WINDOW_WIDTH, //width of the window
  67. WINDOW_HEIGHT, //height of the window
  68. NULL, //parent HWND is NULL since this is the only window
  69. NULL, //handle to the menu of the window
  70. hInstance, //specifies which program instance window shall belong to
  71. NULL);
  72. //the last parameter can be a pointer to be sent with the WM_CREATE message
  73. //test whether the window failed to be created - in that case return 0
  74. if (hWnd == NULL) //if no handle was recieved
  75. {
  76. MessageBox(NULL, "CreateWindow failed.", "Double buffering", MB_ICONERROR);
  77. return 0;
  78. }
  79. /* Retrieve the window device context,
  80. and create a device context compatible with it.
  81. Then select it to describe hBackbuffer. */
  82. hWndDC = GetDC(hWnd);
  83. hBackbuffer = CreateCompatibleBitmap(hWndDC, WINDOW_WIDTH, WINDOW_HEIGHT);
  84. hBackbufferDC = CreateCompatibleDC(hWndDC);
  85. SelectObject(hBackbufferDC, hBackbuffer);
  86. //message loop
  87. while (true)
  88. {
  89. //peek messages for all windows belonging to this thread
  90. //PM_REMOVE specifies that
  91. if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
  92. {
  93. if (msg.message == WM_QUIT) //the window has been destroyed
  94. break;
  95. DispatchMessage(&msg); //send the message to the window procedure
  96. }
  97. else
  98. {
  99. //idle process - do the drawing here
  100. if (left >= WINDOW_WIDTH)
  101. {
  102. //randomize the color of the rectangle
  103. color = RGB(rand() % 255, rand() % 255, rand() % 255);
  104. //randomize the width and height of the rectangle
  105. width = rand() % WINDOW_WIDTH;
  106. height = rand() % WINDOW_HEIGHT;
  107. //randomize the position of the rectangle
  108. left = -width;
  109. top = rand() % (WINDOW_HEIGHT - height);
  110. }
  111. else
  112. {
  113. left += 2; //move the rectangle to the right
  114. }
  115. //clear the backbuffer by painting a black (0) rectangle over it
  116. PaintRectangle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, hBackbufferDC);
  117. //paint the moving rectangle onto the backbuffer
  118. PaintRectangle(left, top, width, height, color, hBackbufferDC);
  119. //copy the whole backbuffer to the main window
  120. BitBlt(hWndDC, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
  121. hBackbufferDC, 0, 0,
  122. SRCCOPY);
  123. //drawing is complete, pause the program for 30 ms by calling Sleep
  124. //this limits the speed and lowers the CPU usage
  125. Sleep(20);
  126. }
  127. } //end of message loop
  128. //this should be done before exiting the program:
  129. ReleaseDC(hWnd, hWndDC); //retrieved device contexts are just released
  130. DeleteDC(hBackbufferDC); //created device contexts must be deleted
  131. DeleteObject(hBackbuffer); //created objects must be deleted
  132. //return the last handled message to the caller (exit the program)
  133. return msg.wParam;
  134. } //end of WinMain
  135. LRESULT WINAPI MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
  136. {
  137. //switch statement to determine the current message
  138. switch (uMsg)
  139. {
  140. case WM_CLOSE:
  141. DestroyWindow(hWnd); //destroys this window and send WM_DESTROY message
  142. return 0;
  143. case WM_DESTROY:
  144. PostQuitMessage(0); //send WM_QUIT message with the return code 0
  145. return 0;
  146. }
  147. //send any unhandled messages to the default window procedure by calling DefWindowProc
  148. return DefWindowProc(hWnd, uMsg, wParam, lParam);
  149. }
  150. int PaintRectangle(SHORT left, SHORT top, USHORT width, USHORT height, COLORREF color, HDC hDstDC)
  151. {
  152. if (hDstDC == NULL)
  153. return -1; //no device context specified - return
  154. int returnValue;
  155. //RECT struct instance to paint
  156. RECT rect = { left, top, left + width, top + height };
  157. HBRUSH hBrush;
  158. hBrush = CreateSolidBrush(color);
  159. returnValue = FillRect(hDstDC, &rect, hBrush);
  160. DeleteObject(hBrush);
  161. return returnValue;
  162. }

GDI双缓冲的更多相关文章

  1. C# GDI+双缓冲技术

    我想有很多搞图形方面的朋友都会用到双缓冲技术的时候,而且有的时候她的确是个头疼的问题.最近我也要用双缓冲技术,程序怎么调试都不合适,当要对图形进行移动时,总是会出现闪烁抖动.在网上找了些资料,说得都不 ...

  2. (转载)GDI+双缓冲

    双缓冲在GDI+里可以有效的提高描画效率.改善显示的质量. 下面的代码是一个最简单的双缓冲的模板.可以根据需要,做简单的修改即可. Bitmap CacheImage( [Width], [Heigh ...

  3. 简单的GDI+双缓冲的分析与实现

    为什么要使用双缓冲绘制 在进行多图元绘制的时候: 因为是要一个一个画上去,所以每画一个图元,系统就要做一次图形的绘制操作,图形的重绘是很占用资源的,特别当需要重绘的图形数量很多的时候,所造成的消耗就特 ...

  4. .net WINFORM的GDI双缓冲的实现

    有时候在窗体中执行不断的GDI+操作的时候会出现闪速的状况,除了修改窗体的参数,更应该解决刷新本身的问题,双缓冲可能就是这样来的. 方法1: 用GDI绘制在位图上,然后再重新生成位图 Bitmap b ...

  5. GDI+ 双缓冲实现

    早前曾为此问题在CSDN发帖求助(GDI+ 如何使用双缓冲绘制图像),得到了一个GDI+下较可行的方法,虽然绘制效果比直接绘制要好一些,不过还不能跟GDI的双缓冲方式比肩.   现在,我终于找到了一个 ...

  6. GDI双缓冲绘图

    一.简介 在进行复杂图形绘制时,若直接在屏幕DC上进行绘制,则会出现明显的闪烁.闪烁产生的原因是当绘制的图形较为 复杂时,图形绘制过程中就被刷新到屏幕上,导致结果断断续续地显示出来.双缓冲绘图的原理是 ...

  7. VC GDI双缓冲机制绘图防屏幕闪烁实现步骤

    在OnDraw(CDC* pDC) 中添加如下代码 CDC MemDC; //首先定义一个显示设备对象 CBitmap MemBitmap;//定义一个位图对象 //随后建立与屏幕显示兼容的内存显示设 ...

  8. GDI+ 双缓冲字体模糊

    只是记录自己的UI库,对其他估计没什么帮助 void CListCtrlUI::ReFillRect(HDC hdc){ if (!m_pImage) { Graphics gs(hdc); int ...

  9. Duilib的双缓冲实现,附带GDI、WTL的双缓冲实现

    前言: 闪烁问题,之前的经验是使用双缓冲,借此机会,把双缓冲的研究心得总结下. 双缓冲的含义: 缓冲这个词,相信大家都不陌生,Cache.主要是为了解决上下游(或者模块.或者系统)等性能不匹配问题.如 ...

随机推荐

  1. 【KakaJSON手册】07_Coding_归档_解档

    KakaJSON可以只用一行代码将常用数据进行归档\解档 后面代码中会用到 file 文件路径 // 文件路径(String或者URL都可以) let file = "/Users/mj/D ...

  2. matlab学习笔记12_3串联结构体,按属性创建含有元胞数组的结构体,filenames,isfield,isstruct,orderfields

    一起来学matlab-matlab学习笔记12 12_3 结构体 串联结构体,按属性创建含有元胞数组的结构体,filenames,isfield,isstruct,orderfields 觉得有用的话 ...

  3. java CountDownLatch报错java.lang.IllegalMonitorStateException: null

    笔者使用websocket进行通信,服务器异步返回.websocket服务器又异步调用其他websocket,也是异步访问. 由于无法预测服务器调用第三方websocket什么时候调用结束,使用了Co ...

  4. 【论文阅读】FaceBoxes- CPU Real-time Face Detector with High Accuracy

    前言 参考 1. FaceBoxes_paper; 2. Faceboxes_github_tf; 3. 翻译: 4. 理解1: 5. 理解2: 完

  5. Spring的@Autowired和@Resource注入

    @Autowired的原理 Spring@Autowired注解与自动装配 @Autowired 与@Resource的区别(详细) spring不但支持自己定义的@Autowired注解,还支持几个 ...

  6. Node.js实现PC端类微信聊天软件(一)

    Github StackChat 技术栈 写这个软件StackChat的主要目的是巩固练习Node和对React的实践,也是为了学习东西,所以选用了这些自己还没在项目里使用过的技术,边学变写 Elec ...

  7. 浅谈 Docker 安全合规建设

    通过阅读网上帖子及浏览相关信息,大家可能会产生一种错觉:Docker 安全性不足,对 Docker 导入生产环境持保守态度.不过实际情况是,虽然我们需要对容器的安全性高度关注,但只要使用得当,完全可以 ...

  8. opencc介绍

    1.什么是opencc? Open Chinese Convert(OpenCC)是一个开源的中文简繁转换项目,致力于制作高质量的基于统计预料的简繁转换词库.还提供函数库(libopencc).命令行 ...

  9. JVM知识点总览-高级Java工程师面试必备

    jvm 总体梳理 jvm体系总体分四大块: 类的加载机制 jvm内存结构 GC算法 垃圾回收 GC分析 命令调优 当然这些知识点在之前的文章中都有详细的介绍,这里只做主干的梳理 这里画了一个思维导图, ...

  10. 【剑指offer】删除链表中重复的结点

    题目描述 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针.例如,链表1->2->3->3->4->4->5 处理后为 ...