当前位置: 首页 > article >正文

【第13节】windows sdk编程:GDI编程

目录

一、GDI 概述

二、设备环境概念

三、使用 GDI 绘图对象

四、使用 GDI 坐标系统

五、使用GDI绘图

5.1 输出文字

5.2 画点和线

5.3 画矩形框、圆和多边形

5.4 画位图和图标

5.5 双缓冲技术

六、综合代码示例


一、GDI 概述

        Windows 应用程序不支持标准输出函数(如 printf),应用程序输出包括文字在内的所有数据均是以图形方式“绘制”到窗口上的。Windows 通过图形设备接口 GDI 对图形输出进行支持。GDI 为应用程序提供了与显示器、打印机等输出设备无关的图形输出接口,让程序员无需直接处理各种显示、打印设备。

        GDI 由几百个函数以及相关的数据类型、宏定义和结构体组成。主要函数类别有:获取和释放(或创建和删除)设备描述表函数、获取设备信息函数、使用 GDI 绘图对象函数、绘图函数、设置和获取设备参数函数等。其显示的图形类型(简称图元)包括直线、曲线、填充、位图、文本等。

二、设备环境概念

        Windows 对图形显示设备进行封装,形成统一的虚拟图形显示设备。应用程序可在该虚拟设备上绘图,虚拟设备图形转换为物理设备图形的任务由设备驱动程序完成。这个虚拟图形设备用包含各种设备属性的数据结构表示,即设备环境 DC(device content),也叫设备上下文。设备环境 DC 是可用来输出图形的对象,如显示器、打印机、某个窗口等,设备环境句柄 HDC 是指向某个设备环境的类似指针的数据类型。

        从应用程序角度看,设备环境 DC 是 Windows 提供的“画板”,程序在上面绘图。程序员想在图形输出设备(如显示器或打印机)上绘图时,必须先获取设备环境句柄,再以此为参数调用 GDI 函数绘图。

        Windows 提供多种获取设备环境句柄的方法,在处理一条消息时获取了设备环境句柄,应在退出窗口过程 WndProc 之前释放或删除它,释放后 DC 句柄不能再用。

1. 处理 WM_PAINT 消息时获取:最常用的方法是在处理 WM_PAINT 消息时,使用 BeginPaint 和 EndPaint。

hdc = BeginPaint(hWnd, &ps); // 客户区准备绘图
// GDI 绘图调用......
EndPaint(hWnd, &ps); // 客户区绘图结束

        其中,`PAINTSTRUCT ps; HDC hdc;`,变量 `hdc` 是设备环境句柄类型 HDC 变量,变量 `ps` 是类型 PAINTSTRUCT 的结构体变量,该结构包含名为 `rcPaint` 的 RECT 矩形结构成员,`rcPaint` 定义一个包围窗口客户区无效范围的矩形。使用从 BeginPaint 获得的 DC 句柄,只能在这个区域内绘图,且只要使用了 BeginPaint 就必须且只能用 EndPaint 释放 DC 句柄。

        WM_PAINT 消息在窗口客户区域的一部分或全部变为“无效”,需“刷新”时发生,如新建窗口、窗口大小调整、窗口移动、被覆盖部分的恢复等情况。例如窗口 A 被窗口 B 覆盖的区域为“无效”区域,窗口 B 移走后,该区域需刷新。

2. 处理非 WM_PAINT 消息时获取:应用程序还可在处理非 WM_PAINT 消息时获取设备环境句柄,如:

hdc = GetDC(hWnd); // hdc 得到客户区 DC 句柄
// GDI 绘图调用......
ReleaseDC(hWnd, hdc); // 释放客户区 DC 句柄

        GetDC 得到的是窗口 hWnd 客户区的句柄,与 BeginPaint 和 EndPaint 的区别是利用其返回的句柄可在整个客户区上绘图,但 GetDC 不会使客户区中任何区域可能的无效区域变成有效,其绘图结果会在下次 WM_PAINT 消息刷新后“消失”。

