Windows编程系列:图形编程基础

前言

很早以前在github上看到的一个项目,通过hook WindowsAPI函数FillRect,对资源管理器背景进行了重绘。

项目地址如下:

https://github.com/Maplespe/explorerTool

 

我第一次见到的时候,觉得这个项目还是非常吸引我,因为从XP过后,我再也没有实现过为资源 管理器添加背景图片。

在XP时代,只需要一个desktop.ini就可以。

 

这个项目里涉及了Windows绘图的一些基础知识,在以前我对绘图这一块 的知识是看了忘忘了看,一直没有掌握下来。

 

这里做个笔记,希望供以后翻阅 。

 

图形输出设备

光栅设备

将图像表现为点(像素),比如视频 显示、点阵打印机和激光打印机。通俗点来说,就是放大以后会失真的图像。

矢量设备

用线段来绘制图像,比如绘图仪,这种图像放大以后不会失真。

 

GDI

GDI:Graphics Device Interface,图形设备接口,是Windows提供的一套函数接口。

Windows系统的图形界面是通过这套函数接口来实现的,这也是我学习绘图的主要原因。

 

GDI的函数都封装在GDI32.dll中,大概有几百个函数,Windows操作系统的窗口管理模块User32.dll大量的使用了GDI函数,

Windows图形界面上的窗口、菜单、图标、滚动条、标题栏等内容都是由User32.dll调用GDI函数进行绘制

 

GDI既可用于光栅设备,也可用于矢量设备。

GDI支持创建三种类型的图像输出 :位图、矢量图和文本。中文本的输出也是按图形方式输出 。

 

GDI通常可以分为17个领域:

* 位图

处理创建、绘制相关设备相关位图(DDB)、设备无关位图(DIB)、DIB段、像素和区域填充的函数

* 画刷

处理创建、修改GDI画刷对象的函数

* 剪裁 

处理设备描述表可绘制的函数

* 颜色 

调色板管理

* 坐标和变换

处理映射模式、设备坐标映射逻辑和通用变换矩阵的函数。

* 设备描述表

创建设备描述表,查询 ,设置其属性及选择GDI对象的函数

* 填充形状

绘制闭合区域及其周线的函数

* 字体和文本

在系统中安装和枚举字体,并用它们绘制文本字符串的函数

* 直线和曲线

绘制直线、椭圆曲线和贝塞尔曲线的函数

* 元文件

处理Windows格式的元文件或增加型元文件的生成和回放的函数

* 多显示监视器

允许在一个系统中使用多个显示器的函数。这些函数实际上是从user32.dll导出的

* 画图和绘图

负责绘图消息管理和窗口已绘图区域的函数,其中一些函数实际上是从user32.dll中导出的

* 路径

负责将一系列直线和曲线组成名为路径的GDI对象,并用它来绘制的函数

* 画笔

处理直线绘制属性的函数

* 打印和打印池

否则将GDI绘图命令发送到硬拷贝设备(如行式打印机和绘图仪)并平滑地管理这些服务。

* 矩形

user32.dll提供的处理RECT结构的函数

* 区域

负责用区域GDI对象描述一个点集的函数,并对该点集进行操作。

 

说明:在C#中,封装了GDI相关的库是System.Drawing.dll,在WPF/UWP应用中,界面的绘制已经由DirectX来完成了,在Winform中,还是调用的GDI。

 

GDI+

Windows GDI+ 是 Windows XP 操作系统或 Windows Server 2003 操作系统的一部分,提供二维矢量图形、图像处理和版式。 GDI+ 通过添加新功能和优化现有功能,改进了 Windows 图形设备接口 (GDI) (早期版本的 Windows) 附带的图形设备接口。

Windows GDI+ 是一个图形设备接口,允许程序员编写与设备无关的应用程序。 GDI+ 的服务通过一组 C++ 类公开。

GDI是通过WinAPI函数体现,GDI+通过C++类的形式来体现。

 

GDI对象

与绘图有关的Windows对象称为GDI对象,一种Windows资源实体,比如窗口对象、画笔、画刷、字体等。

Windows对象通过分为内核 对象、GDI对象、和User对象这三类,通常用句柄来标识 。

GDI对象的拥有者通常是一个进程,不能被多个进程共有。

GDI对象与绘图有关,包手位图、画刷、画笔、字体、调色板、区域等。

 

GDI对象在使用完成后,需要及时释放,我以前就遇到过GDI对象没有及时释放,多次绘制,最终导致程序闪退的问题。

这种由于GDI对象导致的闪退问题,查找起来非常麻烦,所以要养成良好的习惯,在使用完成GDI对象后,及时释放。

 

