Windows编程:图标资源、光标资源、字符串资源、加速键资源、WM_PAINT消息、绘图
Published in:2024-08-13 |



图标资源

.ico为后缀的图片,icon 图标资源。一个图标文件中可以有多个不同分辨率大小的图标,以适应不同场景下的使用。

想要使用图标资源,第一步添加图标资源,第二步加载图标资源,第三步设置窗口类。

1
2
3
4
HICON LoadIcon(
HINSTANCE hInstance, //handle to application instance
LPCTSTR lpIconName //name string or resource identifier
); //成功返回HICON句柄

添加图标资源

在这里插入图片描述

添加图标资源后可以自己绘制图标:从图中可以看到有不同大小的图标(256x256像素、48x48像素等),有支持彩色的图标,也有黑白图标。

在这里插入图片描述

代码中添加资源头文件,并将图标赋值给窗口类的成员变量。

1
2
3
#include "resource.h"    //把资源头文件包含进来
... ...
wc.hIcon = LoadIcon(hIns, (char*)IDI_ICON1);

编译运行改变窗口图标:
在这里插入图片描述

在Visual Studio中编辑图标并不怎么方便,在这里可以将提前制作好的图标放在项目目录中,修改.rc 文件中对于图标资源的描述,修改为自己提前制作好的图标,然后在Visual Studio中重新加载被修改的 .rc 文件,再次编译运行。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述




光标资源

光标资源就是鼠标光标,这个光标在不同情况下是不同的:有的时候是箭头光标,有的时候是一个小手,有的时候是一闪一闪的竖杠,等等;在游戏中光标的形状也各不相同。一个光标就是一个图片,也是需要自己提前绘制,然后用代码进行应用。

光标资源使用步骤:

  • 第一步添加光标资源,光标的默认大小是32x32像素,每个光标有一个HotSpot热点,也就是点击时生效的点(因为一个光标图片有很多像素,但是生效的一个点只有一个点,这个点的位置可以自己设置,例如箭头光标的热点就在箭头的尖上)。
  • 第二步加载资源:
    1
    2
    3
    4
    HCURSOR LoadCursor(
    HINSTANCE hInstance, //handle to application instance
    LPCTSTR lpCursorName //name or resource identifier
    ); // hInstance 可以为 NULL, 获取系统默认的Cursor
  • 第三步设置光标:
    • 注册窗口类时设置。
    • 或 使用 SetCursor() 设置光标。

添加光标资源:

在这里插入图片描述

绘制光标资源:

在这里插入图片描述

使用图中的 Set Hot Spot Tool 来设置光标热点。

设置光标:

1
2
3
4
#include "resource.h"
... ...
//wc.hCursor = LoadCursor(NULL, IDC_ARROW); //默认光标
wc.hCursor = LoadCursor(hIns, (char*)IDC_CURSOR1);

无论是 LoadCursor() 函数还是 LoadIcon() 函数,他们的作用都是通过窗口实例句柄和资源的ID来在内存中找到这个资源。窗口实例句柄 hInstance 的作用就是在内存中找到一块保存窗口各种信息的内存,这些资源被编译后会编译到 .exe 里面,当程序运行的时候,程序就会被加载到内存,其中就包括了各种资源。

自己创建的光标只在客户区起作用,在标题栏就会自动变成默认光标。

使用 SetCursor() 函数可以在程序运行的过程中修改光标

1
2
3
HCURSOR SetCursor(
HCURSOR hCursor // handle to cursor
);

SetCursor()函数必须放在 WM_SETCURSOR 消息下进行调用。

WM_SETCURSOR 消息

只有光标有移动的情况,这个消息就会不断地出现。专职用法:改光标。

附带参数:

  • wParam :当前使用的光标句柄。
  • lParam :
    • LOWORD :当前区域的代码(Hit-Test code)HTCLIENT (客户区)/ HTCAPTION(标题栏、边框)。
    • HIWORD :没太大用处。

再绘制一个光标 IDC_CURSOR2,在程序运行时进行切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//添加全局变量 hInstance
HINSTANCE g_hInstance = 0;

//WinMain()函数中给hInstance赋值
g_hInstance = hIns;

//消息处理函数中添加代码:
case WM_SETCURSOR:
{
HCURSOR hCursor = LoadCursor(g_hInstance, (char*)IDC_CURSOR2);
SetCursor(hCursor);
}
break;
//运行后,鼠标移动时,光标闪烁变化,这是因为该消息产生后立马修改了光标,但是return DefWindowProc()又将光标修改回去
//SetCursor()后面加上 return 0;即可解决光标随着鼠标的移动不断闪烁修改的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//修改如下
//在客户区光标时 IDC_CURSOR2,在标题栏 IDC_CURSOR1,在其他地方(顶层菜单栏、图标等位置处)为默认光标
case WM_SETCURSOR:
{
HCURSOR hCursor1 = LoadCursor(g_hInstance, (char*)IDC_CURSOR1);
HCURSOR hCursor2 = LoadCursor(g_hInstance, (char*)IDC_CURSOR2);
if (LOWORD(lParam) == HTCLIENT) {
SetCursor(hCursor2);
return 0;
}
else if (LOWORD(lParam) == HTCAPTION) {
SetCursor(hCursor1);
return 0;
}
}
break;



字符串资源

字符串资源存在的目的就是方便代码中各种字符串的修改,例如语言的切换(中英文切换等)。

具体来说,字符串资源的作用包括:

中心化管理文本:将应用程序中使用的所有文本信息集中管理,方便统一修改和维护。