3. 获取整个窗口的设备环境句柄:应用程序可获取整个窗口的设备环境句柄,如:

hdc = GetWindowDC(hWnd); // hdc 得到窗口 DC 句柄
// GDI 绘图调用......
ReleaseDC(hWnd, hdc); // 释放窗口 DC 句柄

        GetWindowDC 得到的设备环境句柄,除客户区外,还包括窗口的标题栏、菜单、滚动条和外框。使用 GetDC 和 GetWindowDC 得到的设备环境句柄,只能调用 ReleaseDC 释放。

4. 使用 CreateDC 获取:BeginPaint、GetDC 和 GetWindowDC 获得的设备环境句柄与显示器的窗口相关,获取设备环境句柄更通用的函数是 CreateDC,如:

// hdc 得到设备 DC 句柄
hdc = CreateDC(lpszDriver, lpszDevice, lpszOutput, lpInitData);
// GDI 绘图调用......
DeleteDC(hdc); // 删除设备 DC 句柄

        其中 `lpszDriver` 参数指定设备驱动名称,`lpszDevice` 指定设备名称,`lpszOutput` 和 `lpInitData` 一般设为 NULL。例如,`hdc = CreateDC("DISPLAY", NULL, NULL, NULL);` 可得到整个屏幕的设备环境句柄,`hdc = CreateDC(NULL, lpszDevice, NULL, NULL);` 可得到打印机的设备环境句柄,其中 `lpszDevice` 是打印机设备的名称。

5. 创建内存设备环境:当使用位图时,需创建一个内存设备环境,如:

hdcMem = CreateCompatibleDC(hdc); // hdcMem 得到内存 DC
// GDI 绘图调用......
DeleteDC(hdcMem); // 删除内存 DC 句柄

        创建了一个基于 `hdc` 设备特性的内存设备环境句柄 `hdcMem`,在 `hdcMem` 句柄上绘制的图形不会显示,只是在内存中“绘图”,一般需调用 GDI 位图函数将它复制到可显示 DC 句柄上才能看到绘图结果。使用 CreateDC 和 CreateCompatibleDC 得到的设备环境句柄,只能调用 DeleteDC 删除。

三、使用 GDI 绘图对象

GDI 绘图对象是绘制图形的工具,常用的 Windows 绘图工具如下:
 

GDI 绘图对象的使用步骤如下:
1. 创建绘图对象或调用 GetStockObject 获取预定义绘图对象。
2. 调用 SelectObject 将绘图对象选进设备环境 DC 中。
3. 调用 DeleteObject 删除绘图对象(GetStockObject 获取的对象除外)。

创建和删除绘图对象的时机:
1. 在 WM_CREATE 消息处理中创建绘图对象,在 WM_DESTROY 消息处理中删除绘图对象,即在窗口建立时创建对象,窗口销毁时删除对象。优点是不用频繁创建和删除对象,缺点是绘图对象始终占用内存,存储开销大。
2. 在设备环境句柄有效期间,如在 BeginPaint、GetDC、GetWindowDC、CreateDC、CreateCompatibleDC 之后创建绘图对象,在 EndPaint、ReleaseDC、DeleteDC 之后删除绘图对象。Windows 规定不能删除设备环境当前选择的绘图对象。

各种 GDI 绘图对象的创建函数如下:
 

        Windows 系统在应用程序初始化时提供一套默认的绘图工具,如默认画笔工具颜色是黑色。若应用程序认为默认绘图工具不合适,需另行创建绘图对象,并将其选进设备环境中,函数原型为:

HGDIOBJ SelectObject(HDC hdc, HGDIOBJ hgdiobj);

        参数 `hdc` 是欲使用绘图对象的 DC 句柄,`hgdiobj` 是绘图对象句柄,即绘图对象创建函数的返回值。SelectObject 的返回值是前一个绘图对象句柄,绘制图形时安全的做法是将默认绘图对象句柄保存下来,在释放或删除 DC 句柄前将默认绘图对象选进 DC 句柄。