更新区域

更新区域(update region)用于标记窗口无效部分的范围,该部分必须重绘,如果我们的绘图函数超出了更新区域,不会画出任何东西。

如果窗口产生了无效部分,系统会把这部分记录在更新区域内。

系统使用更新区域主要是为了加速,只需要重绘无效区域即可,从而加快绘画 速度。

当系统确定某个窗口需要更新时,它会将更新区域的尺寸设置为窗口的无效部分。 设置更新区域不会立即导致应用程序

绘制,应用程序会继续从应用程序消息队列中检索消息,直到没有消息。

然后,系统会检查更新区域,如果该区域不为空 (非 NULL) ,则会向窗口过程发送 WM_PAINT 消息。

程序在WM_PAINT消息中处理,并进行重绘。

 

窗口无效部分的产生可能由于窗口被缩放、移动、创建、滚动等,此时系统会根据该窗口无效部分设置好相应尺寸的更新区域。

 

窗口无效部分的产生还可以通过调用API函数,如InvalidateRectInvalidateRgn能在客户区域上产生无效部分,RedrawWindow既可以在客户区上,也可以在非客户区产生无效部分。

InvalidateRect函数声明如下:

1 BOOL InvalidateRect(
2   [in] HWND       hWnd,
3   [in] const RECT *lpRect,
4   [in] BOOL       bErase
5 );

参数说明:

hWnd

窗口句柄, 如果此参数为 NULL,则系统会使所有窗口(而不仅仅是此应用程序的窗口)失效并重绘,并在函数返回之前发送 WM_ERASEBKGND 和 WM_NCPAINT 消息。 不建议将此参数设置为 NULL 。

lpRect

指向RECT结构,指定要加入到更新区域中的窗口某块矩形区域的尺寸,若为NULL则表示将整个客户区都加入到更新区域。

bErase

如果此参数为 TRUE,则调用 BeginPaint 函数时将擦除背景。 如果此参数为 FALSE,则背景保持不变

 

InvalidateRgnInvalidateRect含义类似,只不过第二个参数指向一个任意形状区域的句柄,这个形状不一定是矩形。函数声明如下:

1 BOOL InvalidateRgn(
2   [in] HWND hWnd,
3   [in] HRGN hRgn,
4   [in] BOOL bErase
5 );

参数说明:

hWnd

窗口句柄

hRgn

要添加到更新区域的区域的句柄。 假设该区域具有客户端坐标。 如果此参数为 NULL,则整个工作区将添加到更新区域。

bErase

指定在处理更新区域时是否应清除更新区域中的背景。 如果此参数为 TRUE,则调用 BeginPaint 函数时将擦除背景。 如果参数为 FALSE,则背景保持不变。

 

RedrawWindow函数声明如下:

1 BOOL RedrawWindow(
2   [in] HWND       hWnd,
3   [in] const RECT *lprcUpdate,
4   [in] HRGN       hrgnUpdate,
5   [in] UINT       flags
6 );

参数说明:

hWnd

要重绘的窗口的句柄。 如果此参数为 NULL,则更新桌面窗口。

 

lprcUpdate

指向 RECT 结构的指针,该结构包含更新矩形的坐标(以设备单位为单位)。 如果 hrgnUpdate 参数标识区域,则忽略此参数。

 

hrgnUpdate

更新区域的句柄。 如果 hrgnUpdate 和 lprcUpdate 参数均为 NULL,则整个工作区将添加到更新区域。

 

flags

一个或多个重绘标志。 此参数可用于使窗口失效或验证、控件重新绘制以及控制受 RedrawWindow 影响的窗口。

以下标志用于使窗口失效。

标记 (失效)说明
RDW_ERASE
使窗口在重新绘制时收到 WM_ERASEBKGND 消息。 还必须指定RDW_INVALIDATE标志;否则,RDW_ERASE无效。
RDW_FRAME
导致与更新区域相交的窗口非工作区的任何部分接收 WM_NCPAINT 消息。 还必须指定RDW_INVALIDATE标志;否则,RDW_FRAME无效。 除非指定 了RDW_UPDATENOW 或RDW_ERASENOW,否则在执行 RedrawWindow 期间通常不会发送WM_NCPAINT消息。
RDW_INTERNALPAINT
导致 WM_PAINT 消息发布到窗口,而不管窗口的任何部分是否无效。
RDW_INVALIDATE
使 lprcUpdate 或 hrgnUpdate 失效 (只有一个可以是非 NULL) 。 如果两者均为 NULL,则整个窗口无效。

 

以下标志用于验证窗口。