多语言支持:通过为不同语言单独创建字符串资源文件,可以实现应用程序的多语言支持,使应用能够在不同语言环境下运行并显示相应的文本。

提高可维护性:由于文本信息集中管理,修改或更新某个文本只需在字符串资源中进行修改,而不需要在代码中逐个替换,提高了代码的可维护性。

便于国际化和本地化:通过使用字符串资源,可以轻松地将应用程序本地化为不同的语言和区域设置,满足不同用户群体的需求。

节省内存空间:采用字符串资源可以在编译时对字符串进行优化,并且可以节省内存空间。

  • 添加字符串资源:添加字符串表,在表中增加字符串。
  • 加载字符串资源:
    1
    2
    3
    4
    5
    6
    int LoadString(
    HINSTANCE hInstance, // handle to resource module
    UINT uID, //字符串ID
    LPTSTR lpBuffer, //存放字符串BUFF
    int nBufferMax //字符串BUFF长度
    ); //成功返回字符串长度,失败返回0

添加字符串资源:

在这里插入图片描述

添加字符串资源:

在这里插入图片描述

使用字符串资源:

1
2
3
4
5
6
7
8
9
char wTitle[20] = { 0 };
LoadString(hIns, STR_Chinese_WinTitle, wTitle, 20);

HWND hWnd = CreateWindowEx(
0, pClassName, wTitle,
WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_OVERLAPPEDWINDOW,
200, 200, 640, 480, //窗口位置(200,200),大小长宽(640,480)。
nullptr, nullptr, hIns, nullptr
);

在程序中尽量多使用字符串资源(string table),这样可以方便文本管理,如果需要大面积的修改文本,也会方便很多。




加速键资源

加速键,就当快捷键理解。例如记事本的快捷键:

在这里插入图片描述

添加资源:资源添加加速键表,增加命令ID对应的加速键。

使用:

1
2
3
4
5
6
7
8
9
10
11
12
//加载加速键表
HACCEL LoadAccelerators(
HINSTANCE hInstance, //handle to module
LPCTSTR lpTableName // accelerator table name
); //返回加速键表句柄

//翻译加速键
int TranslateAccelerator(
HWND hWnd, //处理消息的窗口句柄
HACCEL hAccTable, //加速键表句柄
LPMSG lpMsg //消息
); //如果是加速键,返回非零。该函数内部首先判断是不是加速键。

添加加速键资源: Accelerator(加速)

在这里插入图片描述

编辑加速键:当加速键的ID和菜单中某一项ID相同时,就实现了加速键和某一个菜单项绑定。加速键和菜单项没有什么对应关系,当它们的ID相同时,就可以说是绑定;当然加速键的ID也可以单独设置,不一定非要和菜单项的ID相同。关于菜单资源参见上一篇文章菜单资源(点击跳转)

在这里插入图片描述

在这里 ID_40001ID_40002ID_40003 分别对应着菜单项的ID 新建打开退出ID_other 不对应任何菜单项。

在这里插入图片描述

当菜单项被点击时,或者加速键被按下时,都可以产生 WM_COMMAND 消息。

WM_COMMAND 消息中:

  • wParam:
    • HIWORD 为 1 表示消息产生自加速键,为 0 表示消息产生自菜单项。
    • LOWORD 为命令 ID。
  • lParam :0.

TranslateAccelerator()函数的内部执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//伪代码
TranslateAccelerator(hWnd, hAccel, &nMsg){
if(nMsg.message != WM_KEYDOWN)
return 0; //不是按键被按下,那就绝对不是加速键消息,直接返回0

根据nMsg.wParam(键码值),获知哪些按键被按下(假如Ctrl + N)
拿着(Ctrl + N)到 hAccel 加速键表中匹配具体加速键
if(没找到)
return 0;
if(找到){
SendMessage(hWnd, WM_COMMAND, HIWORD(1)|||LOWORD(ID_40001), ... ); //产生COMMAND消息
return 1;
}
}

加速键的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//加速键要放在消息循环中使用。
//使用前首先加载加速键表
HACCEL hAccel = LoadAccelerators(hIns, (char*)IDR_ACCELERATOR1);

//TranslateAccelerator()函数应该放在TranslateMessage()函数之前调用,因为TranslateMessage()会区分大小写。


消息循环加上翻译加速键函数的调用后,完整代码如下:

while (1) //先进入死循环,循环体中进行判断是否退出循环
{
if (PeekMessage(&nMsg, NULL, 0, 0, PM_NOREMOVE)) //PeekMessage侦察兵侦察是否有消息,如果有消息
{
if ((gResult = GetMessage(&nMsg, NULL, 0, 0)) > 0) //gResult值大于零意味着GetMessage没有抓到 WM_QUIT(返回值为0) 也没有出错(出错返回值为-1)
{
if (!TranslateAccelerator(hWnd, hAccel, &nMsg)) //返回值为0,不是加速键,则翻译、派发消息。是加速键消息,则产生一个 WM_COMMAND 消息,然后进入下次消息循环
{
TranslateMessage(&nMsg);
DispatchMessage(&nMsg);
}
}
else //如果抓到 WM_QUIT 或者 出错 退出while循环
{
break;
}
}
else //如果PeekMessage()没有侦察到消息,空闲处理
{
//WriteConsole(g_dos_output, "No Message", strlen("No Message"), NULL, NULL);
}
}