四、使用 GDI 坐标系统

        大多数 GDI 绘图函数需要坐标值或尺寸,使用逻辑单位,实际图形输出时,Windows 将逻辑单位转换为设备单位(像素),这种转换由映射方式、窗口和视口的原点及其范围控制。

1. 映射方式:Windows 定义了 8 种映射方式,如下表所示:
 

可调用 SetMapMode 为 DC 设置映射方式,函数原型为:

int SetMapMode(HDC hdc, int fnMapMode);

        参数 `fnMapMode` 是 8 种映射方式之一。也可获取当前 DC 的映射方式,函数原型为:

int GetMapMode(HDC hdc);

        设备环境默认的映射方式为 MM_TEXT,此映射方式下,逻辑单位与物理单位相同,都是以像素为单位。例如 `TextOut(hdc, 8, 16, "Hello", 5);` 会在距离客户区左端 8 像素、上端 16 像素的位置开始输出“Hello”。若映射方式设置为 MM_LOMETRIC,如 `SetMapMode(hdc, MM_LOMETRIC);`,逻辑单位是十分之一毫米,`TextOut(hdc, 50, -10, "Hello", 5);` 会在距离客户区左端 5 毫米、上端 1 毫米的位置开始输出字符串,更适合打印机。

2. 设备坐标系:Windows 对所有非 GDI 函数,甚至一部分 GDI 函数使用设备坐标,以像素为单位。从显示设备看,存在三种不同的设备坐标系:


        (1)屏幕坐标:整个屏幕是“屏幕坐标”,屏幕左上角为(0,0)点,使用 CreateDC 创建屏幕设备环境句柄时,默认逻辑坐标将被映射为屏幕坐标。
        (2)窗口坐标:整个窗口是“窗口坐标”,包括标题栏、窗口外框、菜单、滚动条等,(0,0)点是窗口的左上角,用 GetWindowDC 获取窗口设备环境句柄,逻辑坐标会转换成客户区坐标。
        (3)客户区坐标:最常使用的设备坐标是“客户区坐标”,(0,0)点是客户区的左上角,使用 GetDC 或 BeginPaint 获得设备环境句柄时,逻辑坐标默认会转换为客户区坐标。

        用函数 `ClientToScreen` 和 `ScreenToClient` 可将客户区坐标转换成屏幕坐标,或反之,使用 `GetWindowRect` 函数可获取屏幕坐标下的窗口位置和大小。

3. 视口和窗口:视口(viewport)是计算机屏幕上一块显示区域,随 GDI 中所使用的设备环境句柄不同,该区域可以是客户区、整个窗口或整个屏幕。视口基于设备坐标(像素),通常视口和客户区相同,(0,0)点在客户区左上角,x 的值向右增加,y 的值向下增加。

        与视口中显示的图形相对应的原始图形区域称为窗口,这里的“窗口”不是指屏幕上显示的窗口对象,而是从现实世界角度看到的图形。窗口基于逻辑坐标,逻辑坐标可以是像素、毫米、英寸或其他单位。

        对于所有的映射方式,利用以下两个公式可将窗口坐标转换为视口坐标:
 

        其中 `xWindow`, `yWindow` 是待转换的逻辑点,`xViewport`, `yViewport` 是转换后的设备坐标点,`xWinOrg`, `yWinOrg` 是逻辑坐标的窗口原点,`xViewOrg`, `yViewOrg` 是设备坐标的视口原点,`xWinExt`, `yWinExt` 是逻辑坐标的窗口范围,`xViewExt`, `yViewExt` 是设备坐标的视口范围。此公式意味着逻辑窗口原点总被映射为设备视口原点。

        Windows 提供两个函数将设备点与逻辑点相互转换,函数原型为:

