可用MinGW编译的win32绘图框架

来源:互联网 发布:禾赛科技李一帆 知乎 编辑:程序博客网 时间:2024/05/23 23:31

鉴于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 downhdc=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有多么冗繁而反人类。
由于从一开始就没有面向对象的概念,所以各种变量数据裸露在外,大量名称交错使用,很容易造成混乱。给程序员的记忆负担也很重。

0 0