承接前文:
- win32窗口编程
- windows 开发基础
- win32-注册窗口类、创建窗口
- win32-显示窗口、消息循环、消息队列
- win32-鼠标消息、键盘消息、计时器消息、菜单资源
本文目录
- 图标资源
- 光标资源
- WM_SETCURSOR 消息
- 字符串资源
- 加速键资源
- WM_PAINT 消息
- 绘图
- 绘图编程
- 绘图基础
- 基本图形绘制
- GDI 绘图对象
- 画笔
- 画刷
- 位图
- 文本绘制
- 字体
- 请求一个图片的 url 并在窗口中显示
图标资源
以.ico
为后缀的图片,icon 图标资源。一个图标文件中可以有多个不同分辨率大小的图标,以适应不同场景下的使用。
想要使用图标资源,第一步添加图标资源,第二步加载图标资源,第三步设置窗口类。
HICON LoadIcon(
HINSTANCE hInstance, //handle to application instance
LPCTSTR lpIconName //name string or resource identifier
); //成功返回HICON句柄
添加图标资源:
添加图标资源后可以自己绘制图标:从图中可以看到有不同大小的图标(256x256像素、48x48像素等),有支持彩色的图标,也有黑白图标。
代码中添加资源头文件,并将图标赋值给窗口类的成员变量。
#include "resource.h" //把资源头文件包含进来
... ...
wc.hIcon = LoadIcon(hIns, (char*)IDI_ICON1);
编译运行改变窗口图标:
在Visual Studio中编辑图标并不怎么方便,在这里可以将提前制作好的图标放在项目目录中,修改.rc
文件中对于图标资源的描述,修改为自己提前制作好的图标,然后在Visual Studio中重新加载被修改的 .rc
文件,再次编译运行。
光标资源
光标资源就是鼠标光标,这个光标在不同情况下是不同的:有的时候是箭头光标,有的时候是一个小手,有的时候是一闪一闪的竖杠,等等;在游戏中光标的形状也各不相同。一个光标就是一个图片,也是需要自己提前绘制,然后用代码进行应用。
光标资源使用步骤:
- 第一步添加光标资源,光标的默认大小是32x32像素,每个光标有一个HotSpot热点,也就是点击时生效的点(因为一个光标图片有很多像素,但是生效的一个点只有一个点,这个点的位置可以自己设置,例如箭头光标的热点就在箭头的尖上)。
- 第二步加载资源:
HCURSOR LoadCursor( HINSTANCE hInstance, //handle to application instance LPCTSTR lpCursorName //name or resource identifier ); // hInstance 可以为 NULL, 获取系统默认的Cursor
- 第三步设置光标:
- 注册窗口类时设置。
- 或 使用 SetCursor() 设置光标。
添加光标资源:
绘制光标资源:
使用图中的 Set Hot Spot Tool
来设置光标热点。
设置光标:
#include "resource.h"
... ...
//wc.hCursor = LoadCursor(NULL, IDC_ARROW); //默认光标
wc.hCursor = LoadCursor(hIns, (char*)IDC_CURSOR1);
无论是 LoadCursor()
函数还是 LoadIcon()
函数,他们的作用都是通过窗口实例句柄和资源的ID来在内存中找到这个资源。窗口实例句柄 hInstance
的作用就是在内存中找到一块保存窗口各种信息的内存,这些资源被编译后会编译到 .exe
里面,当程序运行的时候,程序就会被加载到内存,其中就包括了各种资源。
自己创建的光标只在客户区起作用,在标题栏就会自动变成默认光标。
使用 SetCursor() 函数可以在程序运行的过程中修改光标:
HCURSOR SetCursor(
HCURSOR hCursor // handle to cursor
);
SetCursor()函数必须放在 WM_SETCURSOR
消息下进行调用。
WM_SETCURSOR 消息
只有光标有移动的情况,这个消息就会不断地出现。专职用法:改光标。
附带参数:
- wParam :当前使用的光标句柄。
- lParam :
- LOWORD :当前区域的代码(Hit-Test code)HTCLIENT (客户区)/ HTCAPTION(标题栏、边框)。
- HIWORD :没太大用处。
再绘制一个光标 IDC_CURSOR2
,在程序运行时进行切换:
//添加全局变量 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;即可解决光标随着鼠标的移动不断闪烁修改的问题。
//修改如下
//在客户区光标时 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;
字符串资源
字符串资源存在的目的就是方便代码中各种字符串的修改,例如语言的切换(中英文切换等)。
具体来说,字符串资源的作用包括:
中心化管理文本:将应用程序中使用的所有文本信息集中管理,方便统一修改和维护。
多语言支持:通过为不同语言单独创建字符串资源文件,可以实现应用程序的多语言支持,使应用能够在不同语言环境下运行并显示相应的文本。
提高可维护性:由于文本信息集中管理,修改或更新某个文本只需在字符串资源中进行修改,而不需要在代码中逐个替换,提高了代码的可维护性。
便于国际化和本地化:通过使用字符串资源,可以轻松地将应用程序本地化为不同的语言和区域设置,满足不同用户群体的需求。
节省内存空间:采用字符串资源可以在编译时对字符串进行优化,并且可以节省内存空间。
- 添加字符串资源:添加字符串表,在表中增加字符串。
- 加载字符串资源:
int LoadString( HINSTANCE hInstance, // handle to resource module UINT uID, //字符串ID LPTSTR lpBuffer, //存放字符串BUFF int nBufferMax //字符串BUFF长度 ); //成功返回字符串长度,失败返回0
添加字符串资源:
添加字符串资源:
使用字符串资源:
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对应的加速键。
使用:
//加载加速键表
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_40001
、ID_40002
、ID_40003
分别对应着菜单项的ID 新建
、打开
、退出
。ID_other
不对应任何菜单项。
当菜单项被点击时,或者加速键被按下时,都可以产生 WM_COMMAND
消息。
在 WM_COMMAND
消息中:
- wParam:
- HIWORD 为 1 表示消息产生自加速键,为 0 表示消息产生自菜单项。
- LOWORD 为命令 ID。
- lParam :0.
TranslateAccelerator()函数的内部执行过程:
//伪代码
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;
}
}
加速键的使用:
//加速键要放在消息循环中使用。
//使用前首先加载加速键表
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)
可以区分出消息来自 加速键 被按下 还是 菜单项 被点击:
//消息处理函数中对于 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
:需要重新绘制的区域。BOOL InvalidateRect( HWND hWnd, //窗口句柄 CONST RECT* lpRect, //区域的矩形坐标,参数为空的话表明整个窗口都需要重新绘制 BOOL bErase //重绘前是否需要擦除 );
其中 RECT 是一个封装了矩形 上下左右的结构体:
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函数,开发者可以实现界面的实时更新和响应用户操作,提升用户体验。
绘图
绘图步骤:
- 开始绘图:
HDC BeginPaint( HWND hWnd, //绘图窗口 LPPAINTSTRUCT lpPaint //绘图参数的BUFF ); //返回绘图设备句柄 HDC
- 正式绘图
- 结束绘图
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,(28 = 256,一个字节)。
- G:0 ~ 255。
- B:0 ~ 255。
- 每个点的颜色是3个字节24位保存 0 ~ 224 - 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
类型来声明一个颜色值。
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);
除了使用这几个函数,也自己自己通过 除法取余运算 来获得各个字节的值。
画点:
//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 :从窗口当前点到指定点绘制一条直线。
MoveToEx(hdc, 0, 260, NULL); //起点
LineTo(hdc,256, 390); //终点(直线),LineTo 绘制完直线后,会将窗口当前点设置到256,390。
封闭图形:能够用画刷填充的图形(Rectangle,Ellipse)。
Rectangle(hdc, 100, 100, 300, 300); //绘制直角矩形 坐标:左上->右下
Ellipse(hdc, 250, 100, 300,200); //坐标:左上->右下,矩形中的内切圆或椭圆
更多的图形的绘制函数可以在网上查找,不赘述。
GDI 绘图对象
画笔
画笔的作用:线的颜色、线型、粗细。
HPEN
画笔句柄。
画笔的使用:
- 创建画笔
-
HPEN CreatePen( int fnPenStyle, //画笔样式,PS_SOLID,实心线,可以支持多个像素宽,其他线型只能是一个像素宽。 int nWidth, //画笔粗细 COLORREF crColor //画笔颜色 ); //创建成功返回句柄
-
- 将画笔应用到DC中
HGDIOBJ SelectObject( HDC hdc, //绘图设备句柄 HGDIOBJ hgdiobj //GDI绘图对象句柄,画笔句柄 ); //返回原来的GDI绘图对象句柄,注意保存原来DC当中的画笔
- 绘图完成后,取出DC画笔,使用SelectObject()函数。
- 释放画笔:
BOOL DeleteObject( HGDIOBJ hObject //GDI绘图对象句柄,画笔句柄 ); //只能删除不被DC使用的画笔,所以在释放前,必须将画笔从DC中取出
代码如图所示:
画笔样式也可以是虚线 PS_DASH
,非实线画笔像素宽度必须为 1 像素。
HPEN red_pen = CreatePen(PS_DASH, 1, RGB(255, 0, 0));
画刷
画刷的作用给封闭图形填充颜色或图案,封闭图形有 Rectangle
、Ellipse
。
HBRUSH
:画刷句柄,保存画刷。
画刷的使用步骤跟画笔的使用步骤基本一样:
- 创建画刷
- CreateSolidBrush :创建实心画刷。
- CreateHatchBrush :创建纹理画刷。
- 将画刷应用到 DC 中:SelectObject()。
- 绘图
- 将画刷从 DC 中取出。
- 删除画刷:DeleteObject()。
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()
。
向操作系统借用透明画刷:
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 */
-
-
使用 SelectObject 将位图的数据放入虚拟的内存DC中。
-
成像(1:1比例):将内存 DC 中的图像显示在窗口上。
-
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 );
-
-
缩放成像:
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 成像代码:
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 显示无法看到全部图像。)
缩放成像:
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;
(注意每个值之间用英文版分号 ;
隔开)。
或者在代码第一行添加如下代码:
#pragma comment(lib,"Gdiplus.lib") //这将使编译器在编译时自动链接所需的 Gdiplus 库,放在代码第一行
#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 :
// 设置透明度
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);
运行:
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()
函数:
int DrawText(
HDC hdc, //DC句柄
LPCTSTR lpString, //字符串
int nCount, //字符数量
LPRECT lpRect, //绘制文字的矩形框
UINT uFormat //绘制的方式
);
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);
-
-
文字背景:SetBkColor。(默认背景色白色)
-
SetBkColor(hdc, RGB(0, 128, 200)); //浅蓝色背景
-
-
文字背景模式:SetBkMode (OPAQUE :默认的不透明模式 / TRANSPARENT :透明模式)。
-
SetBkMode(hdc, TRANSPARENT);
-
字体
Windows 常用字体为 TrueType 格式的字体文件。
字体名:标识字体类型。
HFONT :字体句柄。
在 Windows 系统盘 C:/Windows/Fonts
目录下有着各种各样的字体:这些字体文件都是 TrueType 格式的,也就是每个字体文件中保存着每个字的真实点阵字型(位图字体,每个字的真实外观),即使双击打开,也不能查看到每个字,只是一个预览而已。
字体的使用:
- 创建字体:想要创建一个字体,那么该电脑中就要有该字体的字体文件。
-
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)。
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 并在窗口中显示
#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;
}
代码中请求了一张图片:https://jackey-song.com/img/alipay.jpg
,没错就是我的支付宝的收款码,欢迎打赏。