if (gResult == -1) //GetMessage()出错返回值 -1
{
//错误处理或直接退出程序
return -1;
}
else //抓到 WM_QUIT 消息,退出程序,返回 PostQuitMessage()的参数值
{
return nMsg.wParam; //此时 wParam 是 PostQuitMessage()的参数值
}

根据 WM_COMMAND 消息的 HIWORD(wParam) 可以区分出消息来自 加速键 被按下 还是 菜单项 被点击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//消息处理函数中对于 WM_COMMAND 消息的处理:
case WM_COMMAND:
OnCommand(hWnd, wParam);
break;

//OnCommand()函数的具体内容如下:
void OnCommand(HWND hWnd, WPARAM wParam) {
switch (LOWORD(wParam)) {
case ID_40001:
{
if (!HIWORD(wParam)) {
MessageBox(hWnd, "菜单项新建被点击", "Information", MB_OK);
}
else {
MessageBox(hWnd, "新建快捷键Ctrl+N被按下", "Information", MB_OK);
}
}
break;
case ID_40002:
{
if (!HIWORD(wParam)) {
MessageBox(hWnd, "菜单项打开被点击", "Information", MB_OK);
}
else {
MessageBox(hWnd, "打开快捷键Ctrl+O被按下", "Information", MB_OK);
}
}
break;
case ID_40003:
{
if (!HIWORD(wParam)) {
MessageBox(hWnd, "菜单项退出被点击", "Information", MB_OK);
}
else {
MessageBox(hWnd, "退出快捷键Ctrl+Q被按下", "Information", MB_OK);
}
}
break;
case ID_other:
MessageBox(hWnd, "快捷键Ctrl+R被按下", "Information", MB_OK);
break;
}
}

运行:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述




WM_PAINT 消息

WM_PAINT

  • 产生时间:当窗口需要绘制的时候。(窗口第一次显示,窗口被拖拽、大小发生变化等等)
  • 附带信息:
    • wParam 、lParam :0。
  • 专职用法:用于绘图。

ShowWindow() 函数在调用显示窗口的时候会发出一个 WM_PAINT 消息。

GetMessage() 函数在执行过程中也会检查窗口是否需要重新绘制,并发出 WM_PAINT 消息。

  • InvalidateRect :需要重新绘制的区域。

    1
    2
    3
    4
    5
    BOOL InvalidateRect(
    HWND hWnd, //窗口句柄
    CONST RECT* lpRect, //区域的矩形坐标,参数为空的话表明整个窗口都需要重新绘制
    BOOL bErase //重绘前是否需要擦除
    );

    其中 RECT 是一个封装了矩形 上下左右的结构体:

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

    InvalidateRect 该函数一旦调用,GetMessage() 也会发出 WM_PAINT 消息。

    用于通知Windows系统某个窗口或者指定的矩形区域需要重绘。当调用InvalidateRect函数时,系统会发送一个WM_PAINT消息给窗口,提示它需要重新绘制。这在开发Windows应用程序时非常有用,可以在需要更新界面时强制触发重绘操作。

    一般情况下,开发者会在需要更新窗口内容的时候调用InvalidateRect函数,然后系统会在适当的时间批量处理所有需要重绘的区域,提高绘制效率。通过使用InvalidateRect函数,开发者可以实现界面的实时更新和响应用户操作,提升用户体验。




绘图

绘图步骤

  • 开始绘图:
    1
    2
    3
    4
    HDC BeginPaint(
    HWND hWnd, //绘图窗口
    LPPAINTSTRUCT lpPaint //绘图参数的BUFF
    ); //返回绘图设备句柄 HDC
  • 正式绘图
  • 结束绘图
    1
    2
    3
    4
    BOOL EndPaint(
    HWND hWnd, //绘图窗口
    CONST PAINTSTRUCT *lpPaint //绘图参数的指针,BeginPaint返回
    );

按下鼠标左键进行字符串绘制:使用TextOut()函数,在客户区绘制字符串。

在这里插入图片描述




绘图编程

绘图基础

GDI - Windows graphics device interface Windows图形设备接口(Win32提供的绘图API), 是 Windows 操作系统提供的一组函数和数据结构,用于在屏幕上绘制图形、文本和图像。GDI 提供了访问和控制显示设备的功能,包括绘制形状、填充颜色、显示文本、裁剪图像等。开发人员可以使用 GDI 来创建图形用户界面(GUI)和进行图形处理,例如创建窗口、按钮、菜单等界面元素。GDI 在 Windows 中扮演着重要的角色,为应用程序提供了图形处理能力,帮助用户和开发者实现丰富的图形显示效果。

绘图设备 DC(Device Context 设备上下文)
Device Context (设备上下文) 是Windows开发中的重要概念,它指代一种用于绘制图形、文本和图像的抽象对象。

Device Context 封装了与设备相关的绘图操作,允许开发者在绘制到屏幕、打印机或其他输出设备上时使用统一的接口。通过与设备上下文进行交互,开发者可以绘制图形、操纵字体、绘制文本、处理图像和执行其他与绘图相关的操作。

DC可以看成 Windows 的画家,想要绘图就要先找到这个画家,通过 DC 句柄来找到画家。

HDC hdc = BeginPaint(hWnd, &pc); BeginPaint() 函数用来找到画家,并将返回值赋值给 HDC。BeginPaint() 的第一个参数 hWnd 窗口句柄,告诉画家在哪个窗口画画。

HDC - DC句柄,表示绘图设备。

抓到 HDC 以后,就可以使用各种绘图函数来进行图形绘制。

绘制图形结束后 EndPaint() 释放DC。

