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。在我们的函原型部分,我们会声明一个函数,后面会用到它,现在则不用想太多。我们同时定义两个宏,表示窗口的宽度和高度,后面用起来会很方便。

#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480 // function prototype - PaintRectangle
int PaintRectangle(SHORT left,
SHORT top,
USHORT width,
USHORT height,
COLORREF color,
HDC hDstDC);

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

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

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

wc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;

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

/*
Retrieve the window device context,
and create a device compatible with it.
Then select it to describe hBackbuffer.
*/
hWndDC = GetDC(hWnd);
hBackbuffer = CreateCompatibleBitmap(hWndDC, WINDOW_WIDTH, WINDOW_HEIGHT);
hBackbufferDC = CreateCompatibleDC(hWndDC);
SelectObject(hBackbufferDC, hBackbuffer);

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

// message loop
while (true)
{
// peek messages for all windows belonging to this thread
// PM_REMOVE specifies that
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message === WM_QUIT) // the window has been destroyed
break; DispatchMessage(&msg); // send the message to the window procedure
}
else
{
// idle process - do the drawing here

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

        if (left >= WINDOW_WIDTH)
{
// randomize the color of the rectangle
color = RGB(rand() % 255, rand() % 255, rand() % 255); // randomize the width and height of the rectangle
width = rand() % WINDOW_WIDTH
height = rand() % WINDOW_HEIGHT // randomize the positioon of the rectangle
left = - width;
top = rand() % (WINDOW_HEIGHT - height);
}
else
{
left += 2; // move rectangle to the right
} // clear the backbuffer by painting a blank (0) rectangle over it
PaintRectangle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, hBackbufferDC);
// paint the moving rectangle onto the backbuffer
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)拷贝到窗口。此外还有其他的光栅操作标识符类型,都是使用位操作来结合位图的像素,从而创建不同的效果。

        // copy the whole backbuffer to the main window
BitBlt(hWndDC, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
hBackbufferDC, 0, 0,
SRCCOPY); // drawing is complete, pause the program for 30 ms by calling Sleep
// thie limits the speed and lowers the CPU usage
Sleep(30);
}
// end of message loop
}

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

    // this should be done before exiting the program:
ReleaseDC(hWnd, hWndDC); // retrieved device contexts are just released
DeleteDC(hBackbufferDC); // created device contexts must be deleted
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。

int PaintRectangle(SHORT left, SHORT top, USHORT width, USHORT height, COLORREF color, HDC hDstDC)
{
if (hDstDC == NULL)
{
return -1; // no device context specified - return
}
int returnValue; // RECT struct instance to paint
RECT rect = {left, top, left+width, top+height};
HBRUSH hBrush; hBrush = CreateSolidBrush(color); returnValue = FillRect(hDstDC, &rect, hBrush); DeleteObject(hBrush); return returnValue;
}

搞定!完整代码如下

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

//from http://www.geocities.ws/dim_yimma_h/cdoublebuffering.htm