标记 (验证)说明
RDW_NOERASE
禁止显示任何挂起 WM_ERASEBKGND 消息。
RDW_NOFRAME
禁止显示任何挂起 WM_NCPAINT 消息。 此标志必须与 RDW_VALIDATE 一起使用,并且通常与 RDW_NOCHILDREN 一起使用。 应谨慎使用RDW_NOFRAME,因为它可能会导致窗口的某些部分被不当绘制。
RDW_NOINTERNALPAINT
禁止任何挂起的内部 WM_PAINT 消息。 此标志不会影响由非 NULL 更新区域生成的WM_PAINT消息。
RDW_VALIDATE
验证 lprcUpdate 或 hrgnUpdate (只有一个可以是非 NULL) 。 如果两者均为 NULL,则验证整个窗口。 此标志不会影响内部 WM_PAINT 消息。

 

以下标志控制何时发生重绘。 除非指定其中一个标志,否则 RedrawWindow 不会重新绘制。

标志描述
RDW_ERASENOW
使受影响的窗口 (RDW_ALLCHILDREN和RDW_NOCHILDREN标志) 接收 WM_NCPAINT ,并在函数返回之前 接收WM_ERASEBKGND 消息(如有必要)。 WM_PAINT 消息在正常时间接收。
RDW_UPDATENOW
使受RDW_ALLCHILDREN和RDW_NOCHILDREN标志指定的受影响的窗口 (,) 在函数返回之前接收 WM_NCPAINT、 WM_ERASEBKGND和 WM_PAINT 消息(如有必要)。

 

默认情况下,受 RedrawWindow 影响的窗口取决于指定的窗口是否具有WS_CLIPCHILDREN样式。 不是WS_CLIPCHILDREN样式的子窗口不受影响;非WS_CLIPCHILDREN窗口以递归方式验证或失效,直到遇到WS_CLIPCHILDREN窗口。 以下标志控制哪些窗口受 RedrawWindow 函数的影响。

 

标志描述
RDW_ALLCHILDREN
在重绘操作中包括子窗口(如果有)。
RDW_NOCHILDREN
从重绘操作中排除子窗口(如果有)。

 

说明:

对于客户区产生的无效部分,系统发送WM_PAINT到消息队列 。对于非客户区上产生的无效部分,系统会产生WM_NCPAINT消息。

 

无效化后不立即重绘,主要是为了提高系统的绘画效率,因为无效区域产生的原因很多,有可能函数调用产生,也有可能 用户操作窗口产生,这样就很有可能 在短时间内出现多个无效区域重叠的情况,此时系统等消息队列中没有消息时,再发送WM_PAINT,就能很大程度的避免多次重复绘画同一区域。

 

如果需要窗口无效化后立即重绘,可以调用UpdateWindow函数。声明如下:

1 BOOL
2 WINAPI
3 UpdateWindow(
4     _In_ HWND hWnd);

 

所以需要对某个区域进行立即 重绘,可以先调用InvalideRect函数,再调用UpdateWindow函数。

此外,还可以通过RedrawWindow函数,它相当于先调用InvalideRect函数,再调用UpdateWindow函数。

WM_PAINT消息的处理中,必须调用BeginPaint函数,这个函数会限制绘画区域,同时将更新区域设置为空。

BeginPaint声明如下:

1 HDC
2 WINAPI
3 BeginPaint(
4     _In_ HWND hWnd,
5     _Out_ LPPAINTSTRUCT lpPaint);

参数说明:

hWnd

窗口句柄

lpPaint

指向绘图信息结构PAINTSTRUCT的指针,该参数是一个输出参数,可以获取绘图相关的状态信息。

返回值

成功,返回窗口的设备描述表句柄,失败,返回NULL。

 

PAINTSTRUCT结构定义如下:

1 typedef struct tagPAINTSTRUCT {
2     HDC         hdc;          //窗口设备描述表句柄
3     BOOL        fErase;     //是否清除背景色
4     RECT        rcPaint;    //更新区域的最小矩形范围
5     BOOL        fRestore; //系统内部使用
6     BOOL        fIncUpdate; //系统内部使用
7     BYTE        rgbReserved[32]; //系统内部使用
8 } PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;

 

至此,界面重绘的过程已经了解了。

当窗口被创建、滚动、缩放或者移动时,系统会先发送WM_ERASEBKGND消息,再发送WM_PAINT消息。

WM_ERASEBKGN消息的默认处理函数DefWindowProc会用预设的画刷擦除背景,这个画刷是我们在注册窗口时候设定的。

1     wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);

 