颜色:

  • 计算机使用 红、绿、蓝 (Red、Green、Blue:RGB) 作为三原色,计算机使用的三原色是物理中的三原色,通过这三种颜色的不同程度的配比可以得到各种各样的颜色,拿放大镜观察屏幕可以看到这三原色构成了显示器的各种颜色。(物理中的三原色要区别于美术中的三原色,美术中的三原色是指黄红蓝,这三种颜料可以配出绝大多数颜色的颜料。)
    • R值(红色值):0 ~ 255,(2^8^ = 256,一个字节)。
    • G:0 ~ 255。
    • B:0 ~ 255。
    • 每个点的颜色是3个字节24位保存 0 ~ 2^24^ - 1,总共有 16,777,216 种颜色。
    • 在过去的计算机中,使用16位来保存颜色值,低5位Red,第二个5位Green,最高6位Blue。
    • 32位表示颜色值:前面的24位用来表示红色(R)、绿色(G)和蓝色(B)的色彩强度,而后面的8位则用来表示 alpha 通道。Alpha 通道通常用来表示颜色的透明度,数值范围从 0 到 255,0 表示完全透明,255 表示完全不透明。通过调整alpha通道的数值,可以控制颜色的透明度,使得它可以适应各种混合和叠加效果。这种32位的颜色表示方式常用于计算机图形和游戏开发中。

颜色的使用:

使用 COLORREF 类型来声明一个颜色值。

1
2
3
typedef DWORD   COLORREF;     //本质是 DWORD。

typedef unsigned long DWORD; // DWORD 本质就是 unsigned long

unsigned long 类型占 4 个字节,正好 32 位,正好可以用来存储颜色值:
在这里插入图片描述


声明一个颜色值 COLORREF nColor = 0;

赋值使用 RGB宏:nColor = RGB(0,0,255); //蓝色

获取RGB值:GetRValue/GetGValue/GetBValue。BYTE nRed = GetRValue(nColor); 除了使用这几个函数,也自己自己通过 除法取余运算 来获得各个字节的值。


画点:

1
2
3
4
5
6
7
//SetPixel 设置指定点的颜色
COLORREF SetPixel(
HDC hdc, // DC 句柄
int X, // X 坐标
int Y, // Y 坐标
COLORREF crColor //设置颜色
); //返回点原来的颜色

在消息处理函数的 switch 语句中添加对 WM_PAINT 消息的处理:在像素 100,100 的位置处绘制了一个非常小的黑点。

在这里插入图片描述


使用 for 循环绘制渐变色: 使用 for 循环绘制点,点成线,线成面,R和G的值在绘制的过程中不断变化,形成渐变色方块,第一个方块中,Blue的分量为255,第二个方块中 Blue的分量为128:

在这里插入图片描述

使用 for 循环一个一个点地去绘制,并不是一种高效的绘制方法。


基本图形绘制


线的使用(直线、弧线)

  • MoveToEx :指定窗口当前点(窗口当前点默认在(0,0)处,窗口客户区左上角)。
  • LineTo :从窗口当前点到指定点绘制一条直线。
1
2
MoveToEx(hdc, 0, 260, NULL);    //起点
LineTo(hdc,256, 390); //终点(直线),LineTo 绘制完直线后,会将窗口当前点设置到256,390。

封闭图形:能够用画刷填充的图形(Rectangle,Ellipse)。

1
2
3
Rectangle(hdc, 100, 100, 300, 300);  //绘制直角矩形 坐标:左上->右下

Ellipse(hdc, 250, 100, 300,200); //坐标:左上->右下,矩形中的内切圆或椭圆

更多的图形的绘制函数可以在网上查找,不赘述。


GDI 绘图对象

画笔

画笔的作用:线的颜色、线型、粗细。

HPEN 画笔句柄。