BOOL DPtoLP(HDC hdc, LPPOINT lpPoints, int nCount);
BOOL LPtoDP(HDC hdc, LPPOINT lpPoints, int nCount);


        `DPtoLP` 函数将设备点转换为逻辑点,`LPtoDP` 函数将逻辑点转换为设备点,`lpPoints` 是指向 POINT 结构数组的指针,`nCount` 表示存储在 `lpPoints` 中要转换的点的个数。例如可将 `GetClientRect` 获取的客户区大小(设备单位)转换为逻辑坐标,代码如下:

RECT ▭
GetClientRect(hWnd, &rect);
DPtoLP(hdc, (LPPOINT)&rect, 2);

4. 处理 MM_TEXT:对于 MM_TEXT 映射方式,默认的原点和范围如下:
    - 窗口原点:(0,0) 可以改变
    - 视口原点:(0,0) 可以改变
    - 窗口范围:(1,1) 不可改变
    - 视口范围:(1,1) 不可改变

        视口范围与窗口范围的比例为 1,不用在逻辑坐标与设备坐标之间进行缩放,上述两式可简化为:

xViewport = xWindow - xWinOrg + xViewOrg
yViewport = yWindow - yWinOrg + yViewOrg

        这种映射方式称为“文本”映射方式,与人们阅读文本从左至右、从上至下的习惯相同。如下图

        Windows 提供函数改变视口和窗口的原点,原型如下:

BOOL SetWindowOrgEx(HDC hdc, int X, int Y, LPPOINT lpPoint);
BOOL SetViewport0rgEx(HDC hdc, int X, int Y, LPPOINT lpPoint);

        `SetWindowOrgEx` 改变窗口原点,`SetViewportOrgEx` 改变视口原点,`(x, y)` 为新的原点值,`lpPoint` 用来接收先前的原点值,设为 NULL 表示不用接收。一般很少同时使用这两个函数,因为窗口和视口原点同时改变会使坐标变换的计算复杂化。

        例如,假定客户区宽为 `cxClient`、高为 `cyClient`,若想将逻辑原点定义为客户区的中心,可进行如下调用:

SetViewport0rgEx(hdc, cxClient/2, cyClient/2, NULL);

        `SetViewportOrgEx` 的参数总是使用设备单位,此时逻辑原点从(0,0)将映射为设备点 `(cxClient/2, cyClient/2)`,客户区坐标系改变如下图。

若想从客户区的左上角开始显示字符串,则需负坐标:

TextOut(hdc, -cxClient/2, -cyClient/2, "Hello", 5);

        用下面的 `SetWindowOrgEx` 调用可获得相同效果:

SetWindowOrgEx(hdc, -cxClient/2, -cyClient/2, NULL);

        `SetWindowOrgEx` 的参数总是使用逻辑单位,此调用后,逻辑点 `(-cxClient/2, -cyClient/2)` 映射为设备点(0,0),即客户区的左上角。若同时调用上述两个函数,的结果是逻辑点(-cxClient/2, -cyClient/2)将映射为设备点(cxClient/2, cyClient/2),客户区坐标系变成如下图所示。

五、使用GDI绘图

5.1 输出文字

GDI文字输出的绘图函数如下:
- `BOOL TextOut(HDC hdc, int nXStart, int nYStart, LPCTSTR lpString, int cbString);` :在指定坐标开始输出`cbString`个字符,如果输出全部字符串,`cbString`设为-1。
- `int DrawText(HDC hDC, LPCTSTR lpString, int nCount, LPRECT lpRect, UINT uFormat);` :在一个矩形范围内输出字符串,`uFormat`值表示使用对齐等格式。
- `COLORREF SetTextColor(HDC hdc, COLORREF crColor);` :设置文本颜色。
- `COLORREF SetBkColor(HDC hdc, COLORREF crColor);` :设置文本背景颜色。
- `int SetTextCharacterExtra(HDC hdc, int nCharExtra);` :设置文本字符间距。
- `UINT SetTextAlign(HDC hdc, UINT fMode);` :设置文本对齐方式。
- `int SetBkMode(HDC hdc, int iBkMode);` :设置背景色模式,`iBkMode = 0`(PAQUE)使用背景色,`iBkMode = TRANSPARENT`背景是透明的。