#include <stdio.h>
#include <stdbool.h>
#include <windows.h> #define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480 //function prototype - PaintRectangle
int PaintRectangle(SHORT left,
SHORT top,
USHORT width,
USHORT height,
COLORREF color,
HDC hDstDC); //declare window procedure
LRESULT WINAPI MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int main()
{
HINSTANCE hInstance = GetModuleHandle(0); MSG msg; //a message struct instance
WNDCLASS wc; //a window class instance //GDI related variables
HWND hWnd; //handle to the main window
HBITMAP hBackbuffer; //handle to the backbuffer bitmap
HDC hWndDC; //handle to the device context of the main window
HDC hBackbufferDC; //handle to the device context of the backbuffer //rectangle variables
int left = 0, top = 0; //upper left corner
USHORT width = 32, height = 24; //size of the rectangle
COLORREF color = RGB(255, 255, 255); //create a 32-bit color using the RGB macro //set the member variables of the main window class instance
wc.lpszClassName = "MainWindowClass"; //string identifier for this class instance
wc.lpfnWndProc = MainWndProc; //the name (address) of the window procedure /* CS_HREDRAW and CS_VREDRAW specifies the window should be redrawn,
both when resized horizontally and vertically.
hInstance specifies program instance a created window belongs to. */
wc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
wc.hInstance = hInstance; /* Load the icon displayed in the corner of the window,
the first argument specifies the HINSTANCE of the icon resource,
since IDI_WINLOGO is a standard icon ID no HINSTANCE is specified.
Also load the mouse cursor displayed when hovering over the window. */
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW); //brushes are used to specify fill color (in this case the background)
//the the color of a window is explicitly converted into a HBRUSH
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszMenuName = NULL; //name of window menu, we won't be creating one so it's NULL
//the following two members specifies extra bytes to allocate for the window instance
wc.cbClsExtra = 0;
wc.cbWndExtra = 0; if (RegisterClass(&wc) == 0)
{
//we have no window yet thus the HWND for the messagebox is set to NULL
//MB_ICONERROR specifies an error icon should be displayed in the messagebox
MessageBox(NULL, "RegisterClass failed.", "Double buffering", MB_ICONERROR);
return 0; //no window has been created so return without destroying it
} /* create the window using the registered class,
the first message for this window is sent now,
it was the WM_CREATE message (see MainWndProc) */
hWnd = CreateWindow("MainWindowClass", //our window class identifier
"Double buffering", //caption
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
0, //left edge position
0, //top edge position
WINDOW_WIDTH, //width of the window
WINDOW_HEIGHT, //height of the window
NULL, //parent HWND is NULL since this is the only window
NULL, //handle to the menu of the window
hInstance, //specifies which program instance window shall belong to
NULL);
//the last parameter can be a pointer to be sent with the WM_CREATE message //test whether the window failed to be created - in that case return 0
if (hWnd == NULL) //if no handle was recieved
{
MessageBox(NULL, "CreateWindow failed.", "Double buffering", MB_ICONERROR);
return 0;
} /* Retrieve the window device context,
and create a device context compatible with it.
Then select it to describe hBackbuffer. */
hWndDC = GetDC(hWnd);
hBackbuffer = CreateCompatibleBitmap(hWndDC, WINDOW_WIDTH, WINDOW_HEIGHT);
hBackbufferDC = CreateCompatibleDC(hWndDC);
SelectObject(hBackbufferDC, hBackbuffer); //message loop
while (true)
{
//peek messages for all windows belonging to this thread
//PM_REMOVE specifies that
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT) //the window has been destroyed
break; DispatchMessage(&msg); //send the message to the window procedure
}
else
{
//idle process - do the drawing here
if (left >= WINDOW_WIDTH)
{
//randomize the color of the rectangle
color = RGB(rand() % 255, rand() % 255, rand() % 255); //randomize the width and height of the rectangle
width = rand() % WINDOW_WIDTH;
height = rand() % WINDOW_HEIGHT; //randomize the position of the rectangle
left = -width;
top = rand() % (WINDOW_HEIGHT - height);
}
else
{
left += 2; //move the rectangle to the right
} //clear the backbuffer by painting a black (0) rectangle over it
PaintRectangle(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0, hBackbufferDC);
//paint the moving rectangle onto the backbuffer
PaintRectangle(left, top, width, height, color, hBackbufferDC);
//copy the whole backbuffer to the main window
BitBlt(hWndDC, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT,
hBackbufferDC, 0, 0,
SRCCOPY); //drawing is complete, pause the program for 30 ms by calling Sleep
//this limits the speed and lowers the CPU usage
Sleep(20);
}
} //end of message loop //this should be done before exiting the program:
ReleaseDC(hWnd, hWndDC); //retrieved device contexts are just released
DeleteDC(hBackbufferDC); //created device contexts must be deleted
DeleteObject(hBackbuffer); //created objects must be deleted //return the last handled message to the caller (exit the program)
return msg.wParam;
} //end of WinMain LRESULT WINAPI MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
//switch statement to determine the current message
switch (uMsg)
{
case WM_CLOSE:
DestroyWindow(hWnd); //destroys this window and send WM_DESTROY message
return 0; case WM_DESTROY:
PostQuitMessage(0); //send WM_QUIT message with the return code 0
return 0;
} //send any unhandled messages to the default window procedure by calling DefWindowProc
return DefWindowProc(hWnd, uMsg, wParam, lParam);
} int PaintRectangle(SHORT left, SHORT top, USHORT width, USHORT height, COLORREF color, HDC hDstDC)
{
if (hDstDC == NULL)
return -1; //no device context specified - return int returnValue; //RECT struct instance to paint
RECT rect = { left, top, left + width, top + height };
HBRUSH hBrush; hBrush = CreateSolidBrush(color); returnValue = FillRect(hDstDC, &rect, hBrush); DeleteObject(hBrush); return returnValue;
}

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. New & make in go_Code

    概述 Go 语言中的 new 和 make 一直是新手比较容易混淆的东西,咋一看很相似.不过解释两者之间的不同也非常容易. 他们所做的事情,和应用的类型也不相同. 二者都是用来分配空间. new 函数 ...

  2. Better ultra_simple for Slamtec RPLIDAR on Linux

    Improved the ultra_simple program to visualize the samples with GLUT on Linux, tested with Slamtec R ...

  3. IDEA 使用SequenceDiagram插件绘制时序图

    最近看代码,由于代码的调用层级深度比较多,层层深入到某处时,已经忘记了身处何处,虽然自己可以使用一些画图工具来时序图,但是,这种情况下,自己画时序图很繁琐,比较浪费时间,上网找了一下IDEA有一个插件 ...

  4. 〓经典文字MUD武侠游戏 我的江湖 〓

    〓经典文字MUD武侠游戏 我的江湖 〓 我的江湖(FFLIB)基于地狱内核扩展,目前已扩展了很多实用功能! 我的江湖玩法 和掌心西游.书剑.东方故事.侠缘.武林等玩法大同小异 但扩展了更多好玩的玩法, ...

  5. iOS——学习网址收集

    1 一个比系统自带的终端好用的软件:http://www.iterm2.com 2 学习和遇到技术问题可以去的网站: CocoaChina      http://developer.cocoachi ...

  6. odoo self.ensure_one()

    源码: def ensure_one(self): """ Verifies that the current recorset holds a single recor ...

  7. Ant Design 学习记录

    遇到的问题: 点击列表中的一个字段 , 显示出一条指定id(其他筛选条件的)数据 解决这个问题之前,要先了解 Antd的 Table中的  Column  列描述数据对象,是 columns 中的一项 ...

  8. 编程语言与python介绍

    目录 一.编程语言的发展史 1.1 机器语言 1.2 汇编语言 1.3 高级语言 1.3.1 编译型 1.3.2 解释型 1.4 总结 2.python介绍 2.1 python解释器版 2.2 运行 ...

  9. 对decimal 类型的数据进行获取调整

    Decimal为SQL Server.MySql等数据库的一种数据类型,不属于浮点数类型,可以在定义时划定整数部分以及小数部分的位数. 好处:使用精确小数类型不仅能够保证数据计算更为精确,还可以节省储 ...

  10. @PropertySource绝对路径(java.io.FileNotFoundException问题解决)

    经常使用@PropertySource 来指定配置文件,然后@value获取配置参数: @Component @PropertySource(value= {"classpath:rules ...