画笔的使用

  • 创建画笔
    •   HPEN CreatePen(
            int fnPenStyle,   //画笔样式,PS_SOLID,实心线,可以支持多个像素宽,其他线型只能是一个像素宽。
            int nWidth,         //画笔粗细
            COLORREF crColor    //画笔颜色
        );      //创建成功返回句柄
        
      1
      2
      3
      4
      5
      6
      - 将画笔应用到DC中
      ```cpp
      HGDIOBJ SelectObject(
      HDC hdc, //绘图设备句柄
      HGDIOBJ hgdiobj //GDI绘图对象句柄,画笔句柄
      ); //返回原来的GDI绘图对象句柄,注意保存原来DC当中的画笔
  • 绘图完成后,取出DC画笔,使用SelectObject()函数。
  • 释放画笔:
    1
    2
    3
    BOOL DeleteObject(
    HGDIOBJ hObject //GDI绘图对象句柄,画笔句柄
    ); //只能删除不被DC使用的画笔,所以在释放前,必须将画笔从DC中取出

代码如图所示:

在这里插入图片描述

画笔样式也可以是虚线 PS_DASH ,非实线画笔像素宽度必须为 1 像素。
HPEN red_pen = CreatePen(PS_DASH, 1, RGB(255, 0, 0));

画刷

画刷的作用给封闭图形填充颜色或图案,封闭图形有 RectangleEllipse

HBRUSH :画刷句柄,保存画刷。

画刷的使用步骤跟画笔的使用步骤基本一样:

  • 创建画刷
    • CreateSolidBrush :创建实心画刷。
    • CreateHatchBrush :创建纹理画刷。
  • 将画刷应用到 DC 中:SelectObject()。
  • 绘图
  • 将画刷从 DC 中取出。
  • 删除画刷:DeleteObject()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void OnPaint(HWND hWnd) {
PAINTSTRUCT ps = { 0 };
HDC hdc = BeginPaint(hWnd, &ps); // 获得 DC 句柄

HPEN red_pen = CreatePen(PS_DASH, 1, RGB(255, 0, 0)); //1个像素宽,虚线的红色画笔
HGDIOBJ old_pen = SelectObject(hdc, red_pen);

HBRUSH blue_brush = CreateSolidBrush(RGB(0, 0, 255));
HGDIOBJ old_brush = SelectObject(hdc, blue_brush); //将DC原来的画刷置换出来,DC默认画刷是白色的。

MoveToEx(hdc, 0, 260, NULL);
LineTo(hdc,256, 390);
LineTo(hdc, 1, 1);
LineTo(hdc, 23, 199);

Rectangle(hdc, 100, 100, 300, 300);

HBRUSH green_brush = CreateHatchBrush(HS_CROSS, RGB(0, 255, 0));
SelectObject(hdc, green_brush);

Ellipse(hdc, 250, 100, 300, 200);

SelectObject(hdc, old_brush);
DeleteObject(blue_brush);
DeleteObject(green_brush);

SelectObject(hdc, old_pen); //置换出红色画笔
DeleteObject(red_pen);

EndPaint(hWnd, &ps); //结束绘画
}

在这里插入图片描述


为了让绘制的封闭图形看上去与背景相契合,可以使用透明画刷。透明画刷已经存在于操作系统中,想要使用透明画刷,只需要向操作系统借用即可。

使用 GetStockObject() 函数获取由操作系统维护的画笔、画刷、字体等等。向操作系统借用的画笔、画刷等使用完后,不需要 DeleteObject()

向操作系统借用透明画刷:

1
2
HGDIOBJ transparent_brush = GetStockObject(NULL_BRUSH);
HGDIOBJ old_brush = SelectObject(hdc, transparent_brush);

位图

  • 光栅图形:记录图像中每一点的颜色等信息,光栅图形是由像素(图像的最小单元)组成的图形,它们在网格状的二维数组中描述。在光栅图形中,每个像素都有自己的颜色值,图像的清晰度和细节取决于像素的密度和分辨率。常见的光栅图形格式包括 JPEG、PNG、BMP 等。复杂的图像和真实世界的场景,但在缩放和变换时可能会失真。
  • 矢量图形:记录图像算法,绘图指令等,矢量图形使用数学公式描述图形,它由线条、曲线和填充区域等基本几何形状组成。与光栅图形不同,矢量图形以对象的形式存储,因此可以在任意比例下缩放而不失真。常见的矢量图形格式包括 SVG、EPS、AI 等。矢量形适合用于图标、标志、表和大型设计元素,能够保持清晰度并支持无损缩放。

位图句柄:HBITMAP,位图句柄能在内存中找到一块内存,里面保存着位图图像每个点的颜色值等信息。


位图的使用

  • 在资源中添加位图资源。

  • 从资源中加载位图 LoadBitmap()

  • 创建一个与当前 DC 相匹配的 DC (内存 DC)。

    •   HDC CeateCompatibleDC(
            HDC hdc     //当前 DC 句柄,可以为 NULL (使用 屏幕 DC)
        );   //返回创建好的的 DC 句柄
        /*
            这里的 内存 DC 是一个在内存中的 DC,可以理解为一个虚拟的 DC
        */
        
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      - 使用 SelectObject 将位图的数据放入虚拟的内存DC中。
      - 成像(1:1比例):将内存 DC 中的图像显示在窗口上。
      - ```cpp
      BOOL BitBlt(
      HDC hdcDest, //目的DC

      int nXDest, //目的DC左上角坐标
      int nYDest, //目的右上角坐标
      int nWidth, //目的宽度
      int nHeight, //目的高度,这四个参数确定成像位置和成像区域

      HDC hdcStr, //源DC
      int nXSrc, //源左上角坐标
      int nYSrc, //源右上角坐标,从虚拟DC的什么位置开始成像

      DWORD dwRop //成像方法 SRCCOPY
      );
  • 缩放成像:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    BOOL StretchBlt(
    HDC hdcDest, //handle to destination DC
    int nXOriginDest, //x-coord of destination upper-left corner
    int nYOriginDest, //y-coord of destination upper-right corner
    int nwidthDest, //width of destination rectangle
    int nHeightDest, //height of destination rectangle
    HDC hdcSrc, //handle to source DC
    int nXOriginSrc, //x-coord of source upper-left corner
    int nYOriginSrc, //y-coord of source upper-right corner
    int nWidthSrc, //源DC宽
    int nHeightSrc, //源DC高
    DWORD dwRop //raster operation code
    );
  • 使用 SelectObject 函数取出位图。

  • 释放位图 DeleteObject。

  • 释放匹配的 DC:DeleteDC。


添加 bitmap :添加 bitmap 成功之后,会显示 bitmap 的绘制界面,也可以修改 Resource.rc 文件中对于 bitmap 资源的描述,修改为从外部导入的 bitmap 文件。

在这里插入图片描述

在这里插入图片描述


1:1 成像代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PAINTSTRUCT ps = { 0 };
HDC hdc = BeginPaint(hWnd, &ps); // 获得 DC 句柄

//添加位图资源
HBITMAP hBmp1 = LoadBitmap(g_hInstance, (char*)IDB_BITMAP1);
HDC hMemdc = CreateCompatibleDC(hdc); //创建一个DC,构建一个虚拟区域,并且内存DC在虚拟区域中绘图