例如:

TextOut(hdc, 8, 16, "Hello", 5); // 输出文字
SetTextColor(hdc, RGB(255, 255, 255)); // 设置文字颜色
SetBkColor(hdc, RGB(0, 0, 0)); // 设置文字背景颜色
GetClientRect(hWnd, &rt); // 获取客户区大小
DrawText(hdc, szHello, -1, &rt, DT_SINGLELINE|DT_CENTER|DT_VCENTER); // 在客户区中央输出

        如果希望在窗口中实现类似`printf`的格式化输出,可以先调用`sprintf`将数据“输出”到字符数组中,再调用GDI函数绘制出来。例如:

char buf[100];
sprintf(buf, "s=%f", 3.1415926*13,2*13.2); // 输出圆面积s=π*r*r
TextOut(hdc, 300, 20, buf, strlen(buf));

        GDI文字输出比标准输出流的输出效果更美观,因为它可以设置文字颜色、背景色、按像素单位设置字符间距、使用字体、设置字号等等。

        在Windows中,绘图的最终结果可以通过设定不同的绘图方式来控制,其函数原型为:

int SetROP2(HDC hdc, int fnDrawMode);

        参数`fnDrawMode`为绘图模式,返回值为先前的绘图模式。常用的绘图模式见下表。当Windows使用画笔画线时,它实际上执行画笔像素颜色值与目标位置处原来像素颜色值的位运算,称为“光栅运算”。

5.2 画点和线

GDI画点和线的绘图函数如下:
- `BOOL PolyPolyline(HDC hdc, CONST POINT *lppt, CONST DWORD *lpdwPolyPoints, DWORD cCount)` :画多组相连的直线。
- `BOOL Arc(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nXStartArc, int nYStartArc, int nXEndArc, int nYEndArc);` :画圆弧。
- `BOOL PolyBezier(HDC hdc, CONST POINT *lppt, DWORD cPoints);` :画贝塞尔样条曲线。
- `BOOL PolyBezierTo(HDC hdc, CONST POINT*lppt, DWORD cCount)` :

例如:

SetPixel(hdc, 20, 20, RGB(255, 0, 0));

        在点`(20, 20)`处画一个红点,`COLOREF`类型用于指定颜色值。颜色值是由红、绿、蓝三个颜色分量组成,每种颜色分量的值为0~255,0表示无此颜色,255表示此颜色最大值。一般使用`RGB`宏定义产生颜色值,定义如下:

#define RGB(r,g,b) ((COLORREF)(((BYTE)(r)|((WORD)(BYTE)(g))<<8))|(((DWORD)(BYTE)(b))<<16)))

例如:

MoveToEx(hdc, 100, 100, NULL);
LineTo(hdc, 200, 200);

        从`(100, 100)`到`(200, 200)`画一条直线。`MoveToEx`用于将画笔移动到画线起点,`LineTo`从当前的起点画线到终点。

        如果希望用别的线条颜色或者线宽画线,那么需要创建不同的画笔对象,例如:

hPen = CreatePen(PS_SOLID, 4, RGB(255, 0, 255)); // 创建紫色画笔
hOldPen = SelectObject(hdc, hPen); // 将画笔选进DC中,此后画线均用此画笔,直到另选画笔为止
LineTo(hdc, 300, 100); // 画紫色线

5.3 画矩形框、圆和多边形

