第5章,[Win32 章节] :练习程序,LittleFontPic

第5章,[Win32 章节] :练习程序,LittleFontPic 专栏导航上一篇第5章[Win32 章节] 用画刷填充内部回到目录下一篇第5章[Win32 章节] 练习程序BigFontPic本专栏课件关于本专栏课件的获取方法请参考下述课节。参考课节MFC笔记本专栏课件本节前言对于本节所讲解的知识有可能你会需要时不时地参考本专栏的其它文章。真的遇到了需要参考之前的文章的知识点请你自行查阅。我呢也会提到一部分的参考课节。但是呢你不应该依赖于我的主动提及。最好呢你自己能够多去了解和查看本专栏目录。经过之前的几个课节的讲解我们已经是将 GDI 画笔和画刷以及其他的许多的基础 GDI 知识给讲完了。学到这种程度我们可以画一些个图形了。从本节课开始我们来领着大家一起来学习一些个绘制图形的代码从中来学习 GDI 绘图的逻辑学习 GDI 画笔、画刷等等的 GDI 对象的使用方法与流程学习 GDI 对象的创建、选入设备环境、选出设备环境、删除等等的流程并由此来体验 Windows 编程的专业性。我本人的 PhotoShop 学得一般有些个图画我觉得很难画出来。然而有了 Windows 编程我倒是可以通过编程的方式来绘制出一些个图形来。我想用 Windows 程序设计的知识来绘制图表应该会对各位也能有一定的帮助的。本节内容是我的原创内容而非佩措尔德原教材的内容。一. 佩措尔德原教材插图分析在之前的某个课节我们在课节中给出了下述的图片。图1这一课节的链接如下。参考课节第4章[标签 Win32] 文本尺寸的度量接下来我来将佩措尔德原教材的图片内容给截图然后贴在下面。图2佩措尔德教材原图佩措尔德原教材中的插图我没有通过截图的方式将其放置在参考课节中而是通过编程的方式将它的基本的东西绘制出来然后通过 PhotoShop 进行补充绘制。绘制好了以后放在了课节里面。下面的图片是我用编程的方式绘制出来的基本图。图3在编程之前我是自己用铅笔结合着观察去给佩措尔德的原教材的插图查数查出了原插图中一行里面有多少个模拟像素方格一列里面有多少个模拟像素方格。查询的结果是佩措尔德原教材的插图中一行有 15 个像素方格一列有 16 个方格。想要将其绘制出来我需要绘制一个方格矩阵一个 16 行、15 列的方格矩阵。在这个方格矩阵中有的方格需要涂上颜色有的则是不需要涂颜色。涂色的部分可以统一地采用同一种颜色。这样一来整个的方格矩阵我们可以将其分成两个部分一个是不涂色的部分我们可以将其标记为 0另一个部分是涂色的部分我们可以将其标记为 1 。我们可以建立一个二维数组它的一维的个数对应着方格矩阵的列数15 列它的二维的个数对应着方格矩阵的行数16 行。这个二维数组的名字如下。BOOL Square[16][15];Square[3][10] 代表着原插图中 3 号行、10 号列的模拟像素方格由于数组元素的索引从 0 开始所以呢它实际上对应着原插图中的第 4 行、第 11 列的模拟像素方格。其值代表着这个模拟像素方格是否涂色1 则涂色0 则不涂色。同理Square[6][7] 代表着原插图中 6 号行、7 号列的模拟像素方格由于数组元素的索引从 0 开始所以呢它实际上对应着原插图中的第 7 行、第 8 列的模拟像素方格。其值代表着这个模拟像素方格是否涂色1 则涂色0 则不涂色。这样一来我们就通过二维数组对模拟像素方格矩阵的涂色与否进行了建模。去编程的时候我们的专栏中给出的图片为了让大家较为清楚地看出原插图的行列我们用虚线对框架进行了绘制。用虚线去绘制而不是用实线去绘制所以我们需要自定义画笔需要使用画笔句柄 hPen 。对于模拟像素方格矩阵的 16 行、15 列的框架我们采用 MoveToEx 和 LineTo 函数通过画线的方式将其绘制了出来。如图1 所示我们在绘制框架的时候线条的颜色是红色。由此我们还需要自行创建画刷并将画刷的颜色设置为红色。在具体绘制的时候我们是先用 MoveToEx 和 LineTo 函数来将模拟像素方格矩阵的框架给绘制了出来。框架画完了以后我们接着去给方格矩阵涂色。如前所述我们已经是通过二维数组 Square[6][7]来对原插图中的模拟像素方格矩阵进行了建模。原插图的任何一个像素方格它都要与二维数组中的元素一一对应。并且呢如果原插图中的某一位置涂色或不涂色也都要与二维数组的对应元素的值相对应不得有误。原插图中的某一个模拟像素方格不涂色则要在二维数组中将某一元素标记为 0 原插图中的某一个模拟像素方格涂色则要在二维数组中将某一元素标记为 1。对于二维数组 Square[6][7]我们是在声明的时候直接对其进行了初始化。这样在给模拟像素方格矩阵涂色的时候我们是通过两层的 for 循环来遍历 Square[6][7] 的每一个元素根据元素值的 0 和 1来决定是否对某一个模拟像素方格涂色。在给模拟像素方格涂色的时候如图1 所示我们所涂的颜色是蓝色。这个呢就需要另外一个画刷了。同一个程序中我们的确是可以使用两个画刷句柄变量来建立两个自定义画刷。不过我们实际上只使用了一个画刷句柄。在程序中我们声明的画刷句柄有两个一个是 hBrush另一个是 hOldBrush 。但是呢用来表示自定义画刷的变量实际上只有一个那就是 hBrush 。另一个画刷句柄变量 hOldBrush它承担着辅助职能。关于 hOldBrush我是在将 hBrush 画刷选入设备环境之时用 hOldBrush 来保存旧画刷然后在完成了一段绘制任务以后用 hOldBrush 来将 hBrush 画刷给选出设备环境。自始至终hOldBrush 只是在承担着辅助职能而非用来表示某一个具体的画刷。在绘制模拟像素方格矩阵框架的时候我们使用的是红色的自定义画刷这个画刷的句柄保存在变量 hBrush 之中。绘制完了框架以后我们又将这个红色的自定义画刷给选出了设备环境并将这个红色画刷给删除了。然后呢我们新建了一个自定义画刷这次的画刷是一个蓝色的画刷我们将其又保存在了 hBrush 之中。然后呢将其选入设备环境然后就是在两层的 for 循环之中根据 Square[6][7] 的值来对模拟像素方格矩阵中的方格进行涂色。涂色过程完结之后我们又将蓝色画刷给选出设备环境然后将蓝色画刷给删除。在给模拟像素方格矩阵涂色的时候我考虑到如果让涂色的部分与像素方格的边界重合则会造成模拟像素方格矩阵的框架与涂色部分重合与混淆的问题。为此我在给某个模拟像素方格涂色的时候特意令涂色的部分比矩阵框架的方格外框向内缩减了少许像素的大小。内缩的像素数也许仍然不是特别地清晰但是还算是能够辨识。另外在绘制模拟像素方格矩阵框架的时候我用的是 MoveToEx 和 LineTo 函数使用的线条是红色的虚线为此我使用了自定义画笔并将其设置为红色、PS_DASHDOT 风格。而在涂色的时候我使用了 Rectangle 函数涂色时不需要绘制边框因此我使用了 NULL_PEN 备用画笔。在画笔方面我也是使用了两个变量一个用来表示实际使用的画笔它由 hPen 变量来表示。另一个是辅助画笔变量它由 hPenOld 变量来表示。hPen 与 hPenOld 的使用方法与 hBrush、hOldBrush 的使用方法类似。都是说实际想要使用某一画笔之时或者是创建一个自定义画笔或者是获取系统备用画笔并将这个画笔呢用 hPen 来保存然后将其选入设备环境。选入设备环境之时用 hPenOld 来保存旧画笔。接下来是进行绘制任务完成了一段绘制任务以后通过 hPenOld 来将 hPen 给选出设备环境。然后呢要看 hPen 表示的画笔是系统备用画笔还是自定义画笔如果是系统备用画笔则无须删除它所代表的 GDI 对象。如果是自定义画笔则还需要删除 hPen 所对应的 GDI 对象。以上就是我对于本节要展示的程序代码的整体思路的大致分析了。有可能此时你还不能够完全理解上面的整体分析的含义。那么大致看看就好。等你对本课节的代码完整地阅读了以后再来对照着看看本分节的整体思路分析多看几遍我想你应该能看懂。在下一分节我们来展示代码。二. 本节程序代码本节程序我是用 VS2019 来编译的。解决方案与项目的名称为 LittleFontPic 。代码文件包含有两个一个是头文件 FontPic.h另一个是源文件 WinMain.cpp 。接下来我们来展示代码。一头文件 FontPic.h代码见下面的代码块。#ifndef _FONTPIC_H_ #define _FONTPIC_H_ BOOL Square[16][15] { 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 }; #endif //二源文件 WinMain.cpp代码如下。/*--------------------------------------------------------- 小字体点阵图画 水饺编程2026 ---------------------------------------------------------*/ #include windows.h #include FontPic.h LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] TEXT(LittleFontPic); HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc WndProc; wndclass.cbClsExtra 0; wndclass.cbWndExtra 0; wndclass.hInstance hInstance; wndclass.hIcon LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName NULL; wndclass.lpszClassName szAppName; if (!RegisterClass(wndclass)) { MessageBox(NULL, TEXT(注册窗口类失败), szAppName, MB_ICONERROR); return 0; } hwnd CreateWindow(szAppName, TEXT(小字体点阵图画), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); while (GetMessage(msg, NULL, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); } return msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int xStart, xEnd, yStart, yEnd, nLength; HDC hdc; int i, j, x, y; PAINTSTRUCT ps; static COLORREF clrLine, clrSquare; HPEN hPen, hPenOld; HBRUSH hBrush, hOldBrush; RECT rect; switch (message) { case WM_CREATE: nLength 27; xStart 200; xEnd xStart 15 * nLength; yStart 60; yEnd yStart 16 * nLength; clrLine RGB(255, 0, 0); clrSquare RGB(0, 0, 255); return 0; case WM_PAINT: hdc BeginPaint(hwnd, ps); // 创建并选入画笔 hPen CreatePen(PS_DASHDOT, 0, clrLine); hPenOld (HPEN)SelectObject(hdc, hPen); // 绘制点线方格17个横线16个竖线。 for (i 0, y yStart; i 17; i) { MoveToEx(hdc, xStart, y, NULL); LineTo(hdc, xEnd, y); y nLength; } for (i 0, x xStart; i 16; i) { MoveToEx(hdc, x, yStart, NULL); LineTo(hdc, x, yEnd); x nLength; } // 创建画刷并选入设备环境 hBrush CreateSolidBrush(clrSquare); hOldBrush (HBRUSH)SelectObject(hdc, hBrush); // 将画笔选出设备环境并销毁然后新建一个备用画笔 NULL_PEN。 SelectObject(hdc, hPenOld); DeleteObject(hPen); hPen (HPEN)GetStockObject(NULL_PEN); hPenOld (HPEN)SelectObject(hdc, hPen); for (i 0; i 16; i) for (j 0; j 15; j) if (Square[i][j]) { rect.left xStart j * nLength 2; rect.right xStart (j 1) * nLength - 1; rect.top yStart i * nLength 2; rect.bottom yStart (i 1) * nLength - 1; Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom); } // 将画刷选出设备环境并销毁 SelectObject(hdc, hOldBrush); DeleteObject(hBrush); // 将画笔选出设备环境由于此时 hPen 是备用 // 画笔因而不能销毁。 SelectObject(hdc, hPenOld); EndPaint(hwnd, ps); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); }三部分代码的讲解1强制类型转换在调用 GetStockObject 和 SelectObject 函数的时候如果我们需要将它们的函数返回值给保存在某一个变量里面我们都对其进行了强制类型转换。这是因为这俩函数的返回值是 GDI 对象的通用类型HGDIOBJ而不是某一个具体的类型比如画笔句柄类型 HPEN或者画刷句柄类型 HBRUSH等等。由于这俩函数返回的都是大类型 HGDIOBJ所以呢我们都使用强制类型转换来处理了返回值比如下述的几个代码。图4图5这种强制类型转换这算是 GDI 绘图常见的流程。初学者可能还不太熟悉。但是呢随着你对本专栏的阅读经验的增长这种强制类型转换你会熟悉的。2选入设备环境和选出设备环境图6这是创建画笔并将其选入设备环境的代码。为啥创建了画笔之后要将其选入设备环境呢因为只有将其选入了设备环境以后接下来在使用 GDI 函数绘图的时候才会使用这个画笔。GDI 函数在绘图的时候无论是使用 MoveToEx、LineTo 函数对还是使用 Rectangle、Ellipse 和 RoundRect 等等的绘图函数在绘制线条的时候都用的是选入设备环境之中的那个画笔所绘制的线条。如果呢你在程序中没有单独地创建画笔并将其选入设备环境那么你在绘图的时候将会采用默认画笔BLACK_PEN这是系统的一个备用画笔。在图6 代码区域的下面紧接着的是绘制像素方格矩阵框架的函数如下图所示图7我们要绘制的模拟像素方格矩阵它是16 行、15 列的方格矩阵所以呢绘制它需要 17 个横线、16 个竖线。图7 中的两个 for 循环执行完了以后绘制模拟像素方格矩阵这一任务也就完成了。完成了以后实际上图6 中所创建的 PS_DASHDOT、RGB(255, 0, 0) 所对应的红色风格的线条也就完成了它的使命。接下来我们需要对模拟像素方格矩阵进行涂色了。而在涂色的时候我们用的函数是 Rectangle而这一函数所用的线条是 NULL_PEN也就是无边框。由于图7 之后的画图任务需要使用 NULL_PEN 风格的画笔因而图6 中的自定义画笔就需要被选出设备环境了。为此我们来看下一段代码。图8图6 中我们创建了自定义画笔并将画笔句柄保存在了 hPen 里面。由于它是自定义画笔所以呢我们需要在调用 SelectObject 来将 hPen 选出设备环境以后用 DeleteObject 函数将 hPen 所代表的 GDI 画笔对象给删除掉。为啥要删除掉如果不予以删除那么在我们只用 hPen 一个画笔句柄变量来保存画笔的情况下图8 中的 108 行代码中对 hPen 的重新赋值将会导致图6 中创建的自定义画笔的句柄丢失丢失了以后那个 PS_DASHDOT、红色风格的自定义画笔对象就会驻留在内存中白白地占着地方。内存资源很宝贵如果我们创建的程序是一个大程序里面所使用 GDI 对象会有好多。如果各个都使用完了以后不及时删除它们就会白白地占用着庞大的内存。随着程序的运行你的内存慢慢地就不够用了。使用完了某个 GDI 对象以后及时将其选出设备环境并删除这是对内存资源的节约利用就像我们平时生活中的节约用水和用电一样用完了水龙头要及时关上用完了电也要及时关上。关于选出设备环境其实它算是一种规范性的流程了。在本节程序中以及以后我们会见到的许多的 GDI 绘图任务中用完了某一个 GDI 对象我们是需要将其及时地选出设备环境的。对于选出设备环境的 GDI 对象如果是自定义的 GDI 对象还要及时将其删除以节约利用内存资源。如果是备用对象则不可以将其删除。在图7 中的两个 for 循环所执行的绘制模拟像素方格矩阵的任务完成了以后我们接下来要去进行的是对模拟像素方格矩阵中的某些个模拟像素方格进行涂色。涂色的时候我们要用到的函数是 Rectangle 。想要用它来给模拟像素方格涂色我们还需要使用 GDI 画刷。涂什么颜色就用什么颜色画刷。为此我们在下述截图所示的代码中创建并选入了自定义画刷。图9接下来呢是用两层 for 循环代码来对模拟像素方格进行涂色代码截图如下所示。图10代码本身不难不过你可能会对每个小矩形的左上加2、右下减1 有所疑惑这一点我们在后面的某个小节来讲解。此处暂且不表。图10 所示的涂色代码完成了以后图8 的108、109 行代码中获取系统备用对象并选入设备环境的画笔我们就用完了。同时图9 的 102、103 行所创建并选入设备环境的画刷我们也用完了。既然用完了那就需要将其选出设备环境。由于 hPen 画笔是备用对象因此在选出设备环境后无须删除它也不能删除它。而画刷 hBrush 是自定义对象因此在选出设备环境后需要删除它。将用完了的 GDI 画笔和画刷选出设备环境及删除对象的代码如下图所示。图11在这里对于删除 GDI 对象我们没有像佩措尔德原教材中所讲解的那样在 WM_DESTROY 中去执行 DeleteObject 函数来执行删除任务而是在用完了某个 GDI 对象之后及时删除清理。这种用法其实我也是跟佩措尔德学的。在后续的许多的原教材的代码中佩措尔德先生也是说用完了某个 GDI 自定义对象之后及时删除它而不是非得等到 WM_DESTROY 消息处理的时候才来执行 DeleteObject 函数来执行删除任务。对于这种用完了以后立即清理的编程思路大家需要去体会和熟悉。你不见得能够深刻领悟其含义但是要会用。知道佩措尔德是这么用的广大的 Windows 程序员是这么用的我们也跟着这么用就好了。3涂色的时候对 rect 的尺寸的设置我们重新将图10 的代码截图贴在下面。图10 副本在图10 中j 是模拟像素方格矩阵中每一个竖线的编号。在整个的模拟像素方格矩阵框架中最左侧的竖线我们将其视为 0 号再往右一个是 1 号竖线再往右一个是 2 号竖线等等。而 i 是模拟像素方格矩阵中每一个横线的编程的编号。在整个的模拟像素方格矩阵框架中最上侧的横线我们将其视为 0 号再往下一个是 1 号横线再往下一个是 2 号横线等等。i 和 j 也同时用来在 Square[i][j] 数组元素中用来表示某一个模拟像素方格。Square[i][j] 用来表示 i 号行、j 号列的像素方格模拟像素方格的行列号都是从 0 开始编号。这一点我们在本篇文章的第一分节中有讲述忘了的请参考第一分节的相关内容。对于某一个模拟像素方格如果它是 3 号列则这个方格的左侧的竖线是 3 号竖线右侧的竖线是 4 号竖线。对于一个属于 j 号列的模拟像素方格它的左侧和右侧的竖线的编号分别是 j 号竖线和 (j1) 号竖线。对于某一个模拟像素方格如果它是 6 号行则这个方格的上侧的横线是 6 号横线下侧的横线是 7 号横线。对于一个属于 i 号行的模拟像素方格它的上侧和下侧的横线的编号分别是 i 号横线和 (i1) 号横线。如图10 所示我们在设置 rect 的 left、top、right、bottom 四个字段的时候分别进行了左上加2、右下减1 的设定。为什么这样呢再用画刷填充模拟像素方格的时候我希望涂色的部分与四周的用 MoveToEx、LineTo 函数对绘制的模拟像素方格矩阵框架之间能够空出来一点像素而不是说涂色的部分将模拟像素方格四周的框架给完全覆盖掉或是紧挨着。左上加2,、右下减1是我自己试出来的一个结果你也可以自己将其调整为别的值比如左上加3、右下减2或者是其它的你觉得看着合适的值。4小结到了这里代码部分我相信我已经是大致讲完了。还有不懂的你可以评论或者是私信联系我。三. 代码编译程序的编译和运行方法如果你忘了可以参照下述的两篇文章。参考课节使用 VS2010 编写 C语言程序参考课节用 VS2019 编写C语言程序至于程序的运行结果它差不多就是图3 啦。我们再次将图3 给贴在下面它差不多就是本节程序的运行结果。图3 副本程序运行结果为啥是差不多呢因为我一开始在用编程的方式来绘制图3 的时候我给模拟像素方格涂色时所用的函数是在以后的课节中会讲到的 FillRect 函数。而此时我在给大家讲解 GDI 绘图代码时我给模拟像素方格涂色时所用的函数是应用了 NULL_PEN 备用画笔的 Rectangle 函数。也就是我们在绘制图3 时实际使用的函数是 FillRect 函数。而本节代码中我们为了不超纲为了早一点给大家演示 GDI 绘图代码我们临时将 FillRect 函数替换为应用了 NULL_PEN 备用画笔的 Rectangle 函数。应用了 NULL_PEN 备用画笔的 Rectangle 函数的行为与 FillRect 函数的行为略有不同所以在你那里实际运行结果会与图3 略有不同。此时请不要深抠这点细节。结束语本节内容字数不少我也觉得有些累了。务必学好本节内容。下一节我将展示 BigFontPic 项目代码它与本节代码的逻辑很相似只是少数的几个变量值我设置成了其它的样子。那个代码我将其作为一个思考题请大家自行分析其逻辑。专栏导航上一篇第5章[Win32 章节] 用画刷填充内部回到目录下一篇第5章[Win32 章节] 练习程序BigFontPic