鉴于Microsoft Visual Studio体积巨大,我坚持不愿意安装,故在windows下编程一直都用MinGW和TCC作为编译器,用Codebocks作为开发环境。MinGW对于C/C++标准库支持很好,但是对于windows API就没那么好了。由于windows家族从win32API-> MFC->WPF->.net,体积庞大演变复杂,第三方库显然无法承载。
对于我个人工作,console下的操作使用标准库函数已经足够,文件操作使用.bat批处理,网路操作使用Python,唯一需要win32系统函数的地方就是图形界面了。这次在编写botzone的贪吃蛇程序时,平台规定用cpp+jsoncpp,为了方便调试,只好使用MinGW调用win32API库做一个简单的绘图框架。
由于GDI库过于底层,使用起来很庞杂,我的思路是写一个固定的框架代码,每隔30ms显示某内存中的固定图片(别忘了加锁),其他所有的绘制过程都归结到对于图片的图形绘制上去。这样最大限度地隔离了win32系统。
首先建立基本的主入口和消息循环函数WinMain:
#include <windows.h>
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 */
"Double Snake Game Simulator", /* Title Text */
WS_OVERLAPPEDWINDOW, /* default window */
CW_USEDEFAULT, /* Windows decides the position */
CW_USEDEFAULT, /* where the window ends up on the screen */
800, /* The programs width */
600, /* 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;
}
消息处理函数
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) /* handle the messages */
{
case WM_TIMER:
RedrawWindow(hwnd,NULL,NULL,RDW_ERASE|RDW_INVALIDATE);
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc;
RECT r;
GetClientRect(hwnd,&r);
EnterCriticalSection(&displock);
hdc=BeginPaint(hwnd,&ps);
DrawText(hdc,"Hello world",-1,&r,DT_SINGLELINE|DT_CENTER|DT_VCENTER);
EndPaint(hwnd,&ps);
LeaveCriticalSection(&displock);
printf("Paint%d!\n",num);
break;
}
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;
}
为了不断重绘,建立定时器
UINT_PTR WINAPI SetTimer(
_In_opt_ HWND hWnd, //窗口句柄
_In_ UINT_PTR nIDEvent, //定时器ID 用户指定
_In_ UINT uElapse, //定时时间ms
_In_opt_ TIMERPROC lpTimerFunc //回调函数 NULL则送入消息循环
);
若在新建定时器时指定了nIDEvent,则可在消息循环中判断wParam的取值来区分定时器消息源。本程序没有用到这一特性。本程序代码为:
SetTimer(hwnd,1,20,NULL);
并在消息处理中添加一行
case WM_TIMER:
//定时器操作
重绘窗口使用
BOOL WINAPI RedrawWindow(
HWND hwnd, //窗口句柄
CONST RECT* lprcUpdate, //重绘区域
HRGN hrgnUpdate, //重绘区域,优先
UINT flags //重绘标志位
);
标志位的设置大有讲究。Win32的重绘概念比较复杂,将窗口标为invalid和生成WM_PAINT消息是两个过程,此外还有WM_ERASEBKGND消息作为前导。
本程序中使用
RedrawWindow(hwnd,NULL,NULL,RDW_ERASE|RDW_INVALIDATE)
RedrawWindow(hwnd,NULL,NULL,RDW_ERASE|RDW_INVALIDATE|RDW_UPDATENOW)
结果一样。但是如果漏掉了前两个参数,则WM_PAINT消息仍会产生并执行相应重绘代码,但重绘后窗口界面不会更新,只有最小化或被遮挡后才行。
创建线程使用
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes=NULL,
_In_ SIZE_T dwStackSize=0,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter=NULL,
_In_ DWORD dwCreationFlags=0,
_Out_opt_ LPDWORD lpThreadId=NULL
);
若调用了C库函数,则应使用
uintptr_t _beginthread( // NATIVE CODE
void( __cdecl *start_address )( void * ),
unsigned stack_size,
void *arglist
);
本程序中,我使用
_beginthread(ThreadFunction,0,NULL);
其中执行函数ThreadFunction形式为
void ThreadFunction(void* pParam)
{
//running code
}
退出线程使用
ExitThread();
_endthread();
创建临界区使用
CRITICAL_SECTION displock;
初始化
InitializeCriticalSection(&displock);
进入临界区
EnterCriticalSection(&displock);
离开临界区
LeaveCriticalSection(&displock);
释放资源
DeleteCriticalSection(&displock);
在WM_PAINT中:
- 清除背景
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW));
- 选择画笔(用于画线)
HPEN hBluePen = CreatePen(PS_SOLID, 1, RGB(200, 0, 80)); //笔型 粗细 颜色
HPEN hOldPen = (HPEN)SelectObject(hdc, hBluePen); //返回老画笔
/********使用画笔**********/
SelectObject(hdc, hOldPen); //设置回原画笔
DeleteObject(hBluePen); //释放资源
- 选择画刷(用于填充)
HBRUSH hPurpleBrush = CreateSolidBrush(RGB(255, 0, 255));
/********使用画刷***********/
FillRect(hdc, &lprc, hPurpleBrush);
DeleteObject(hPurpleBrush);
画线
hdc = BeginPaint(hWnd, &ps);
MoveToEx(hdc, 60, 20, NULL);
LineTo(hdc, 264, 122);
EndPaint(hWnd, &ps);
折线
BOOL Polyline(HDC hdc, CONST POINT *lppt, int cPoints);
POINT Pt[7];
Pt[0].x = 20; Pt[0].y = 50;
Pt[1].x = 180; Pt[1].y = 50;
Pt[2].x = 180; Pt[2].y = 20;
hdc = BeginPaint(hWnd, &ps);
Polyline(hdc, Pt, 3);
EndPaint(hWnd, &ps);
绘制矩形
PAINTSTRUCT ps;
HDC hdc;
RECT lprc;
//GetClientRect(hwnd,&lprc); 获取窗口矩形大小
SetRect(&lprc,1,1,800,600); //left up right down
hdc=BeginPaint(hwnd,&ps);
FillRect(hdc, &lprc, (HBRUSH) (1));
EndPaint(hwnd,&ps);
绘制内存中的图片
需要使用Bitmap,概念也比较复杂。Bitmap可分为设备无关DIB和设备相关DDB两种,DDB是和dc相关的位图,不同情况下用CreateBMP(),CreateCompatibleBMP(),LoadBMP(),LoadImage()等创建的就是DDB。DIB就是一片内存,里面存储着位图掐头去尾,只留下RGB,或者像素+色板的信息。
Bitmap属于一种GDI对象,同Brushes Fonts Paths Pens Regions等对象一样,可以被指定到GDI目标设备context上。
以下函数从图像像素矩阵创建DDB句柄
HBITMAP CreateBitmap(
_In_ int nWidth, //位图宽度
_In_ int nHeight, //位图高度
_In_ UINT cPlanes, //该设备使用的颜色位面数目
_In_ UINT cBitsPerPel, //点颜色位数
_In_ const VOID *lpvBits //数据指针
);
对于彩色图像,用以下函数不需要类型转换步骤,速度更快:
HBITMAP CreateCompatibleBitmap(HDC hdc,int nWidth,int nHeight);
为了防止屏幕闪烁,新建DC上下文应用双缓冲机制。
HDC CreateCompatibleDC(
HDC hdc
);
将DC与Bitmap绑定
HDC memdc=CreateCompatibleDC(hdc);
HBITMAP memBmp=CreateCompatibleBitmap(hdc,
GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN));
SelectObject(memdc,memBmp);
将缓冲区数据复制到DIB
int SetDIBits(
_In_ HDC hdc, //设备句柄
_In_ HBITMAP hbmp, //位图句柄
_In_ UINT uStartScan, //开始行=0
_In_ UINT cScanLines, //行数
_In_ const VOID *lpvBits, //像素数据数组
_In_ const BITMAPINFO *lpbmi, //指向DIB信息的数据结构
_In_ UINT fuColorUse=DIB_RGB_COLORS
);
从DIB复制到缓冲区数据.
若lpvBits=NULL且BITMAPINFO中的biBitCount成员被初始化为零,binfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER),则当前显示设备属性将被输出。
int GetDIBits(
_In_ HDC hdc,
_In_ HBITMAP hbmp,
_In_ UINT uStartScan,
_In_ UINT cScanLines,
_Out_ LPVOID lpvBits,
_Inout_ LPBITMAPINFO lpbi,
_In_ UINT uUsage
);
其中,BITMAPINFO定义了DIB的维度和颜色信息
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO, *PBITMAPINFO;
typedef struct tagBITMAPINFOHEADER {
DWORD biSize; //自身大小
LONG biWidth; //宽度
LONG biHeight; //高度
WORD biPlanes=1;
WORD biBitCount; //位深度
DWORD biCompression; //压缩方式 一般BI_RGB
DWORD biSizeImage; //假如不是BI_JPEG或BI_PNG,设为0即可
LONG biXPelsPerMeter; //像素密度
LONG biYPelsPerMeter; //像素密度
DWORD biClrUsed=0;
DWORD biClrImportant=0;
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;
typedef struct tagRGBQUAD {
BYTE rgbBlue;
BYTE rgbGreen;
BYTE rgbRed;
BYTE rgbReserved;
} RGBQUAD;
以下是绘制图片的代码
HDC memdc=CreateCompatibleDC(hdc);
HBITMAP memBmp=CreateCompatibleBitmap(hdc,
GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN));
SelectObject(memdc,memBmp);
BITMAPINFO binfo;
ZeroMemory(&binfo,sizeof(BITMAPINFO));
binfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
binfo.bmiHeader.biBitCount=32;
binfo.bmiHeader.biCompression=0;
binfo.bmiHeader.biWidth=SCREEN_W;
binfo.bmiHeader.biHeight=SCREEN_H;
binfo.bmiHeader.biPlanes=1;
binfo.bmiHeader.biSizeImage=0;
BYTE *pBuf=new BYTE[SCREEN_H*SCREEN_W*4];
int row_size=SCREEN_W*4;
for(int i=0;i<SCREEN_H;i++)
pBuf[i*row_size+i*4]=200;
SetDIBits(memdc,memBmp,0,
binfo.bmiHeader.biHeight,
pBuf,(BITMAPINFO*)&binfo,
DIB_RGB_COLORS);
BitBlt(hdc, 0, 0, binfo.bmiHeader.biHeight, binfo.bmiHeader.biWidth,memdc, 0, 0, SRCCOPY);
DeleteDC(memdc);
DeleteObject(memBmp);
按照以上代码运行,发现重绘闪烁严重。这个网站给出了全面的解决办法。
主要问题在于windows的重绘分为两个阶段
- WM_ERASEBKGND消息:清除背景
- WM_PAINT: 绘制新内容
这样,在响应两个消息之间的间隔,屏幕为白板,从而发生闪烁。解决方法有以下两种:
- 在WM_ERASEBKGND响应代码中返回非零值
case WM_ERASEBKGND:
return 1;
- 将窗口的背景刷设为NULL
wincl.hbrBackground=0;
此外,当绘制对象过多时,必须添加双缓冲,否则将可见图像刷新过程。典型的双缓冲过程如下:
HDC hdcMem;
HBITMAP hbmMem;
HANDLE hOld;
PAINTSTRUCT ps;
HDC hdc;
....
case WM_PAINT:
// Get DC for window
hdc = BeginPaint(hwnd, &ps);
// Create an off-screen DC for double-buffering
hdcMem = CreateCompatibleDC(hdc);
hbmMem = CreateCompatibleBitmap(hdc, win_width, win_height);
hOld = SelectObject(hdcMem, hbmMem);
/*********Draw into hdcMem here*************/
/*********Draw into hdcMem here*************/
// Transfer the off-screen DC to the screen
BitBlt(hdc, 0, 0, win_width, win_height, hdcMem, 0, 0, SRCCOPY);
// Free-up the off-screen DC
SelectObject(hdcMem, hOld);
DeleteObject(hbmMem);
DeleteDC (hdcMem);
EndPaint(hwnd, &ps);
至此基本解决了闪烁问题
键盘相应
case WM_KEYDOWN:
switch (wParam)
{
case VK_UP:
SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);
break;
case VK_DOWN:
SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
break;
case VK_LEFT:
SendMessage(hwnd, WM_HSCROLL, SB_PAGEUP, 0);
break;
case VK_RIGHT:
SendMessage(hwnd, WM_HSCROLL, SB_PAGEDOWN, 0);
break;
}
break;
以下是总体代码
BYTE *pBuf=new BYTE[SCREEN_H*SCREEN_W*4];
int row_size=SCREEN_W*4;
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) /* handle the messages */
{
case WM_CREATE:
ZeroMemory(pBuf,SCREEN_H*SCREEN_W*4);
for(int i=0;i<SCREEN_H;i++)
pBuf[i*row_size+i*4]=200;
break;
case WM_TIMER:
RedrawWindow(hwnd,NULL,NULL,RDW_ERASE|RDW_INVALIDATE);
break;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc;
RECT lprc;
EnterCriticalSection(&displock);
hdc=BeginPaint(hwnd,&ps);
//清除背景
FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW));
HPEN hBluePen = CreatePen(PS_SOLID, 10, RGB(200, 0, 80));
HPEN hPen = (HPEN)SelectObject(hdc, hBluePen);
MoveToEx(hdc, 100, 100, NULL);
LineTo(hdc, num, 2*num);
SelectObject(hdc, hPen);
DeleteObject(hBluePen);
HBRUSH hPurpleBrush = CreateSolidBrush(RGB(255-num, num, 20));
SetRect(&lprc,600,400,650,450);
FillRect(hdc, &lprc, hPurpleBrush);
DeleteObject(hPurpleBrush);
//checkScreenProperty(hdc);
HDC memdc=CreateCompatibleDC(hdc);
HBITMAP memBmp=CreateCompatibleBitmap(hdc,
GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN));
SelectObject(memdc,memBmp);
BITMAPINFO binfo;
ZeroMemory(&binfo,sizeof(BITMAPINFO));
binfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
binfo.bmiHeader.biBitCount=32;
binfo.bmiHeader.biCompression=0;
binfo.bmiHeader.biWidth=SCREEN_W;
binfo.bmiHeader.biHeight=SCREEN_H;
binfo.bmiHeader.biPlanes=1;
binfo.bmiHeader.biSizeImage=0;
SetDIBits(memdc,memBmp,0,
binfo.bmiHeader.biHeight,
pBuf,(BITMAPINFO*)&binfo,
DIB_RGB_COLORS);
BitBlt(hdc, 0, 0, binfo.bmiHeader.biWidth, binfo.bmiHeader.biHeight,memdc, 0, 0, SRCCOPY);
DeleteDC(memdc);
DeleteObject(memBmp);
GetClientRect(hwnd,&lprc);
DrawText(hdc,"Hello World",-1,&lprc,DT_SINGLELINE|DT_CENTER|DT_VCENTER);
EndPaint(hwnd,&ps);
LeaveCriticalSection(&displock);
break;
}
case WM_DESTROY:
running=false;
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;
}
总结与吐槽
微软网页上的这段典型代码体现出GDI有多么冗繁而反人类。
由于从一开始就没有面向对象的概念,所以各种变量数据裸露在外,大量名称交错使用,很容易造成混乱。给程序员的记忆负担也很重。