如果想让系统简单地擦除窗口背景,就不需要处理这个消息,系统会调用默认消息处理函数DefWindowProc,在这个函数中会对整个更新区域范围内的窗口背景进行擦除,更新区域外则不会擦除。

如果想自己绘制窗口背景,则可以在WM_ERASEBKGND消息处理中添加自己的代码,然后返回TRUE。

 

关于WM_ERASEBKGND消息处理这里还有一部分理论还未理解透彻,这里就暂时不写出来了。

 

设备描述表(DC)

 在Windows操作系统中,我们要在屏幕的窗口上画图或在打印机上输出 图形,需要先确定绘图所需要的相关工具和参数。这个工具和参数相当于一个绘图的环境,为了方便管理各个工具和参数,GDI用设备描述表(Device Context,DC)来表示绘图环境,而绘图相关的工具和参数就成为设备描述表的属性。

在窗口上进行绘图时,需要先取得这个窗口的设备描述表,然后开始在窗口上绘图。

 

设备描述表是GDI内部的数据结构 ,无法直接访问,但是系统会提供一个句柄来引用 设备描述表,并提供一些API函数对这个句柄进行操作。

 

设备描述表可以分为

* 显示设备描述表

用于在显示设备(比如显示器)上绘图

 

* 打印设备描述表

用于在点阵打印机、激光打印机和喷墨打印机上绘图

 

*内存设备描述表

用于为特定的设备存储位图,并支持在位图上绘图

 

* 信息设备描述表

用于获取默认设备的数据

 

设备描述表的获取和释放

Win32中,图形设备描述用一个句柄来表示 ,HDC。

Windows操作系统中,屏幕、窗口和窗口客户区分别有不同的设备描述表。

要在这3个不同区域绘图,必须先得该区域的设备描述表。

 

获取屏幕的设备描述表方式如下:

1 HDC hdc = CreateDC(L"Display",0,0,0); //获取屏幕的设备描述表

使用完成后,需要释放,释放的函数是DeleteDC。

CreateDC和DeleteDC是成对使用。

 

获取窗口的设备描述表通过GetWindowDC函数,声明如下:

1 HDC GetWindowDC(HWND hwnd);

使用完成后,需要释放,释放的函数是ReleaseDC

GetWindowDC和ReleaseDC也是成对使用

 

对于窗口客户区,通常有两种方式获取设备描述表

1、通过函数BeginPaint

一般而言,窗口重启区的重绘操作都是在WM_PAINT消息中进行的,在这个消息处理(WM_PAINT)分支中,先通过函数BeginPaint来获取设备描述表句柄,然后进行GDI绘图操作。绘图完成后再调用EndPaint来标记绘图已经完成。

BeginPaint除了可以获取设备描述表句柄以外,还能把更新区域变为裁剪区,同时将更新区域标记为空,这样系统就不会一直发出WM_PAINT的消息,此外,该函数还会填充绘图信息结构体的数据。

 

通过下面的示例代码,可以更好的理解:

1  case WM_PAINT:
2      {
3          PAINTSTRUCT ps;
4          HDC hdc = BeginPaint(hWnd, &ps);
5          // TODO: 在此处添加使用 hdc 的任何绘图代码...
6          EndPaint(hWnd, &ps);
7      }
8      break;

 

 和前面的HDC操作一样,BeginPaint和EndPaint也是成对出现。绘图完成后,需要调用EndPaint来标记绘图已经完成。

 

2、通过函数GetDC

如果不是在WM_PAINT消息中获取设备描述表句柄,例如要用鼠标进行画线,可以通过在鼠标消息的处理事件中通过GetDC函数来获取设备描述表句柄,然后进行画线操作。GetDC声明如下:

1 WINUSERAPI
2 HDC
3 WINAPI
4 GetDC(
5     _In_opt_ HWND hWnd);

 

hWnd就是要获取DC的窗口句柄,如果这个参数为NULL,就返回整个屏幕的DC。成功就返回指定窗口客户区的设备描述表句柄,否则为NULL

 

和前面的GetWindowDC一样,使用GetDC在绘图完成后,需要调用ReleaseDC函数释放设备描述表。

GetDCReleaseDC也是成对出现。

 

下面的示例代码就是通过GetDC函数使用鼠标绘图

 1 BOOL fDraw = FALSE; 
 2 POINT ptPrevious; 
 3  
 4   . 
 5   . 
 6   . 
 7  
 8 case WM_LBUTTONDOWN: 
 9     fDraw = TRUE; 