HGDIOBJ oldbmp = SelectObject(hMemdc, hBmp1);
BitBlt(hdc, 10, 10, 1536, 1024, hMemdc, 0, 0, SRCCOPY);

SelectObject(hMemdc, oldbmp);
DeleteObject(hBmp1);
DeleteDC(hMemdc);

EndPaint(hWnd, &ps); //结束绘画

在窗口中 1:1 显示位图:(该图高1536像素,宽1024像素,所以 1:1 显示无法看到全部图像。)

在这里插入图片描述


缩放成像:

1
2
3
4
5
StretchBlt(hdc, 5, 5, //窗口位置
512, 768, //窗口区域
hMemdc, 0, 0, //从图像的的哪里开始绘制
1024, 1536, //显示图像的宽高
SRCCOPY);

运行:
在这里插入图片描述

从上图的运行结果可以看到,图片缩小后显示,出现了很多的纹理。出现这种情况可能是因为在使用StretchBlt函数时,将图像进行缩放时导致了图像质量下降。StretchBlt函数可以对图像进行拉伸和压缩,但它是基于像素级的操作,因此在缩放时可能会导致图像的锯齿状边缘和失真。

想要在窗口中显示缩小的BMP图像并保持较好的质量,可以考虑使用双线性插值进行图像缩放。双线性插值是一种图像处理算法,它可以在进行图像缩放时减少锯齿状边缘和纹理。

使用 GDI+ 库来实现双线性插值

首先配置项目属性:在 Solution Explorer 栏下,右键项目名,项目属性 properties——>Linker 链接器——>Input——>Additional Dependencies 附加属性后面:添加一个值 gdiplus.lib; (注意每个值之间用英文版分号 ; 隔开)。
在这里插入图片描述

或者在代码第一行添加如下代码:

1
#pragma comment(lib,"Gdiplus.lib")     //这将使编译器在编译时自动链接所需的 Gdiplus 库,放在代码第一行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <gdiplus.h>     //包含所需要的头文件

using namespace Gdiplus; //使用命名空间
... ...

void OnPaint(HWND hWnd) {
//添加位图资源
HBITMAP hBmp1 = LoadBitmap(g_hInstance, (char*)(IDB_BITMAP1));
HBITMAP hBmp2 = LoadBitmap(GetModuleHandle(NULL), MAKEINTRESOURCE(IDB_BITMAP2));
HBITMAP hBmp3 = LoadBitmap(GetModuleHandle(NULL), MAKEINTRESOURCE(IDB_BITMAP4));
if ((hBmp1 == NULL) && (hBmp2 == NULL) && (hBmp3 == NULL))
{
MessageBox(hWnd, "Failed to load bitmap resource", "Error", MB_OK | MB_ICONERROR);
exit(-1);
}

Bitmap bitmap1(hBmp1, NULL);
Bitmap bitmap2(hBmp2, NULL);
Bitmap bitmap3(hBmp3, NULL);

// 缩小图片并保证质量
HDC hdc = GetDC(hWnd);
Graphics graphics(hdc);
graphics.SetInterpolationMode(InterpolationModeHighQualityBicubic);
graphics.DrawImage(&bitmap1, 0, 0, 512, 768); // 以左上角为起点,缩放到 512x768 大小
graphics.DrawImage(&bitmap2, 512, 0, 512, 768);
graphics.DrawImage(&bitmap3, 1024, 0, 512, 768);

// 释放资源
DeleteObject(hBmp1);
DeleteObject(hBmp2);
DeleteObject(hBmp3);
ReleaseDC(hWnd, hdc);
}

//消息处理
LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
switch (msgID)
{
case WM_PAINT:
OnPaint(hWnd);
break;
... ...
}

... ...
//WinMain函数添加下面代码:
int CALLBACK WinMain(HINSTANCE hIns, HINSTANCE hPreIns, LPSTR lpCmdLine, int nCmdShow)
{
//在程序开始时初始化GDI+库
GdiplusStartupInput gdiplusStartupInput; //定义了一个名为gdiplusStartupInput的变量,其类型是GdiplusStartupInput。GdiplusStartupInput是一个结构体,用于指定GDI+库的初始化参数。通常情况下,你可以保持其默认值。
ULONG_PTR gdiplusToken; //定义了一个名为gdiplusToken的变量,其类型是ULONG_PTR。在初始化GDI+库时,GdiplusStartup函数将为其分配一个令牌,并将此令牌存储在gdiplusToken中。这个令牌是用来标识GDI+库的实例化过程。
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); //调用了GdiplusStartup函数来初始化GDI+库。它接受三个参数:分别是一个指向令牌的指针(&gdiplusToken),一个指向GdiplusStartupInput结构的指针(&gdiplusStartupInput),以及一个GdiplusStartupOutput结构的指针,通常为NULL
... ...

while(1) {
... ... //消息循环
}

//消息循环后面添加
GdiplusShutdown(gdiplusToken); //调用了GdiplusShutdown函数来清理并关闭GDI+库。它接受一个参数,即在初始化时获得的令牌gdiplusToken。该函数用于释放GDI+库所使用的资源,确保在程序结束时正确清理。
... ...
return ...
}

运行:消除图片缩小时产生的纹理。

在这里插入图片描述

修改图片绘制的透明度为 0.5 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 设置透明度
ColorMatrix colorMatrix = {
1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.5f, 0.0f, // 设置透明度,范围从0.0(完全透明)到1.0(完全不透明)
0.0f, 0.0f, 0.0f, 0.0f, 1.0f
};

