可用MinGW编译的win32绘图框架

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

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值