10     ptPrevious.x = LOWORD(lParam); 
11     ptPrevious.y = HIWORD(lParam); 
12     return 0L; 
13  
14 case WM_LBUTTONUP: 
15     if (fDraw) 
16     { 
17         hdc = GetDC(hwnd); 
18         MoveToEx(hdc, ptPrevious.x, ptPrevious.y, NULL); 
19         LineTo(hdc, LOWORD(lParam), HIWORD(lParam)); 
20         ReleaseDC(hwnd, hdc); 
21     } 
22     fDraw = FALSE; 
23     return 0L; 
24  
25 case WM_MOUSEMOVE: 
26     if (fDraw) 
27     { 
28         hdc = GetDC(hwnd); 
29         MoveToEx(hdc, ptPrevious.x, ptPrevious.y, NULL); 
30         LineTo(hdc, ptPrevious.x = LOWORD(lParam), 
31           ptPrevious.y = HIWORD(lParam)); 
32         ReleaseDC(hwnd, hdc); 
33     } 
34     return 0L;

 

运行效果:

 

 

 

BeginPaint和GetDC获取窗口区DC句柄的区别

1、BeginPaint函数针对的是必须重绘的区域(也称无效区域、失效区域),该区域可能 是整个窗口客户区,也可能 是客户区中某块无效矩形区域。

BeginPaint函数获取HDC后,后续绘图操作都是在无效区域中进行。如果绘图范围超过无效区域,将被忽略。

说明:

InvalidateRect 或 InvalidateRgn 函数可以间接为窗口生成WM_PAINT消息。 这些函数将工作区的全部或部分标记为无效 

GetDC针对的区域是整个客户区,后续绘图操作可以在整个客户区中进行,没有无效区域的概念。

 

2、BeginPaint会把无效区域变成有效区域,GetDC则不会。无效区域依旧是无效区域。

 

3、BeginPaint只能在WM_PAINT消息里使用,因为进入了WM_PAINT消息处理,说明一定有无效区域了,有无效区域了BeginPaint才有用武之地,没有无效区域BeginPaint后续的绘图操作会被忽略掉。

GetDC既可以在WM_PAINt消息里使用,也可以在非WM_PAINT消息里使用。

 

设备描述表的属性

设备描述表包含在窗口上进行绘图操作所需的环境信息,这些环境信息就是设备描述表的属性,比如当前环境的颜色、坐标、映射模式、绘制文本所用的字体等。

在获取设备描述表时,系统已经为设备描述表分配了一些默认属性值,如绘制文本时,颜色默认是黑色。

 

常见的设备描述表的属性

 

常用数据结构

点的坐标POINT

点的坐标用POINT结构体来表示 ,定义如下:

1 typedef struct tagPOINT {
2   LONG x;  //横坐标
3   LONG y;  //纵坐标
4 } POINT, *PPOINT, *NPPOINT, *LPPOINT;

使用方法如下:

1 POINT pt = {1,1}  //定义一个点,坐标为(1,1)

 

矩形尺寸SIZE

用SIZE表示矩形的长和宽,定义如下:

1 typedef struct tagSIZE {
2   LONG cx;
3   LONG cy;
4 } SIZE, *PSIZE, *LPSIZE;

使用方法如下:

SIZE sz = {100,100} //定义宽高100的矩形

 

矩形坐标RECT

矩形坐标就是一个矩形的四个角的坐标,定义如下:

1 typedef struct tagRECT {
2   LONG left;
3   LONG top;
4   LONG right;
5   LONG bottom;
6 } RECT, *PRECT, *NPRECT, *LPRECT;

left:左上角x坐标

top:左上角y坐标

right:右下角x坐标

bottom:右下角y坐标

使用方法如下:

1 RECT rect = {0,0,100,100};

 

RECT提供了一些辅助函数

PtInRect:判断是否在矩形范围内

SetRect:确定一个矩形坐标

InflateRect:增大或缩小矩形的长和宽

CopyRect:复制矩形坐标

EqualRect:判断两个矩形坐标是否相等

IntersectRect:将坐标交集并赋给目标矩形

IsRectEmpty:判断坐标是否都为0

OffsetRect:偏移一段距离 

SetRectEmpty:设置坐标都为0

SubtractRect:两个坐标相减

UnionRect:两个坐标的并集

 

 

参考资料:

https://learn.microsoft.com/en-us/windows/win32/gdi/drawing-in-the-client-area

https://learn.microsoft.com/en-us/windows/win32/gdi/painting-and-drawing

https://learn.microsoft.com/zh-cn/windows/win32/gdi/drawing-a-minimized-window

posted @ 2024-04-01 17:16  zhaotianff  阅读(21)  评论(0编辑  收藏  举报