ImageAttributes imageAttr;
imageAttr.SetColorMatrix(&colorMatrix, ColorMatrixFlagsDefault, ColorAdjustTypeBitmap);

// 绘制图像
graphics.DrawImage(&bitmap1, RectF(0, 0, 512, 768), 0, 0, bitmap1.GetWidth(), bitmap1.GetHeight(), UnitPixel, &imageAttr);
graphics.DrawImage(&bitmap2, RectF(512, 0, 512, 768), 0, 0, bitmap2.GetWidth(), bitmap2.GetHeight(), UnitPixel, &imageAttr);
graphics.DrawImage(&bitmap3, RectF(1024, 0, 512, 768), 0, 0, bitmap3.GetWidth(), bitmap3.GetHeight(), UnitPixel, &imageAttr);

运行:

在这里插入图片描述


1
2
3
4
5
6
7
ColorMatrix colorMatrix = {
1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.5f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 1.0f
};

这段代码创建了一个 ColorMatrix 对象,其中第四行第四列的值被设置为 0.5,即设置了图像的透明度为 50%。

ColorMatrix 是一个 5x5 的矩阵,用于在 GDI+ 中进行颜色转换。当我们将一个图像绘制到目标上时,可以使用 ColorMatrix 对图像进行颜色调整。它的基本原理是将每个像素的颜色值与 ColorMatrix 进行矩阵乘法运算,得到新的颜色值。

在这个 ColorMatrix 中,每行代表着输出颜色的一个分量(红、绿、蓝、透明度和偏移量),而每列代表输入颜色的一个分量。在这个矩阵中,第四行第四列的值表示了输出的 alpha(透明度)通道,因此将其设置为 0.5 就使得输出的透明度减半,从而实现了图像的半透明效果。

通过调整 ColorMatrix 中的各个元素,我们可以实现对图像的不同颜色分量的调整,包括亮度、对比度、饱和度以及透明度等。这种颜色转换的方式提供了一种非常灵活的方法,可以在绘制图像时对其进行各种颜色效果的处理。


文本绘制

在前文中提到了一个文本的绘制函数 TextOut (将文字绘制在指定坐标位置),这个函数对于文本的绘制功能比较基础。下面介绍 DrawText() 函数:

1
2
3
4
5
6
7
int DrawText(
HDC hdc, //DC句柄
LPCTSTR lpString, //字符串
int nCount, //字符数量
LPRECT lpRect, //绘制文字的矩形框
UINT uFormat //绘制的方式
);
1
2
3
4
5
6
7
RECT rc;    //矩形结构体
rc.left = 100; //矩形距离窗口左边距离
rc.top = 150; //矩形距离窗口顶端的距离
rc.right = 200;
rc.bottom = 200;
Rectangle(hdc, 100, 150, 200, 200); //绘制矩形以查看字符串绘制的位置
DrawText(hdc, "你好 hello hello Long LONG LONG", strlen("你好 hello hello Long LONG LONG"), &rc, DT_LEFT | DT_TOP | DT_WORDBREAK | DT_NOCLIP);
  • DT_LEFT :水平靠左开始绘制字符串。
  • DT_TOP :垂直靠上绘制字符串。
  • DT_WORDBREAK :单行超出矩形右侧的文本另起一行绘制。
  • DT_NOCLIP :文本太长超出矩形范围时,不剪切字符串(完整显示所有文本)。
  • DT_CENTER :水平居中。
  • DT_VCENTER :垂直居中。(只能单行居中,与WORDBREAK冲突)
  • DT_SINGLELINE :单行绘制。
    … …