GDI画封闭形状的绘图函数如下,封闭形状用画笔画线,用画刷填充形状。
- `BOOL Ellipse(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);` :画椭圆。
- `BOOL Rectangle(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);` :画矩形框。
- `BOOL RoundRect(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nWidth, int nHeight);` :画带圆角的矩形框。
- `BOOL Pie(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nXRadiall, int nYRadial1, int nXRadial2, int nYRadial2);` :画饼图。
- `BOOL Chord(HDC hdc, int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nXRadiall, int nYRadial1, int nXRadial2, int nYRadial2);` :画弦。
- `int FillRect(HDC hDC, CONST RECT *lprc, HBRUSH hbr);` :填充矩形。
- `int FrameRect(HDC hDC, CONST RECT *lprc, HBRUSH hbr);` :画矩形边界。
- `BOOL Polygon(HDC hdc, CONST POINT *lpPoints, int nCount);` :画封闭的多边形。
- `BOOL PolyPolygon(HDC hdc, CONST POINT*lpPoints, CONST INT*lpPolyCounts, int nCount);` :画多组封闭的多边形。

如果希望用别的填充颜色或者填充图案,那么需要创建不同的画刷对象,例如:

hbr = CreateSolidBrush(RGB(255, 0, 0)); // 创建红色画刷
hOldBrush = SelectObject(hdc, hbr); // 将画刷选进DC中,此后填充均用此画刷,直到另选画刷为止
Rectangle(hdc, 300, 100, 400, 200); // 画矩形框且填充红色

5.4 画位图和图标

GDI可以直接输出位图图像,步骤如下:
1. 首先调用`LoadBitmap`函数加载位图资源,例如:

hBmp = LoadBitmap(hInst, MAKEINTRESOURCE(IDB_PICTURE)); // 加载位图资源

调用`GetObject`可以得到位图的宽和高,例如:

GetObject(hBmp, sizeof(BITMAP), &bm); // bm.bmWidth, bm.bmHeight


2. 其次,为便于在内存中对位图进行绘图操作,位图对象不是直接选进显示设备DC中,创建一个内存DC,将位图对象选进内存DC中。例如:

hdcMem = CreateCompatibleDC(hdc); // 创建内存DC
hOldBmp = (HBITMAP)SelectObject(hdcMem, hBmp); // 位图选进内存DC


3. 为了将内存DC的图形结果显示出来,可以调用位图像素复制函数,原型为:
    - `BOOL BitBlt(HDC hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, HDC hdcSrc, int nXSrc, int nYSrc, DWORD dwRop);` :等比复制`hdcSrc`的图形到`hdcDest`中,`dwRop`表示像素光栅运算模式。
    - `BOOL StretchBIt(HDC hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, HDC hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, DWORD dwRop);` :拉伸复制`hdeSrc`的图形到`hdcDest`中,`dwRop`表示像素光栅运算模式。

例如:

//将位图复制到显示设备(420,0)处
BitBlt(hdc, 420, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); // 复制到hdc

可以调用`DrawIcon`函数输出图标,其函数原型为:
`BOOL DrawIcon(HDC hDC, int X, int Y, HICON hIcon);`

例如:

hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_APP)); // 从资源加载图标
DrawIcon(hdc, 200, 200, hIcon); // 画图标

5.5 双缓冲技术

        GDI将图形直接绘制在显示设备上时,如果绘制内容较多或者绘制时间较长,会出现显示闪烁。为解决这个问题,可采用双缓冲技术(double buffering)。

        双缓冲原理是在内存中创建一个与屏幕绘图区域一致的内存DC,然后在其上绘制图形,由于闪烁是在内存中,因此不可见。绘制完成后采用`BitBlt`快速将图形送到显示设备上,从而避免显示闪烁。利用双缓冲技术,可以编写动画和游戏所要求的画面。但GDI绘图过程本身是比较慢的,如果需要编写快速显示的动画或游戏场景时,最好采用专业图形开发库,例如DirectX和OpenGL等。

        Windows窗口调整时会产生`WM_ERASEBKGND`消息,这个消息的作用是通知窗口用背景画刷填充背景,结果也会导致显示闪烁。因此应用程序可以增加`OnEraseBkgnd`消息处理,用来禁止背景填充,其代码如下:

LRESULT OnEraseBkgnd(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    // 擦除背景消息处理
    return FALSE; // 不擦除背景
}

        双缓冲技术实现代码如下:

