在编写游戏或其它任何需要在一秒钟内刷新许多次屏幕显示的程序时,总是会屏幕闪烁,这是因为显示器背后的电子枪在刷新显示图像时,是从屏幕的左上角开始,水平的扫描到最右端,然后移回最左边并下移一个像素,继续向右扫描,当扫描到屏幕的最右下角时又回到左上角重新开始。连续两次扫描到左上角的时间间隔,称为垂直刷新周期,每秒钟此过程的重复次数称为垂直刷新率,也称作帧率,画面刷新的过程造成了闪烁。解决办法是使用后备缓冲来防止闪烁。
前端缓冲是一块直接映射到显示器的内存区域,在上面绘制任何东西时都会立即显示出来。后备缓冲则是在内存中再创建一个区域,这个区域的格式和大小与前端缓冲器完全相同,在这个后备缓冲区中进行绘画,由于这个缓冲区在画的时候不会有任何图形显示出来,需要做的就是将后备缓冲区中画好的内容复制到前台缓冲区(这个复制过程通常叫做位块传送),这种传送速度极快,屏幕上的刷新不会因电子枪的移动而混乱,这样就实现了不闪烁的动画显示,这种技术一般称为双缓冲技术,有时也称页面切换技术。
下面就来体验一下双缓冲技术,这里我使用了CodeBlock+MinGW 作为开发环境。首先作一些准备工作,建立一个基本的工程。
新建一个Win32 GUI Project:
选择基于Frame 的类型:
继续,设置好项目名称的保存路径:
再设置编译器,我这里选择了GNU的GCC 编译器:
现在CodeBlocks自动生成了一个main.cpp文件,包含着一个创建GUI应用的基础代码:
代码如下(main.cpp):
#if defined(UNICODE) && !defined(_UNICODE)
#define _UNICODE
#elif defined(_UNICODE) && !defined(UNICODE)
#define UNICODE
#endif
#include <tchar.h>
#include <windows.h>
/* Declare Windows procedure */
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);
/* Make the class name into a global variable */
TCHAR szClassName[ ] = _T("CodeBlocksWindowsApp");
int WINAPI WinMain (HINSTANCE hThisInstance,
HINSTANCE hPrevInstance,
LPSTR lpszArgument,
int nCmdShow)
{
HWND hwnd; /* This is the handle for our window */
MSG messages; /* Here messages to the application are saved */
WNDCLASSEX wincl; /* Data structure for the windowclass */
/* The Window structure */
wincl.hInstance = hThisInstance;
wincl.lpszClassName = szClassName;
wincl.lpfnWndProc = WindowProcedure; /* This function is called by windows */
wincl.style = CS_DBLCLKS; /* Catch double-clicks */
wincl.cbSize = sizeof (WNDCLASSEX);
/* Use default icon and mouse-pointer */
wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
wincl.lpszMenuName = NULL; /* No menu */
wincl.cbClsExtra = 0; /* No extra bytes after the window class */
wincl.cbWndExtra = 0; /* structure or the window instance */
/* Use Windows's default colour as the background of the window */
wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;
/* Register the window class, and if it fails quit the program */
if (!RegisterClassEx (&wincl))
return 0;
/* The class is registered, let's create the program*/
hwnd = CreateWindowEx (
0, /* Extended possibilites for variation */
szClassName, /* Classname */
_T("Code::Blocks Template Windows App"), /* Title Text */
WS_OVERLAPPEDWINDOW, /* default window */
CW_USEDEFAULT, /* Windows decides the position */
CW_USEDEFAULT, /* where the window ends up on the screen */
544, /* The programs width */
375, /* and height in pixels */
HWND_DESKTOP, /* The window is a child-window to desktop */
NULL, /* No menu */
hThisInstance, /* Program Instance handler */
NULL /* No Window Creation data */
);
/* Make the window visible on the screen */
ShowWindow (hwnd, nCmdShow);
/* Run the message loop. It will run until GetMessage() returns 0 */
while (GetMessage (&messages, NULL, 0, 0))
{
/* Translate virtual-key messages into character messages */
TranslateMessage(&messages);
/* Send message to WindowProcedure */
DispatchMessage(&messages);
}
/* The program return-value is 0 - The value that PostQuitMessage() gave */
return messages.wParam;
}
/* This function is called by the Windows function DispatchMessage() */
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) /* handle the messages */
{
case WM_DESTROY:
PostQuitMessage (0); /* send a WM_QUIT to the message queue */
break;
default: /* for messages that we don't deal with */
return DefWindowProc (hwnd, message, wParam, lParam);
}
return 0;
}
View Code
这份代码可以直接编译运行(如果你的编译器已经设置好了的话),点击构建并运行:
正常显示:
OK,准备工作已经搞好,可以开始双缓冲技术的实现了!
先看看不使用双缓冲时的情况,加入一个小球不停的移动。
ball.h 定义一个小球的结构和半径,最大速率等:
#ifndef __BALL_H
#define __BALL_H
//小球的半径
#define RADIUS 10
//小球的最大速率
#define MAX_VELOCITY 5
struct SBall
{
//小球的坐标
int posX;
int posY;
//小球的速度
int velX;
int velY;
SBall(){}
};
//产生一个范围的随机数
inline int RandInt(int x, int y)
{
return rand() % (y - x + 1) + x;
}
#endif // __BALL_H
View Code
main.cpp 中包含 time.h 和 ball.h,修改消息循环的流程并加入小球移动:
#if defined(UNICODE) && !defined(_UNICODE)
#define _UNICODE
#elif defined(_UNICODE) && !defined(UNICODE)
#define UNICODE
#endif
#include <tchar.h>
#include <windows.h>
#include <time.h>
#include "ball.h"
/* Declare Windows procedure */
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);
/* Make the class name into a global variable */
TCHAR szClassName[ ] = _T("CodeBlocksWindowsApp");
int WINAPI WinMain (HINSTANCE hThisInstance,
HINSTANCE hPrevInstance,
LPSTR lpszArgument,
int nCmdShow)
{
HWND hwnd; /* This is the handle for our window */
MSG messages; /* Here messages to the application are saved */
WNDCLASSEX wincl; /* Data structure for the windowclass */
/* The Window structure */
wincl.hInstance = hThisInstance;
wincl.lpszClassName = szClassName;
wincl.lpfnWndProc = WindowProcedure; /* This function is called by windows */
wincl.style = CS_DBLCLKS; /* Catch double-clicks */
wincl.cbSize = sizeof (WNDCLASSEX);
/* Use default icon and mouse-pointer */
wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
wincl.lpszMenuName = NULL; /* No menu */
wincl.cbClsExtra = 0; /* No extra bytes after the window class */
wincl.cbWndExtra = 0; /* structure or the window instance */
/* Use Windows's default colour as the background of the window */
wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND;
/* Register the window class, and if it fails quit the program */
if (!RegisterClassEx (&wincl))
return 0;
/* The class is registered, let's create the program*/
hwnd = CreateWindowEx (
0, /* Extended possibilites for variation */
szClassName, /* Classname */
_T("Code::Blocks Template Windows App"), /* Title Text */
WS_OVERLAPPEDWINDOW, /* default window */
CW_USEDEFAULT, /* Windows decides the position */
CW_USEDEFAULT, /* where the window ends up on the screen */
544, /* The programs width */
375, /* and height in pixels */
HWND_DESKTOP, /* The window is a child-window to desktop */
NULL, /* No menu */
hThisInstance, /* Program Instance handler */
NULL /* No Window Creation data */
);
/* Make the window visible on the screen */
ShowWindow (hwnd, nCmdShow);
/* Run the message loop. It will run until GetMessage() returns 0 */
//修改消息循环的流程
bool bDone = false;
while (!bDone)
{
while (PeekMessage (&messages, NULL, 0, 0, PM_REMOVE)){
if (messages.message == WM_QUIT) {
bDone = true;
} else {
/* Translate virtual-key messages into character messages */
TranslateMessage(&messages);
/* Send message to WindowProcedure */
DispatchMessage(&messages);
}
}
InvalidateRect(hwnd, NULL, TRUE);
UpdateWindow(hwnd);
}
/* The program return-value is 0 - The value that PostQuitMessage() gave */
return messages.wParam;
}
/* This function is called by the Windows function DispatchMessage() */
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//创建画笔
static HPEN bluePen = CreatePen(PS_SOLID, 1, RGB(0, 0, 255));
static HPEN oldPen = NULL;
//创建画刷
static HBRUSH redBrush = CreateSolidBrush(RGB(255, 0, 0));
static HBRUSH oldBrush = NULL;
//两个变量保存窗口大小
static int cxClient, cyClient;
//创建一个小球
static SBall *ball = new SBall();
switch (message) /* handle the messages */
{
case WM_CREATE:
{
//获取窗口大小
RECT rect;
GetClientRect(hwnd, &rect);
cxClient = rect.right;
cyClient = rect.bottom;
//随机数种子
srand((unsigned)time(NULL));
//设置小球初始的位置和速度
ball->posX = RandInt(0, cxClient);
ball->posY = RandInt(0, cyClient);
ball->velX = RandInt(0, MAX_VELOCITY);
ball->velY = RandInt(0, MAX_VELOCITY);
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
//选用新的画笔和画刷
oldPen = (HPEN)SelectObject(ps.hdc, bluePen);
oldBrush = (HBRUSH)SelectObject(ps.hdc, redBrush);
//检查如果小球到了边界,则改变方向
if ((ball->posX >= cxClient) || (ball->posX < 0)) {
ball->velX = -ball->velX;
}
if ((ball->posY >= cyClient) || (ball->posY < 0)) {
ball->velY = -ball->velY;
}
//更新小球的位置
ball->posX += ball->velX;
ball->posY += ball->velY;
//绘制小球
Ellipse(ps.hdc,
ball->posX - RADIUS,
ball->posY - RADIUS,
ball->posX + RADIUS,
ball->posY + RADIUS);
//恢复原来的画笔和画刷
SelectObject(ps.hdc, oldPen);
SelectObject(ps.hdc, oldBrush);
EndPaint(hwnd, &ps);
Sleep(10);
}
break;
case WM_KEYDOWN:
{
//当按下Esc键时退出程序
switch (wParam) {
case VK_ESCAPE:
PostQuitMessage(0);
}
}
break;
case WM_SIZE:
{
//改变窗口大小时更新变量
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
}
break;
case WM_DESTROY:
{
//删除画笔
DeleteObject(bluePen);
DeleteObject(oldPen);
//删除画刷
DeleteObject(redBrush);
DeleteObject(oldBrush);
//删除小球
delete ball;
PostQuitMessage (0); /* send a WM_QUIT to the message queue */
}
break;
default: /* for messages that we don't deal with */
return DefWindowProc (hwnd, message, wParam, lParam);
}
return 0;
}
View Code
现在能看到小球不停的移动,并伴随有闪烁:
现在来加入双缓冲,先是创建后备缓冲器,大致步骤是:
1. 用 CreateCompatibleDC 创建一个内存设备,得到后备缓冲区的hdc;
2. GetDC 取得前端客户区的hdc,然后用 CreateCompatibleBitmap 创建相容的位图,得到一个位图句柄;
3. 把上一步得到的位图句柄选入后备缓冲区的hdc,别忘了用 ReleaseDC 释放客户区的hdc;
创建好后,就可以使用后备缓冲器了:
1. 清除后备缓冲,通常用背景色填充;
2. 在后备缓冲区的hdc中绘制,写字等;
3. 用 BitBlt 将后备缓冲区中的内容复制到前台缓冲区;
4. 在程序结束时释放资源。
5. 为了确保后备缓冲区能随用户改变窗口尺寸大小而改变,必须在 WM_SIZE 消息中删除已有的兼容位图,并创建一个新的合适大小的位图;
修改后的main.cpp:
#if defined(UNICODE) && !defined(_UNICODE)
#define _UNICODE
#elif defined(_UNICODE) && !defined(UNICODE)
#define UNICODE
#endif
#include <tchar.h>
#include <windows.h>
#include <time.h>
#include "ball.h"
/* Declare Windows procedure */
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);
/* Make the class name into a global variable */
TCHAR szClassName[ ] = _T("CodeBlocksWindowsApp");
int WINAPI WinMain (HINSTANCE hThisInstance,
HINSTANCE hPrevInstance,
LPSTR lpszArgument,
int nCmdShow)
{
HWND hwnd; /* This is the handle for our window */
MSG messages; /* Here messages to the application are saved */
WNDCLASSEX wincl; /* Data structure for the windowclass */
/* The Window structure */
wincl.hInstance = hThisInstance;
wincl.lpszClassName = szClassName;
wincl.lpfnWndProc = WindowProcedure; /* This function is called by windows */
wincl.style = CS_DBLCLKS; /* Catch double-clicks */
wincl.cbSize = sizeof (WNDCLASSEX);
/* Use default icon and mouse-pointer */
wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION);
wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION);
wincl.hCursor = LoadCursor (NULL, IDC_ARROW);
wincl.lpszMenuName = NULL; /* No menu */
wincl.cbClsExtra = 0; /* No extra bytes after the window class */
wincl.cbWndExtra = 0; /* structure or the window instance */
/* Use Windows's default colour as the background of the window */
wincl.hbrBackground = NULL;//(HBRUSH) COLOR_BACKGROUND; //注意这里设置为NULL,不然还会出现偶尔闪烁
/* Register the window class, and if it fails quit the program */
if (!RegisterClassEx (&wincl))
return 0;
/* The class is registered, let's create the program*/
hwnd = CreateWindowEx (
0, /* Extended possibilites for variation */
szClassName, /* Classname */
_T("Code::Blocks Template Windows App"), /* Title Text */
WS_OVERLAPPEDWINDOW, /* default window */
CW_USEDEFAULT, /* Windows decides the position */
CW_USEDEFAULT, /* where the window ends up on the screen */
544, /* The programs width */
375, /* and height in pixels */
HWND_DESKTOP, /* The window is a child-window to desktop */
NULL, /* No menu */
hThisInstance, /* Program Instance handler */
NULL /* No Window Creation data */
);
/* Make the window visible on the screen */
ShowWindow (hwnd, nCmdShow);
/* Run the message loop. It will run until GetMessage() returns 0 */
//修改消息循环的流程
bool bDone = false;
while (!bDone)
{
while (PeekMessage (&messages, NULL, 0, 0, PM_REMOVE)){
if (messages.message == WM_QUIT) {
bDone = true;
} else {
/* Translate virtual-key messages into character messages */
TranslateMessage(&messages);
/* Send message to WindowProcedure */
DispatchMessage(&messages);
}
}
InvalidateRect(hwnd, NULL, TRUE);
UpdateWindow(hwnd);
}
/* The program return-value is 0 - The value that PostQuitMessage() gave */
return messages.wParam;
}
/* This function is called by the Windows function DispatchMessage() */
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
//创建画笔
static HPEN bluePen = CreatePen(PS_SOLID, 1, RGB(0, 0, 255));
static HPEN oldPen = NULL;
//创建画刷
static HBRUSH redBrush = CreateSolidBrush(RGB(255, 0, 0));
static HBRUSH oldBrush = NULL;
//两个变量保存窗口大小
static int cxClient, cyClient;
//创建一个小球
static SBall *ball = new SBall();
static HDC hdcBackBuffer;
static HBITMAP hBitmap;
static HBITMAP hOldBitmap;
switch (message) /* handle the messages */
{
case WM_CREATE:
{
//获取窗口大小
RECT rect;
GetClientRect(hwnd, &rect);
cxClient = rect.right;
cyClient = rect.bottom;
//随机数种子
srand((unsigned)time(NULL));
//设置小球初始的位置和速度
ball->posX = RandInt(0, cxClient);
ball->posY = RandInt(0, cyClient);
ball->velX = RandInt(0, MAX_VELOCITY);
ball->velY = RandInt(0, MAX_VELOCITY);
//创建后备缓冲器
//1. 用 CreateCompatibleDC 创建一个内存设备,得到后备缓冲区的hdc;
hdcBackBuffer = CreateCompatibleDC(NULL);
//2. GetDC 取得前端客户区的hdc,然后用 CreateCompatibleBitmap 创建相容的位图,得到一个位图句柄;
HDC hdc = GetDC(hwnd);
hBitmap = CreateCompatibleBitmap(hdc, cxClient, cyClient);
//3. 把上一步得到的位图句柄选入后备缓冲区的hdc,别忘了用 ReleaseDC 释放客户区的hdc;
hOldBitmap = (HBITMAP)SelectObject(hdcBackBuffer, hBitmap);
ReleaseDC(hwnd, hdc);
}
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
BeginPaint(hwnd, &ps);
//使用后备缓冲器
//1. 清除后备缓冲,通常用背景色填充;
// int backColor;
// GetClassLong(hwnd, backColor);
BitBlt(hdcBackBuffer, 0, 0, cxClient, cyClient, NULL, NULL, NULL, WHITENESS);
//2. 在后备缓冲区的hdc中绘制,写字等;
//选用新的画笔和画刷
oldPen = (HPEN)SelectObject(hdcBackBuffer, bluePen);
oldBrush = (HBRUSH)SelectObject(hdcBackBuffer, redBrush);
//检查如果小球到了边界,则改变方向
if ((ball->posX >= cxClient) || (ball->posX < 0)) {
ball->velX = -ball->velX;
}
if ((ball->posY >= cyClient) || (ball->posY < 0)) {
ball->velY = -ball->velY;
}
//更新小球的位置
ball->posX += ball->velX;
ball->posY += ball->velY;
//绘制小球
Ellipse(hdcBackBuffer,
ball->posX - RADIUS,
ball->posY - RADIUS,
ball->posX + RADIUS,
ball->posY + RADIUS);
//恢复原来的画笔和画刷
SelectObject(hdcBackBuffer, oldPen);
SelectObject(hdcBackBuffer, oldBrush);
//3. 用 BitBlt 将后备缓冲区中的内容复制到前台缓冲区;
BitBlt(ps.hdc, 0, 0, cxClient, cyClient, hdcBackBuffer, 0, 0, SRCCOPY);
EndPaint(hwnd, &ps);
//必要的延时
Sleep(10);
}
break;
case WM_KEYDOWN:
{
//当按下Esc键时退出程序
switch (wParam) {
case VK_ESCAPE:
PostQuitMessage(0);
}
}
break;
case WM_SIZE:
{
//改变窗口大小时更新变量
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
//5. 为了确保后备缓冲区能随用户改变窗口尺寸大小而改变,必须在 WM_SIZE 消息中删除已有的兼容位图,并创建一个新的合适大小的位图;
//把 old bitmap选回到DC
SelectObject(hdcBackBuffer, hOldBitmap);
//删除原先创建的bitmap,否则会资源泄露
DeleteObject(hBitmap);
HDC hdc = GetDC(hwnd);
//创建和客户区有相同大小的另一个bitmap
hBitmap = CreateCompatibleBitmap(hdc, cxClient, cyClient);
ReleaseDC(hwnd, hdc);
//把新的 bitmap 选入DC
SelectObject(hdcBackBuffer, hBitmap);
}
break;
case WM_DESTROY:
{
//删除画笔
DeleteObject(bluePen);
DeleteObject(oldPen);
//删除画刷
DeleteObject(redBrush);
DeleteObject(oldBrush);
//删除小球
delete ball;
//4. 最后在程序结束时释放资源。
SelectObject(hdcBackBuffer, hOldBitmap);
DeleteObject(hdcBackBuffer);
DeleteObject(hBitmap);
PostQuitMessage (0); /* send a WM_QUIT to the message queue */
}
break;
default: /* for messages that we don't deal with */
return DefWindowProc (hwnd, message, wParam, lParam);
}
return 0;
}
View Code
这时看不到窗口闪烁了。
如果要改变窗口背景颜色的话,不能直接修改 WNDCLASSEX.hbrBackground 的值,
WNDCLASSEX.hbrBackground 只能设置为NULL,然后修改清空后备缓冲的BitBlt 处,
最后一个参数使用PATCOPY。
SelectObject(hdcBackBuffer, bkBrush); //bkBrush 是已创建好的蓝色画刷
BitBlt(hdcBackBuffer, 0, 0, cxClient, cyClient, NULL, NULL, NULL, PATCOPY);
这时窗口背景变成蓝色。