文字颜色和背景:

  • 文字颜色:SetTextColor。

    •   SetTextColor(hdc, RGB(0, 0, 255));   //蓝色字体
        
        DrawText(hdc, "你好\n hello hello\n Long\n LONG LONG Long\n LONG", strlen("你好\n hello hello\n Long\n LONG LONG Long\n LONG"), &rc, DT_CENTER | DT_VCENTER | DT_NOCLIP);
        
      1
      2
      3
      4
      5
      		![在这里插入图片描述](https://jackey-song.com/img/direct/dbfe792c29f441b3af5a0df61720518f.png)

      - 文字背景:SetBkColor。(默认背景色白色)
      - ```cpp
      SetBkColor(hdc, RGB(0, 128, 200)); //浅蓝色背景
      ![在这里插入图片描述](https://jackey-song.com/img/direct/b5be766309a2411cb83c947e7f8f29eb.png)
  • 文字背景模式:SetBkMode (OPAQUE :默认的不透明模式 / TRANSPARENT :透明模式)。

    •   SetBkMode(hdc, TRANSPARENT);
        
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      		![在这里插入图片描述](https://jackey-song.com/img/direct/8913edb785fd4a5785b96568d5f62e4d.png)

      ---
      #### 字体
      Windows 常用字体为 TrueType 格式的字体文件。

      字体名:标识字体类型。
      HFONT :字体句柄。

      在 Windows 系统盘 ``C:/Windows/Fonts`` 目录下有着各种各样的字体:这些字体文件都是 TrueType 格式的,也就是每个字体文件中保存着每个字的真实点阵字型(位图字体,每个字的真实外观),即使双击打开,也不能查看到每个字,只是一个预览而已。

      ![在这里插入图片描述](https://jackey-song.com/img/direct/e577b0a6cf7e4bb489bb35b27b1677fd.png)

      ![在这里插入图片描述](https://jackey-song.com/img/direct/ca486fedd28d495fa6b19c2c67103121.png)

      ---
      **字体的使用**:
      - 创建字体:想要创建一个字体,那么该电脑中就要有该字体的字体文件。
      - ```cpp
      HFONT CreateFont(
      int nHeight, //字体高度
      int nWidth, //字体宽度,字体高度给出后,宽度填0即可自动匹配合适的宽度
      int nEscapement, //字符串倾斜角度,字符串与水平方向的夹角,以0.1度为单位。(字体站得竖直,但是字符串中所有的字符不在一水平线上)
      int nOrientation, //字符旋转角度,以x轴为轴心,向里或向外旋转角度。
      int fnWeight, //字体的粗细
      DWORD fdwItalic, //斜体,1或0。
      DWORD fdwUnderline, //字符下划线
      DWORD fdwStrikeOut, //删除线
      DWORD fdwCharSet, //字符集
      DWORD fdwOutputPrecision, //输出精度,没什么用
      DWORD fdwClipPrecision, //剪切精度,没什么用
      DWORD fdwQuality, //输出质量,没什么用
      DWORD fdwPitchAndFamily, //匹配字体,没什么用
      LPCTSTR lpszFace //字体名称
      );
  • 应用到字体 DC,(SelectObject)。

  • 绘制文本,(DrawText / TextOut)。

  • 取出字体,(SelectObject)。

  • 删除字体,(DeleteObject)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SetTextColor(hdc, RGB(0, 128, 200));    //字体颜色浅蓝色
SetBkColor(hdc, RGB(0, 128, 200)); //字体背景色
SetBkMode(hdc, TRANSPARENT); //设置字体背景为透明色,此时背景色会被覆盖

HFONT hFont = CreateFont(30, 0, 45, 0, 900, 1, 1, 1, GB2312_CHARSET, 0, 0, 0, 0, "楷体"); //创建字体
HGDIOBJ oldFont = SelectObject(hdc, hFont);

HGDIOBJ hBrush = GetStockObject(NULL_BRUSH); //创建透明画刷
HGDIOBJ oldBrush = SelectObject(hdc, hBrush);

RECT rc;
rc.left = 100;
rc.top = 150;
rc.right = 200;
rc.bottom = 200;
Rectangle(hdc, 100, 150, 200, 200); //绘制矩形背景为透明色
DrawText(hdc, "你好\n hello hello\n Long\n LONG LONG Long\n LONG", strlen("你好\n hello hello\n Long\n LONG LONG Long\n LONG"), &rc, DT_CENTER | DT_VCENTER | DT_NOCLIP);

// 释放资源
SelectObject(hdc, oldFont);
DeleteObject(hFont);
SelectObject(hdc, oldBrush);
DeleteObject(hBrush);

字体绘制如下:

在这里插入图片描述




请求一个图片的 url 并在窗口中显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include <Windows.h>
#include <WinINet.h>
#include <gdiplus.h>
#include <Shlwapi.h>
#include <vector>
#include <string>

#pragma comment(lib, "wininet.lib")
#pragma comment(lib, "gdiplus.lib")
#pragma comment(lib, "Shlwapi.lib")

using namespace Gdiplus;

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
// Initialize GDI+
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

// Register the window class
const char CLASS_NAME[] = "Sample Window Class";

WNDCLASS wc = {};
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;

RegisterClass(&wc);

// Create the window
HWND hwnd = CreateWindowEx(
0, // Optional window styles
CLASS_NAME, // Window class
"HTTP Image Viewer", // Window text
WS_OVERLAPPEDWINDOW, // Window style

// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, 800, 800,

NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);

if (hwnd == NULL) {
return 0;
}

ShowWindow(hwnd, nCmdShow);

// Main message loop
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}

// Shutdown GDI+
GdiplusShutdown(gdiplusToken);

return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);

// Example URL to request image from
std::string url = "https://jackey-song.com/img/alipay.jpg";

// Use WinINet to download image data (simplified)
HINTERNET hInternet = InternetOpen("HTTP Example", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (hInternet) {
HINTERNET hConnect = InternetOpenUrl(hInternet, url.c_str(), NULL, 0, INTERNET_FLAG_RELOAD, 0);
if (hConnect) {
// Read image data into a buffer (simplified)
BYTE buffer[1024];
DWORD bytesRead;
std::vector<BYTE> imageData;
while (InternetReadFile(hConnect, buffer, sizeof(buffer), &bytesRead) && bytesRead > 0) {
imageData.insert(imageData.end(), buffer, buffer + bytesRead);
}

// Create GDI+ Bitmap from memory stream (simplified)
if (!imageData.empty()) {
IStream* pStream = SHCreateMemStream(&imageData[0], static_cast<UINT>(imageData.size()));
if (pStream) {
Bitmap bmp(pStream);

// Draw the image using GDI+
Graphics graphics(hdc);
graphics.DrawImage(&bmp, 0, 0, bmp.GetWidth(), bmp.GetHeight());

pStream->Release();
}
else {
OutputDebugString("Failed to create memory stream\n");
}
}
else {
OutputDebugString("Failed to download image data\n");
}

InternetCloseHandle(hConnect);
}
else {
OutputDebugString("Failed to open URL\n");
}

InternetCloseHandle(hInternet);
}
else {
OutputDebugString("Failed to open internet\n");
}

EndPaint(hwnd, &ps);
break;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
Prev:
JavaScript ECMA Script 6
Next:
《Secret》钢琴谱