LRESULT OnPaint(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    // 重绘消息处理
    PAINTSTRUCT ps;
    HDC hdc, hdcMem;
    HBITMAP hBmp, hOldBmp;
    HBRUSH hbr;
    RECT rt;
    COLORREF cr;
    int nWidth, nHeight;

    hdc = BeginPaint(hWnd, &ps); // 客户区绘图开始
    // 双缓冲开始---------------
    GetClientRect(hWnd, &rt); // 获取客户区大小
    hdcMem = CreateCompatibleDC(hdc); // 创建内存DC,建立双缓冲内存区域
    nWidth = rt.right - rt.left;
    nHeight = rt.bottom - rt.top;
    hBmp = CreateCompatibleBitmap(hdc, nWidth, nHeight); // 创建与显示区域相同的位图对象
    hOldBmp = (HBITMAP)SelectObject(hdcMem, hBmp); // 位图对象选进内存DC
    cr = GetBkColor(hdc); // 获取背景色
    hbr = CreateSolidBrush(cr); // 创建背景色实体画刷
    FillRect(hdcMem, &rt, hbr); // 使用背景色填充内存DC
    OnBufferPaint(hWnd, hdcMem); // 开始双缓冲绘图

    BitBlt(hdc, rt.left, rt.top, nWidth, nHeight, hdcMem, rt.left, rt.top, SRCCOPY); // 快速复制内存DC到显示DC设备上
    DeleteObject(hbr); // 删除画刷对象
    SelectObject(hdcMem, hOldBmp); // 恢复默认位图对象
    DeleteObject(hBmp); // 删除位图对象
    DeleteDC(hdcMem); // 删除内存DC
    // 双缓冲结束---------
    EndPaint(hWnd, &ps); // 客户区绘图结束
    return 0; // 处理了这条消息必须返回0
}

        `OnBufferPaint`函数中的绘图就是在双缓冲DC上进行的。如果将上面代码中的双缓冲开始至结束间的代码替换为:`OnBufferPaint(hWnd, hdc);`
        可以看到用与不用双缓冲技术,绘图效果区别是很大的。

六、综合代码示例

#include <windows.h>
#include <tchar.h>

// 双缓冲绘图函数
void OnBufferPaint(HWND hWnd, HDC hdcMem) {
    // 设置文本颜色和背景颜色
    SetTextColor(hdcMem, RGB(255, 255, 255));  // 白色文字
    SetBkColor(hdcMem, RGB(0, 0, 0));         // 黑色背景

    // 输出文字
    TextOut(hdcMem, 10, 10, _T("Hello, GDI!"), 11);

    // 创建红色画笔并绘制直线
    HPEN hPen = CreatePen(PS_SOLID, 2, RGB(255, 0, 0));  // 2像素宽,红色
    if (hPen) {
        HPEN hOldPen = (HPEN)SelectObject(hdcMem, hPen);
        MoveToEx(hdcMem, 50, 50, NULL);  // 起点
        LineTo(hdcMem, 200, 50);         // 终点
        SelectObject(hdcMem, hOldPen);   // 恢复旧画笔
        DeleteObject(hPen);              // 删除画笔
    }

    // 创建绿色画刷并绘制矩形
    HBRUSH hBrush = CreateSolidBrush(RGB(0, 255, 0));  // 绿色画刷
    if (hBrush) {
        HBRUSH hOldBrush = (HBRUSH)SelectObject(hdcMem, hBrush);
        Rectangle(hdcMem, 50, 70, 200, 150);  // 绘制矩形
        SelectObject(hdcMem, hOldBrush);      // 恢复旧画刷
        DeleteObject(hBrush);                 // 删除画刷
    }

    // 绘制圆形
    Ellipse(hdcMem, 250, 50, 350, 150);  // 绘制椭圆(圆形)
}

// 窗口过程函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
    case WM_PAINT: {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);  // 获取设备环境句柄

        // 双缓冲开始
        RECT rt;
        GetClientRect(hWnd, &rt);  // 获取客户区大小

        HDC hdcMem = CreateCompatibleDC(hdc);  // 创建内存DC
        if (hdcMem) {
            HBITMAP hBmp = CreateCompatibleBitmap(hdc, rt.right, rt.bottom);  // 创建兼容位图
            if (hBmp) {
                HBITMAP hOldBmp = (HBITMAP)SelectObject(hdcMem, hBmp);  // 选定位图

                // 使用背景色填充内存DC
                HBRUSH hbr = CreateSolidBrush(GetBkColor(hdc));
                if (hbr) {
                    FillRect(hdcMem, &rt, hbr);  // 填充背景
                    DeleteObject(hbr);           // 删除画刷
                }

                // 在内存DC上绘图
                OnBufferPaint(hWnd, hdcMem);

                // 将内存DC的内容复制到显示设备DC
                BitBlt(hdc, rt.left, rt.top, rt.right, rt.bottom, hdcMem, rt.left, rt.top, SRCCOPY);

                // 清理资源
                SelectObject(hdcMem, hOldBmp);  // 恢复旧位图
                DeleteObject(hBmp);             // 删除位图
            }
            DeleteDC(hdcMem);  // 删除内存DC
        }
        // 双缓冲结束
        EndPaint(hWnd, &ps);  // 结束绘制
        break;
    }
    case WM_ERASEBKGND:
        return TRUE;  // 禁止背景擦除,避免闪烁
    case WM_DESTROY:
        PostQuitMessage(0);  // 退出消息循环
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);  // 默认消息处理
    }
    return 0;
}

// 程序入口
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    // 注册窗口类
    WNDCLASS wc = { 0 };
    wc.lpfnWndProc = WndProc;              // 窗口过程函数
    wc.hInstance = hInstance;             // 实例句柄
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);  // 背景画刷
    wc.lpszClassName = _T("GDIDemo");     // 窗口类名
    if (!RegisterClass(&wc)) {             // 注册窗口类
        MessageBox(NULL, _T("窗口类注册失败!"), _T("错误"), MB_ICONERROR);
        return 0;
    }

    // 创建窗口
    HWND hWnd = CreateWindow(_T("GDIDemo"), _T("GDI 综合示例"), WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 800, 600, NULL, NULL, hInstance, NULL);
    if (!hWnd) {  // 检查窗口是否创建成功
        MessageBox(NULL, _T("窗口创建失败!"), _T("错误"), MB_ICONERROR);
        return 0;
    }

    // 显示窗口
    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    // 消息循环
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) {  // 获取消息
        TranslateMessage(&msg);            // 翻译消息
        DispatchMessage(&msg);             // 分发消息
    }

    return (int)msg.wParam;  // 返回程序退出码
}

 


http://www.kler.cn/a/591385.html

相关文章:

  • LeetCode 452. 用最少数量的箭引爆气球 java题解
  • springboot实现调用百度ocr实现身份识别
  • Joker靶机实战攻略
  • 《大语言模型》学习笔记(三)
  • 【R语言】lm线性回归及输出含义,置信区间,预测,R方,ggplot 拟合直线
  • 如何在前端处理文件上传,避免大文件造成的性能问题?
  • Linux并发程序设计(5):线程的相关操作
  • __str__特殊方法
  • 机器学习——数据清洗(缺失值处理、异常值处理、数据标准化)
  • 【QT:窗口】
  • 我在哪,要去哪
  • LogicFlow介绍
  • 漏洞知识点《一句话木马》
  • 堆(heap)
  • HTML CSS
  • 检查 YAML 文件格式是否正确的命令 yamllint
  • 【Linux】浅谈环境变量和进程地址空间
  • 王者荣耀道具页面爬虫(json格式数据)
  • Rust + WebAssembly 实现康威生命游戏
  • 如何开始搭建一个交易所软件?从规划到上线的完整指南