当前位置:   article > 正文

37_MFC自绘UI你离不开GDI绘图_mfc程序ui

mfc程序ui

MFC自绘UI你离不开GDI绘图

GDI (Graphics Device Interface)是图形设备接口的英文缩写,处理Windows程序的图形和图像输出。程序员不需要关心硬件设备及设备驱动,就可以将应用程序的输出转换为硬件设备上的输出,实现应用程序与硬件设备的隔离,大大简化程序开发工作。在Windows操作系统中,图形界面应用程序通常离不开GDI,利用GDI所提供的众多函数可以方便地在屏幕、打印机以及其他输出设备上实现输出文本、图形等操作。

设备环境(DC)

设备无关性(也称设备独立性)是Windows的主要功能之一。应用程序可以在各种设备上进行绘制和打印输出,系统统一把所有外部设备都当作文件来看待,只要安装了它们的驱动程序,应用程序就可以像使用文件一样操纵、使用这些设备,GDI代表应用程序和设备驱动程序进行交互。为了实现设备无关性,引入了逻辑设备和物理设备这两个概念,在应用程序中,使用逻辑设备名称来请求使用某类设备,而系统在实际执行时,使用的是物理设备名称。设备无关性的支持包含在两个动态链接库中,第一个是GDI相关动态链接库,称为图形设备接口﹔第二个是设备驱动程序,设备驱动程序的名称取决于应用程序绘制输出的设备。GDI处理程序的绘图酗数调用,将这些调用传递给设备驱动程序,设备驱动程序接收来自GDI的输入,将输入转换为设备命今,并将这些命今传递给对应的设备。

当程序在客户区中显示文本或图形时,我们通常称程序在“绘制"客户区。GDI在加载驱动程序后,准备设备进行绘制操作,例如选择线条颜色和宽度、画刷颜色和图案、字体名称、裁剪区域等。这些任务是通过创建和维护设备环境(DC)来完成的。DC是定义一组图形对象及其关联属性以及影响输出的图形模式的结构体。

与DC相关的部分图形对象及属性如下表所示。

图形对象属性

  • 画笔样式、宽度和颜色

  • 画刷样式、颜色、图案和原点

  • 字体字体名称、字体大小、字符宽度、字符高度、字符集等

  • 位图大小(以字节为单位),尺寸(以像素为单位)、颜色格式、压缩方案等

  • 路径形状

  • 区域位置和尺寸

与大多数结构体不同,应用程序不能直接访问DC,而是通过调用各种函数间接地对DC结构进行操作。

Windows支持5种图形模式,允许应用程序指定颜色的混合方式、输出的位置、输出的缩放方式等。下表描述了存储在DC中的这些模式。

图形模式描述

  • 背景模式文本的背景色与现有窗口或屏幕颜色的混合方式等
  • 绘图模式画笔、画刷的颜色与目标显示区域颜色的混合方式等
  • 映射模式如何将图形输出从逻辑坐标映射到客户区、屏幕或打印机纸张
  • 多边形填充模式如何使用画刷填充复杂区域的内部
  • 拉伸模式当位图被放大或缩小时如何计算新位图

Windows有4种类型的DC,分别是显示设备DC、打印DC、内存DC(也称内存兼容DC)、信息DC,每种类型的DC都有特定的用途,如下表所述。

DC类型描述

  • 显示设备DC在显示器上进行绘图操作
  • 打印DC在打印机或绘图仪上进行绘图操作
  • 内存DC通常是在内存中的位图上进行绘图操作
  • 信息DC获取设备环境信息

也就是说,通过设备环境,不仅可以在屏幕窗口进行绘图,也可以在打印机或绘图仪上进行绘图,还可以在内存中的位图上进行绘图。关于图形对象、图形模式以及各种DC类型,后面会分别进行详细介绍。

获取显示设备DC句柄

DC句柄是程序使用GDI函数的通行证,几乎所有的GDI绘图函数都需要一个DC句柄参数,有了DC句柄,便能随心所欲地绘制窗口客户区。

前面说过,当窗口客户区的部分或全部变为"无效"且必须"更新"时,比如说改变窗口大小、最小化/最大化窗口、拖动窗口一部分到屏幕外再拖动回来时,应用程序将会获取到WM_PAINT消息。窗口过程的大部分绘图操作是在处理WM_PAINT消息期间进行的,可以通过调用BeginPaint函数来获取显示DC句柄。WM_PAINT消息的处理逻辑一般如下∶

 HDC hdc;//显示设备DC句柄
 PAINTSTRUCT ps;//绘图结构体

 hdc = ::BeginPaint(hwnd, &ps);
 //TODO:在这里开始你的 绘图代码 编写

 ::EndPaint(hwnd, &ps);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

BeginPaint函数的返回值就是需要更新区域的DC句柄hdc。

BeginPaint返回的hdc对应的尺寸仅是无效区域,程序无法通过该句柄绘制到这个区域以外的地方。由于窗口过程每次接收到WM_PAINT消息时的无效区域可能不同,因此这个hdc值仅在当次WM_PAINT消息中有效,程序不应该保存它并把它,用在WM_PAINT消息以外的代码中。BeginPaint和EndPaint函数只能用在WM_PAINT消息中,因为只有这时才存在无效区域。BeginPaint函数还有一个作用就是把无效区域有效化,如果不调用BeginPaint,那么窗口的无效区域就一直不为空,系统会一直发送WM PAINT消息。

窗口客户区中存在一个无效区域,这将导致Windows在应用程序的消息队列中放置一条WM_PAINT消息,即只有当程序客户区的一部分或全部无效时,窗口过程才会接收到WM_PATNT消息。Windows在内部为每个窗口都保存了一个绘制信息结构PAINTSTRUCT,这个结构保存着一个可以覆盖该无效区域的最小矩形的坐标和一些其他信息,这个最小矩形称为无效矩形。如果在窗口过程处理一条WM_PAINT消息之前,窗口客户区中又出现了另一个无效区域,那么Windows将计算出一个可以覆盖这两个无效区域的新的无效区域,并更新
PAINTSTRUCT结构。Windows不会在消息队列中放置多条WM_PAINT消息。

WM_PAINT消息是一个低优先级的消息,Windows总是在消息循环为空的时候孑把WM_PAINT消息放入消息队列。每当消息循环为空的时候,如果Windows发现存在一个无效区域,就会在程序的消息队列中放入一个WM_PAINT消息。前面说过“当程序窗口被首次创建时,整个客户区都是无效的",因为此时应用程序尚未在该窗口上绘制任何东西。在WinMain中调用UpdateWindow函数时会发送第一条
wM_PAINT消息,指示窗口过程在窗口客户区进行绘制,
UpdateWindow函数将WM_PAINT消息直接发送到指定窗口的窗口过程,绕过应用程序的消息队列。现在大家应该明白,
UpdateWindow函数只不过是让窗口过程尽快更新窗口,
HelloWindows程序去掉UpdateWindow函数调用也可以正常运行。
如果应用程序在其他任何时间(例如在处理键盘或鼠标消息期间)需要进行绘制,可以调用GetDC或GetDCEx函数来获取显示DC句柄∶

hdc = GetDC(hwnd);
//绘图代码
ReleaseDC(hwnd, hdc);
  • 1
  • 2
  • 3

GetDC函数返回的hdc对应指定窗口的整个客户区,通过GetDC 函数返回的hdc可以在客户区的任何位置进行绘制操作,不存在无效矩形的概念,无效矩形和BeginPaint才是原配。当使用完毕时,必须调用ReleaseDC函数释放DC。对于用GetDC获取的hdc,Windows建议使用的范围限于单条消息内。当程序处理某条消息的时候,如果需要绘制客户区,可以调用GetDC函数获取hdc,但在消息返回前,必须调用ReleaseDC函数将它释放掉。如果在下一条消息中还需要用到hdc,那么可以重新调用GetDC函数获取。如果将GetDC的hwnd参数设置为NULL,那么函数获取的是整个屏幕的DC句柄。

现在我提出一个问题,相信读者是可以理解的,按下鼠标左键时将会产生WM_LBUTTONDOWN消息,鼠标在客户区中移动的时候会不断产生WM_MOUSEMOVE消息,这两个消息的IParam参数中都含有鼠标坐标信息。按住鼠标左键不放拖动鼠标会产生WM_LBUTTONDOWN消息和一系列WM_MOUSEMOVE消息,我们在窗口过程中处理WM_LBUTTONDOWN和WM_MOUSEMOVE消息,利用GetDC函数获取DC句柄进行绘图,连接
WM_LBUTTONDOWN消息和一系列WM_MOUSEMOVE消息的这些坐标点就会形成一条线,但是当改变窗口大小、最小化然后最大化窗口、拖动窗口一部分到屏幕外再拖回来时,读者会发现这条线没有了,因为在需要重绘的时候Windows会使用指定的背景画刷擦除背景。如果希望这条线继续存在,就必须在WM_PAINT消息中重新绘制(可以事先保存好那些点)。如果可能,我们最好是在WM_PAINT消息中处理所有绘制工作。

GetWindowDC函数可以获取整个窗口的DC句柄,包括非客户区(例如标题栏、菜单和滚动条)。使用GetWindowDC函数返回的hdc可以在窗口的任何位置进行绘制,因为这个DC的原点是窗口的左上角,而不是窗口客户区的左上角,例如,程序可以使用
GetWindowDC函数返回的hdc在窗口的标题栏上进行绘制,这时程序需要处理WM_NCPAINT(非客户区绘制)消息。

HDC GetWindowDC( _In_ HWND hWnd);
  • 1

函数执行成功,返回值是指定窗口的DC句柄。同样的,完成绘制后必须调用ReleaseDC函数来释放DC。如果将参数hWnd参数设置为NULL,GetWindowDC函数获取的是整个屏幕的DC句柄。

Windows有4种类型的DC,关于其他类型DC句柄的获取,后面用到的时候再讲解。理论知识讲解太多实在乏味,接下来先实现一个输出(绘制)文本的示例,并实现滚动条功能。

绘制文本

GetSystemMetrics函数用于获取系统度量或系统配置信息,例如可以获取屏幕分辨率、全屏窗口客户区的宽度和高度、滚动条的宽度和高度等,该函数获取到的相关度量信息均以像素为单位∶

int WINAPI GetSystemMetrics(_In_ int nlndex);
  • 1

该函数只有一个参数,称之为索引,这个索引有95个标识符可以使用。

例如GetSystemMetrics(SM_CXSCREEN)获取的是屏幕的宽度(Cx表示 Count X,X轴像素数), SystemMetrics程序根据95个索引在客户区中输出95行,每行的格式类似下面的样子∶

SM_CXSCREEN	屏幕的宽度	1366
  • 1

通过TextOut函数输出METRICS结构数组的每个数组元素很简单。这里列举下笔者的实现。

  • Metrics.h
#pragma once

struct
{
    int     m_nIndex;
    PTSTR   m_pLabel;
    PTSTR   m_pDesc;
}METRICS[] = {
    SM_CXSCREEN,                    TEXT("SM_CXSCREEN"),                    TEXT("屏幕的宽度"),
    SM_CYSCREEN,                    TEXT("SM_CYSCREEN"),                    TEXT("屏幕的高度"),
    SM_CXFULLSCREEN,                TEXT("SM_CXFULLSCREEN"),                TEXT("全屏窗口的客户区宽度"),
    SM_CYFULLSCREEN,                TEXT("SM_CYFULLSCREEN"),                TEXT("全屏窗口的客户区高度"),
    SM_ARRANGE,                     TEXT("SM_ARRANGE"),                     TEXT("如何排列最小化窗口"),
    SM_CLEANBOOT,                   TEXT("SM_CLEANBOOT"),                   TEXT("系统启动方式"),
    SM_CMONITORS,                   TEXT("SM_CMONITORS"),                   TEXT("监视器的数量"),
    SM_CMOUSEBUTTONS,               TEXT("SM_CMOUSEBUTTONS"),               TEXT("鼠标上的按钮数"),
    SM_CONVERTIBLESLATEMODE,        TEXT("SM_CONVERTIBLESLATEMODE"),        TEXT("笔记本电脑或平板电脑模式"),
    SM_CXBORDER,                    TEXT("SM_CXBORDER"),                    TEXT("窗口边框的宽度"),
    SM_CYBORDER,                    TEXT("SM_CYBORDER"),                    TEXT("窗口边框的高度"),
    SM_CXCURSOR,                    TEXT("SM_CXCURSOR"),                    TEXT("光标的宽度"),
    SM_CYCURSOR,                    TEXT("SM_CYCURSOR"),                    TEXT("光标的高度"),
    SM_CXDLGFRAME,                  TEXT("SM_CXDLGFRAME"),                  TEXT("同SM_CXFIXEDFRAME,有标题但不可调整大小的窗口边框的宽度"),
    SM_CYDLGFRAME,                  TEXT("SM_CYDLGFRAME"),                  TEXT("同SM_CYFIXEDFRAME,有标题但不可调整大小的窗口边框的高度"),
    SM_CXDOUBLECLK,                 TEXT("SM_CXDOUBLECLK"),                 TEXT("鼠标双击事件两次点击的X坐标不可以超过这个值"),
    SM_CYDOUBLECLK,                 TEXT("SM_CYDOUBLECLK"),                 TEXT("鼠标双击事件两次点击的Y坐标不可以超过这个值"),
    SM_CXDRAG,                      TEXT("SM_CXDRAG"),                      TEXT("拖动操作开始之前,鼠标指针可以移动的鼠标下方点的任意一侧的像素数"),
    SM_CYDRAG,                      TEXT("SM_CYDRAG"),                      TEXT("拖动操作开始之前,鼠标指针可以移动的鼠标下移点上方和下方的像素数"),
    SM_CXEDGE,                      TEXT("SM_CXEDGE"),                      TEXT("三维边框的宽度"),
    SM_CYEDGE,                      TEXT("SM_CYEDGE"),                      TEXT("三维边框的高度"),
    SM_CXFIXEDFRAME,                TEXT("SM_CXFIXEDFRAME"),                TEXT("同SM_CXDLGFRAME,有标题但不可调整大小的窗口边框的宽度"),
    SM_CYFIXEDFRAME,                TEXT("SM_CYFIXEDFRAME"),                TEXT("同SM_CYDLGFRAME,有标题但不可调整大小的窗口边框的高度"),
    SM_CXFOCUSBORDER,               TEXT("SM_CXFOCUSBORDER"),               TEXT("DrawFocusRect绘制的焦点矩形的左边缘和右边缘的宽度"),
    SM_CYFOCUSBORDER,               TEXT("SM_CYFOCUSBORDER"),               TEXT("DrawFocusRect绘制的焦点矩形的上边缘和下边缘的高度"),
    SM_CXFRAME,                     TEXT("SM_CXFRAME"),                     TEXT("同SM_CXSIZEFRAME,可调大小窗口边框的宽度"),
    SM_CYFRAME,                     TEXT("SM_CYFRAME"),                     TEXT("同SM_CYSIZEFRAME,可调大小窗口边框的高度"),
    SM_CXHSCROLL,                   TEXT("SM_CXHSCROLL"),                   TEXT("水平滚动条中箭头位图的宽度"),
    SM_CYHSCROLL,                   TEXT("SM_CYHSCROLL"),                   TEXT("水平滚动条中箭头位图的高度"),
    SM_CXVSCROLL,                   TEXT("SM_CXVSCROLL"),                   TEXT("垂直滚动条中箭头位图的宽度"),
    SM_CYVSCROLL,                   TEXT("SM_CYVSCROLL"),                   TEXT("垂直滚动条中箭头位图的高度"),
    SM_CXHTHUMB,                    TEXT("SM_CXHTHUMB"),                    TEXT("水平滚动条中滚动框(滑块)的高度"),
    SM_CYVTHUMB,                    TEXT("SM_CYVTHUMB"),                    TEXT("垂直滚动条中滚动框(滑块)的宽度"),
    SM_CXICON,                      TEXT("SM_CXICON"),                      TEXT("图标的默认宽度"),
    SM_CYICON,                      TEXT("SM_CYICON"),                      TEXT("图标的默认高度"),
    SM_CXICONSPACING,               TEXT("SM_CXICONSPACING"),               TEXT("大图标视图中项目的网格单元格宽度"),
    SM_CYICONSPACING,               TEXT("SM_CYICONSPACING"),               TEXT("大图标视图中项目的网格单元格高度"),
    SM_CXMAXIMIZED,                 TEXT("SM_CXMAXIMIZED"),                 TEXT("最大化顶层窗口的默认宽度"),
    SM_CYMAXIMIZED,                 TEXT("SM_CYMAXIMIZED"),                 TEXT("最大化顶层窗口的默认高度"),
    SM_CXMAXTRACK,                  TEXT("SM_CXMAXTRACK"),                  TEXT("具有标题和大小调整边框的窗口可以拖动的最大宽度"),
    SM_CYMAXTRACK,                  TEXT("SM_CYMAXTRACK"),                  TEXT("具有标题和大小调整边框的窗口可以拖动的最大高度"),
    SM_CXMENUCHECK,                 TEXT("SM_CXMENUCHECK"),                 TEXT("菜单项前面复选框位图的宽度"),
    SM_CYMENUCHECK,                 TEXT("SM_CYMENUCHECK"),                 TEXT("菜单项前面复选框位图的高度"),
    SM_CXMENUSIZE,                  TEXT("SM_CXMENUSIZE"),                  TEXT("菜单栏按钮的宽度"),
    SM_CYMENUSIZE,                  TEXT("SM_CYMENUSIZE"),                  TEXT("菜单栏按钮的高度"),
    SM_CXMIN,                       TEXT("SM_CXMIN"),                       TEXT("窗口的最小宽度"),
    SM_CYMIN,                       TEXT("SM_CYMIN"),                       TEXT("窗口的最小高度"),
    SM_CXMINIMIZED,                 TEXT("SM_CXMINIMIZED"),                 TEXT("最小化窗口的宽度"),
    SM_CYMINIMIZED,                 TEXT("SM_CYMINIMIZED"),                 TEXT("最小化窗口的高度"),
    SM_CXMINSPACING,                TEXT("SM_CXMINSPACING"),                TEXT("最小化窗口的网格单元宽度"),
    SM_CYMINSPACING,                TEXT("SM_CYMINSPACING"),                TEXT("最小化窗口的网格单元高度"),
    SM_CXMINTRACK,                  TEXT("SM_CXMINTRACK"),                  TEXT("窗口的最小拖动宽度,用户无法将窗口拖动到小于这些尺寸"),
    SM_CYMINTRACK,                  TEXT("SM_CYMINTRACK"),                  TEXT("窗口的最小拖动高度,用户无法将窗口拖动到小于这些尺寸"),
    SM_CXPADDEDBORDER,              TEXT("SM_CXPADDEDBORDER"),              TEXT("标题窗口的边框填充量"),
    SM_CXSIZE,                      TEXT("SM_CXSIZE"),                      TEXT("窗口标题或标题栏中按钮的宽度"),
    SM_CYSIZE,                      TEXT("SM_CYSIZE"),                      TEXT("窗口标题或标题栏中按钮的高度"),
    SM_CXSIZEFRAME,                 TEXT("SM_CXSIZEFRAME"),                 TEXT("同SM_CXFRAME,可调大小窗口边框的宽度"),
    SM_CYSIZEFRAME,                 TEXT("SM_CYSIZEFRAME"),                 TEXT("同SM_CYFRAME,可调大小窗口边框的厚度"),
    SM_CXSMICON,                    TEXT("SM_CXSMICON"),                    TEXT("小图标的建议宽度"),
    SM_CYSMICON,                    TEXT("SM_CYSMICON"),                    TEXT("小图标的建议高度"),
    SM_CXSMSIZE,                    TEXT("SM_CXSMSIZE"),                    TEXT("小标题按钮的宽度"),
    SM_CYSMSIZE,                    TEXT("SM_CYSMSIZE"),                    TEXT("小标题按钮的高度"),
    SM_CXVIRTUALSCREEN,             TEXT("SM_CXVIRTUALSCREEN"),             TEXT("虚拟屏幕的宽度"),
    SM_CYVIRTUALSCREEN,             TEXT("SM_CYVIRTUALSCREEN"),             TEXT("虚拟屏幕的高度"),
    SM_CYCAPTION,                   TEXT("SM_CYCAPTION"),                   TEXT("标题区域的高度"),
    SM_CYKANJIWINDOW,               TEXT("SM_CYKANJIWINDOW"),               TEXT("屏幕底部的日文汉字窗口的高度"),
    SM_CYMENU,                      TEXT("SM_CYMENU"),                      TEXT("单行菜单栏的高度"),
    SM_CYSMCAPTION,                 TEXT("SM_CYSMCAPTION"),                 TEXT("小标题的高度"),
    SM_DBCSENABLED,                 TEXT("SM_DBCSENABLED"),                 TEXT("User32.dll是否支持DBCS"),
    SM_DEBUG,                       TEXT("SM_DEBUG"),                       TEXT("是否安装了User.exe的调试版本"),
    SM_DIGITIZER,                   TEXT("SM_DIGITIZER"),                   TEXT("设备支持的数字转换器输入类型"),
    SM_IMMENABLED,                  TEXT("SM_IMMENABLED"),                  TEXT("是否启用了输入法管理器/输入法编辑器功能"),
    SM_MAXIMUMTOUCHES,              TEXT("SM_MAXIMUMTOUCHES"),              TEXT("系统中是否有数字化仪"),
    SM_MEDIACENTER,                 TEXT("SM_MEDIACENTER"),                 TEXT("当前操作系统是不是Windows XP Media Center"),
    SM_MENUDROPALIGNMENT,           TEXT("SM_MENUDROPALIGNMENT"),           TEXT("下拉菜单是否与相应的菜单栏项右对齐"),
    SM_MIDEASTENABLED,              TEXT("SM_MIDEASTENABLED"),              TEXT("系统是否启用希伯来语和阿拉伯语"),
    SM_MOUSEHORIZONTALWHEELPRESENT, TEXT("SM_MOUSEHORIZONTALWHEELPRESENT"), TEXT("是否安装了带有水平滚轮的鼠标"),
    SM_MOUSEPRESENT,                TEXT("SM_MOUSEPRESENT"),                TEXT("是否安装了鼠标"),
    SM_MOUSEWHEELPRESENT,           TEXT("SM_MOUSEWHEELPRESENT"),           TEXT("是否安装了带有垂直滚轮的鼠标"),
    SM_NETWORK,                     TEXT("SM_NETWORK"),                     TEXT("是否存在网络"),
    SM_PENWINDOWS,                  TEXT("SM_PENWINDOWS"),                  TEXT("是否安装了Microsoft Windows for Pen Computing扩展"),
    SM_REMOTECONTROL,               TEXT("SM_REMOTECONTROL"),               TEXT("当前终端服务器会话是否被远程控制"),
    SM_REMOTESESSION,               TEXT("SM_REMOTESESSION"),               TEXT("调用进程是否与终端服务客户机会话关联"),
    SM_SAMEDISPLAYFORMAT,           TEXT("SM_SAMEDISPLAYFORMAT"),           TEXT("所有显示器的颜色格式是否相同"),
    SM_SECURE,                      TEXT("SM_SECURE"),                      TEXT("始终返回0"),
    SM_SERVERR2,                    TEXT("SM_SERVERR2"),                    TEXT("系统是否是Windows Server 2003 R2"),
    SM_SHOWSOUNDS,                  TEXT("SM_SHOWSOUNDS"),                  TEXT("用户是否要求应用程序在其他情况下以可视方式呈现信息"),
    SM_SHUTTINGDOWN,                TEXT("SM_SHUTTINGDOWN"),                TEXT("当前会话是否正在关闭"),
    SM_SLOWMACHINE,                 TEXT("SM_SLOWMACHINE"),                 TEXT("计算机是否具有低端(慢速)处理器"),
    SM_STARTER,                     TEXT("SM_STARTER"),                     TEXT("当前操作系统版本"),
    SM_SWAPBUTTON,                  TEXT("SM_SWAPBUTTON"),                  TEXT("鼠标左键和右键的功能是否互换了"),
    SM_SYSTEMDOCKED,                TEXT("SM_SYSTEMDOCKED"),                TEXT("停靠模式的状态"),
    SM_TABLETPC,                    TEXT("SM_TABLETPC"),                    TEXT("是否启动了Tablet PC输入服务"),
    SM_XVIRTUALSCREEN,              TEXT("SM_XVIRTUALSCREEN"),              TEXT("虚拟屏幕左侧的坐标"),
    SM_YVIRTUALSCREEN,              TEXT("SM_YVIRTUALSCREEN"),              TEXT("虚拟屏幕顶部的坐标")
};
  • 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
  • SystemMetrics.cpp
#include <Windows.h>
#include <tchar.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]); //写入文本的行数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
    TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
    TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
    HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
    MSG msg;                                        // 消息循环所用的消息结构体

    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);    // 窗口背景使用标准系统颜色
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    ::RegisterClassEx(&wndclass);
    hwnd = ::CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
    ::ShowWindow(hwnd, nCmdShow);
    ::UpdateWindow(hwnd);
    while (::GetMessage(&msg, NULL, 0, 0) != 0)
    {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }
    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    TCHAR szBuf[10];
    int y;

    if (uMsg == WM_PAINT)
    {
        hdc = ::BeginPaint(hwnd, &ps);
        for (int i = 0; i < NUMLINES; i++)
        {
            y = 18 * i; //行间距
            ::TextOut(hdc, 0, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
            ::TextOut(hdc, 240, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
            ::TextOut(hdc, 760, y, szBuf,
                wsprintf(szBuf, TEXT("%d"), ::GetSystemMetrics(METRICS[i].m_nIndex)));
        }
        ::EndPaint(hwnd, &ps);
        return 0;
    }
    else if (uMsg == WM_DESTROY)
    {
        ::PostQuitMessage(0);
        return 0;
    }
    return ::DefWindowProc(hwnd, uMsg, wParam, lParam);
}
  • 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

在这里插入图片描述

程序使用wndclass.hbrBackground =(HBRUSH)(COLOR_BTNFACE+ 1);把窗口背景设置为标准系统颜色(浅灰色),所以很容易发现文本其实是有背景色的,默认是白色背景﹔字体是系统字体(System字体,标题栏、菜单、对话框默认情况下使用系统字体);对于每一行的行间距以及每一列的距离,我们大体设置了一个数值,这并不准确;客户区一共输出了95行,但是由于屏幕分辨率的原因,无法完整显示出来,很明显程序需要一个垂直滚动条。

格式化文本

文本输出是程序客户区中最常见的图形输出类型,有一些函数可以格式化和绘制文本。

格式化函数可以设置背景模式、背景颜色、对齐方式、文本颜色、字符间距等,这些都是DC的文本格式属性。背景模式不透明、背景颜色为白色、对齐方式为左对齐、文本颜色为黑色等都是默认的DC文本格式属性。

格式函数可以分为三类∶

  • 获取或设置DC的文本格式属性的函数
  • 获取字符宽度和高度的函数
  • 获取字符串宽度和高度的函数。

文本格式属性

  • 文本对齐方式

SetTextAlign函数为指定的DC设置文本对齐方式∶

UINT SetTextAlign
(
	_In_HDC hdc, //设备环境句柄
	_In_ UINT fMode  //文本对齐方式
);
  • 1
  • 2
  • 3
  • 4
  • 5

fMode参数指定文本对齐方式,可用的值及含义如下表所示。

常量常量含义
TA_TOP起始点在文本边界矩形的上边缘
TA_BOTTOM起始点在文本边界矩形的下边缘
TA_BASELINE起始点在文本的基线上
TA_LEFT起始点在文本边界矩形的左边缘
TA_RIGHT起始点在文本边界矩形的右边缘
TA_CENTER起始点在文本边界矩形的中心(水平方向)
TA_UPDATECP使用当前位置作为起始点,当前位置在每次文本输出函数调用后会更新
TA_NOUPDATECP每次文本输出函数调用以后,当前位置不会更新

默认值为TA_LEFT|TA_TOP |TA_NOUPDATECP.

调用SetTextAlign函数可以改变TextOutExtTextOut.TabbedTextOut等函数中nXStart和nYStart参数表示的含义

  • 使用TA_LEFT TA_RIGHTTA_CENTER标志会影响nXStart表示的水平坐标值。

  • 使用TA_TOP TA_BOTTOMTA_BASELINE标志会影响nYStart表示的垂直坐标值。

例如在SetTextAlign函数中指定TA_RIGHT标志,那么TextOut函数的nXStart表示字符串中最后一个字符右侧的水平坐标。如果指定TA_TOP,则nYStart表示字符串中所有字符的最高点,即所有字符都在nYStart指定的位置之下﹔如果指定TA_BOTTOM则表示字符串中所有字符都会在nYStart指定的位置之上。

如果设置了TA_UPDATECP标志,Windows会忽略TextOut函数的nXStart和nYStart参数指定的值,而是将由先前调用的MoveToEx或LineTo函数(或其他一些可以改变当前位置的函数)指定的当前位置坐标值作为起始点。

如果没有调用改变当前位置的函数,那么默认情况下当前位置的坐标为(0,0),相对于客户区左上角;设置
TA_UPDATECP标志以后,对TextOut函数的每次调用也会更新当前位置。

例如,如果设置为TA_LEFT|TA_UPDATECP,TextOut函数返回后新的当前位置就是该字符串的结束位置,下次调用TextOut函数时就会从上一个字符串的结束位置开始绘制,有时候可能需要这个特性。

如果函数执行成功,则返回值是原来的文本对齐设置 如果函数执行失败,则返回值为GDI_ERROR.

大家可以把SystemMetrics程序的最后一个TextOut改为∶

#include <Windows.h>
#include <tchar.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]); //写入文本的行数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
    TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
    TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
    HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
    MSG msg;                                        // 消息循环所用的消息结构体

    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);    // 窗口背景使用标准系统颜色
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    ::RegisterClassEx(&wndclass);
    hwnd = ::CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
    ::ShowWindow(hwnd, nCmdShow);
    ::UpdateWindow(hwnd);
    while (::GetMessage(&msg, NULL, 0, 0) != 0)
    {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }
    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    TCHAR szBuf[10];
    int y;

    if (uMsg == WM_PAINT)
    {
        hdc = ::BeginPaint(hwnd, &ps);
        for (int i = 0; i < NUMLINES; i++)
        {
            y = 18 * i; //行间距

            ::TextOut(hdc, 0, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
            ::TextOut(hdc, 240, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
           

            //设置最后一列右对齐
            ::SetTextAlign(hdc, TA_RIGHT | TA_TOP | TA_NOUPDATECP);
            ::TextOut(hdc, 760, y, szBuf, wsprintf(szBuf, TEXT("%d"), ::GetSystemMetrics(METRICS[i].m_nIndex)));
            //渲染完毕之后,恢复预设
            ::SetTextAlign(hdc, TA_LEFT | TA_TOP | TA_NOUPDATECP);
        }
        ::EndPaint(hwnd, &ps);
        return 0;
    }
    else if (uMsg == WM_DESTROY)
    {
        ::PostQuitMessage(0);
        return 0;
    }
    return ::DefWindowProc(hwnd, uMsg, wParam, lParam);
}

  • 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

将fMode参数设置为TA_RIGHT,那么TextOut的nXStart参数指定的就是字符串中最后一个字符右侧的X坐标。

可以看到数据从右开始变得对齐了。

可以通过调用GetTextAlign函数来获取指定DC的当前文本对齐设置∶

UINT GetTextAlign(_In_ HDC hdc);
  • 1

调用SetTextAlign函数的时候通常使用按位或运算符组合几个标志,调用GetTextAlign函数的时候可以使用按位”与"运算符检测返回值是否包含某标志。

  • 字符间距

可以通过调用SetTextCharacterExtra函数设置指定DC中文本输出的字符间距︰

int SetTextCharacterExtra
(
    HDC hdc, //设备环境句柄 
  
    int nCharExtra //字符间距,逻辑单位
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

大家可以在SystemMetrics程序的3个TextOut前调用SetTextCharacterExtra函数设置一下字符间距,看一下效果,比如说:

 //设置字符宽度
 ::SetTextCharacterExtra(hdc, 5);
 ::TextOut(hdc, 760, y, szBuf, wsprintf(szBuf, TEXT("%d"), ::GetSystemMetrics(METRICS[i].m_nIndex)));
 //恢复预设
 ::SetTextCharacterExtra(hdc, 0);
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述

可以通过调用GetTextCharacterExtra函数来获取指定DC的当前字符间距∶

int GetTextCharacterExtra(HDC hdc);
  • 1
  • 背景模式、背景颜色和文本颜色

可以通过调用SetTextColor函数设置绘制的文本颜色,以及在彩色打印机上绘制的文本颜色;

可以通过调用SetBkColor函数设置每个字符后显示的颜色(也就是背景颜色);

可以通过调用SetBkMode函数设置背景模式为透明或不透明。

COLORREF SetTextColor
(
HDC hdc, //设备环境句柄
COLORREF crColor //文本颜色值
);
  • 1
  • 2
  • 3
  • 4
  • 5

如果函数执行成功,则返回原来的背景颜色值;如果函数执行失败,则返回值为CLR_INVALID.

int SetBkMode(
	HDC hdc,	//设备环境句柄
	int iBkMode //背景模式
);
  • 1
  • 2
  • 3
  • 4

iBkMode参数指定背景模式,可用的值只有两个∶指定为OPAQUE表示不透明背景,指定为TRANSPARENT表示透明背景。

如果函数执行成功,则返回原来的的背景模式﹔如果函数执行失败,则返回值为0。

COLORREF用于指定RGB颜色值,在windef.h头文件中定义如下∶

typedef DWORD COLORREF;
typedef DWORD *LPCOLORREF;
  • 1
  • 2

COLORREF值的十六进制为"Ox00BBGGRR"的形式,低位字节包含红色值,倒数第2字节包含绿色值,倒数第3字节包含蓝色值,高位字节必须为0,单字节的最大值为255.

要创建COLORREF颜色值,可以使用RGB宏分别指定红色、绿色、蓝色的值;

要提取COLORREF颜色值中的的红色、绿色和蓝色值,可以分别使用GetRValue GetGValue和GetBValue宏。

这些宏在wingdi.h头文件中定义如下∶

#define RGB(r,g,b)
((COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g))<<8))|(DWORD)(BYTE)(b))<<16)))
#define GetRValue(rgb)(LOBYTE(rgb))
#define GetGValue(rgb)(LOBYTE((WORD)(rgb)) >>8))
#define GetBValue(rgb)(LOBYTE((rgb)>>16))
  • 1
  • 2
  • 3
  • 4
  • 5

现在,我们在SystemMetrics程序的TextOut函数调用前面加上以下语句︰

#include <Windows.h>
#include <tchar.h>
#include<strsafe.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]); //写入文本的行数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
    TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
    TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
    HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
    MSG msg;                                        // 消息循环所用的消息结构体

    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);    // 窗口背景使用标准系统颜色
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    ::RegisterClassEx(&wndclass);
    hwnd = ::CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
    ::ShowWindow(hwnd, nCmdShow);
    ::UpdateWindow(hwnd);
    while (::GetMessage(&msg, NULL, 0, 0) != 0)
    {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }
    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    TCHAR szBuf[10];
    int y;

    if (uMsg == WM_PAINT)
    {
        hdc = ::BeginPaint(hwnd, &ps);
        for (int i = 0; i < NUMLINES; i++)
        {
            y = 18 * i; 

   

            //设置背景模式是透明的,文本颜色为蓝色
            int preSetBkMode = ::SetBkMode(hdc, TRANSPARENT);
            COLORREF preSetTextColor =  SetTextColor(hdc, RGB(0, 0, 255));
 

            ::TextOut(hdc, 0, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));

            //恢复预设
            SetBkMode(hdc, preSetBkMode);
            SetTextColor(hdc, preSetTextColor);


            ::TextOut(hdc, 240, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
            ::TextOut(hdc, 760, y, szBuf, wsprintf(szBuf, TEXT("%d"), ::GetSystemMetrics(METRICS[i].m_nIndex)));
            ::SetTextCharacterExtra(hdc, 0);

        }
        ::EndPaint(hwnd, &ps);
        return 0;
    }
    else if (uMsg == WM_DESTROY)
    {
        ::PostQuitMessage(0);
        return 0;
    }
    return ::DefWindowProc(hwnd, uMsg, wParam, lParam);
}

  • 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

在这里插入图片描述

可以看到背景模式是透明的,文本颜色为蓝色。

显示DC的默认文本颜色TextColor为黑色,默认背景颜色BkColor为白色,默认背景模式BkMode为不透明。

程序可以通过调用GetTextColor函数获取DC的当前文本颜色。

可以通过调用GetBkColor函数获取DC的当前背景颜色。

可以通过调用GetBkMode函数获取DC的当前背景模式。

获取字符串的宽度和高度

GetCharWidth32函数可以获取指定DC当前字体中指定范围内的连续字符的宽度︰

BOOL GetCharWidth32
(
_In_ HDC hdc,//设备环境句柄
_In_ UINT iFirstChar,//连续字符中的第一个字符
_In_ UINT iLastChar,//连续字符中的最后一个字符,不得位于指定的第一个字符之前
_out_ LPINT lpBuffer //接收每个字符宽度的INT数组,字符宽度是逻辑单位
); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

可以把iFirstCha和iLastChar参数指定为相同的值,只获取一个字符的宽度。

GetTextExtentPoint32函数用于获取指定DC中一个字符串的宽度和高度值︰

BOOL GetTextExtentPoint32
(
_In_ HDC hdc,//设备环境句柄
_In_ LPCTSTR lpString,//字符串指针,不要求以零结尾,因为参数c可以指定字符串长度
_ln_ intc,//字符串长度,可以使用_tcslen
_Out_LPSIZE lpSize //在这个SIZE结构中返回字符串的宽度和高度,逻辑单位
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

lpSize是一个指向SIZE结构的指针,在这个SIZE结构中返回字符串的宽度和高度。SIZE结构在windef.h头文件中定义如下∶

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

前面说过∶“WM_CREATE消息是窗口过程较早收到的消息之一,程序通常会在这里做一些初始化的工作”。对于SystemMetrics程序,我们可以在WM_CREATE消息中获取字符串高度,用于在TextOut函数中指定y坐标值︰

#include <Windows.h>
#include <tchar.h>
#include<strsafe.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]); //写入文本的行数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
	TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
	TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
	HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
	MSG msg;                                        // 消息循环所用的消息结构体

	wndclass.cbSize = sizeof(WNDCLASSEX);
	wndclass.style = CS_HREDRAW | CS_VREDRAW;
	wndclass.lpfnWndProc = WindowProc;
	wndclass.cbClsExtra = 0;
	wndclass.cbWndExtra = 0;
	wndclass.hInstance = hInstance;
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);    // 窗口背景使用标准系统颜色
	wndclass.lpszMenuName = NULL;
	wndclass.lpszClassName = szClassName;
	wndclass.hIconSm = NULL;
	::RegisterClassEx(&wndclass);
	hwnd = ::CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
	::ShowWindow(hwnd, nCmdShow);
	::UpdateWindow(hwnd);
	while (::GetMessage(&msg, NULL, 0, 0) != 0)
	{
		::TranslateMessage(&msg);
		::DispatchMessage(&msg);
	}
	return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	HDC hdc;
	PAINTSTRUCT ps;
	static SIZE size = { 0 };
	TCHAR szBuf[10];
	int y;


	if (uMsg == WM_CREATE)
	{
		//查字符串高度和宽度
		hdc = GetDC(hwnd);
		GetTextExtentPoint32(hdc,METRICS[0].m_pLabel,_tcslen(METRICS[0].m_pLabel),&size);
		ReleaseDC(hwnd,hdc);
		return 0;
	}
	else if (uMsg == WM_PAINT)
	{
		hdc = ::BeginPaint(hwnd, &ps);



		for (int i = 0; i < NUMLINES; i++)
		{
			// y = 18 * i; //这里计算行间距就不用写死了

			y = size.cy * i; //计算行间距

			::TextOut(hdc, 0, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
			::TextOut(hdc, 240, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
			::TextOut(hdc, 760, y, szBuf, wsprintf(szBuf, TEXT("%d"), ::GetSystemMetrics(METRICS[i].m_nIndex)));
			::SetTextCharacterExtra(hdc, 0);

		}
		::EndPaint(hwnd, &ps);
		return 0;
	}
	else if (uMsg == WM_DESTROY)
	{
		::PostQuitMessage(0);
		return 0;
	}
	return ::DefWindowProc(hwnd, uMsg, wParam, lParam);
}

  • 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

GetTextExtentPoint32函数适用于字符串中不包含制表符的情况,如果字符串中包含制表符,则应该调用GetTabbedTextExtent函数︰

DWORD GetTabbedTextExtent(
_In_	HDC	hDC,//设备环境句柄
_In_	LPCTSTR lpString,//字符串指针,不要求以需结尾,因为nCount指定字符串长度
_In_ int	nCount, //字符串长度,可以使用_tcslen
_In_ int nTabPositions,//lpnTabStopPositions数组中元素的个数
_In_opt_ const LPINT lpnTabStopPositions //指向包含制表符位置的数组
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 如果将nTabPositions参数设置为0,并将lpnTabStopPositions参数设置为NULL,制表符会自动按平均字符宽度的8倍来扩展

  • 如果将nTabPositions参数设置为1,则所有制表符按lpnTabStopPositions参数指向的数组中的第一个数组元素指定的距离来分隔。

  • 如果函数执行成功,则返回值是字符串的宽度和高度(逻辑单位),高度值在高位字中,宽度值在低位字中;如果函数执行失败,则返回值为0.

HIWORD宏可以得到一个32位数的高16位;

LOWORD宏可以得到一个32位数的低16位;

HIBYTE宏可以得到一个16位数的高字节;

LOBYTE宏可以得到一个16位数的低字节。

类似的还有,MAKELONG宏可以将两个16位的数合成为一个32位的LONG型;MAKEWORD宏可以将两个8位的数合成为一个16位的WORD型,等等。这些宏在minwindef.h头文件中定义如下︰

#define MAKEWORD(a, b)      ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
#define MAKELONG(a, b)      ((LONG)(((WORD)(((DWORD_PTR)(a)) & 0xffff)) | ((DWORD)((WORD)(((DWORD_PTR)(b)) & 0xffff))) << 16))
#define LOWORD(l)           ((WORD)(((DWORD_PTR)(l)) & 0xffff))
#define HIWORD(l)           ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff))
#define LOBYTE(w)           ((BYTE)(((DWORD_PTR)(w)) & 0xff))
#define HIBYTE(w)           ((BYTE)((((DWORD_PTR)(w)) >> 8) & 0xff))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

绘制文本函数

在选择了适当的字体和所需的文本格式选项后,可以通过调用相关函数来绘制字符或字符串,常用的文本绘制函数有DrawText.
DrawTextEx-TextOut- ExtTextOut- Poly TextOut和TabbedTextOut等。当调用其中一个函数时,操作系统将此调用传递给GDI图形引擎,而GDI图形引擎又将调用传递给相应的设备驱动程序。其中
ExtTextOut函数执行速度最快,该调用将快速转换为设备的
ExtTextOut调用。但是,有时程序可能更适合调用其他函数,例如,要在指定的矩形区域范围内绘制文本,可以调用DrawText函数,要创建具有对齐列的多列文本,可以调用TabbedTextOut函数。

DrawText和DrawTextEx函数在指定的矩形内绘制文本:

int DrawText(
_In_ HDC hdc, //设备环境句柄
_Inout_ LPCTSTR lpchText, //字符串指针
_In_ int cchText, //字符串长度,以字符为单位
_In_out_LPRECT lpRect, //所绘制的文本限定在这个矩形范围内
_In_ UINT uFormat //绘制格式选项
);

int DrawTextEx(
_In_ HDC hdc,//设备环境句柄
_Inout_ LPTSTR lpchText,//字符串指针
_ln_ int cchText, //字符串长度,以字符为单位
_In_out_ LPRECT lpRect,//所绘制的文本限定在这个矩形范围内
_In_ UINT uFormat, //绘制格式选项
_In_opt_ LPDRAWTEXTPARAMS lpDTParams //指定扩展格式选项的DRAWTEXTPARAMS结构,可以为NULL
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 参数cchText指定字符串的长度。如果lpchText参数指定的字符串是以需结尾的,那么cchText参数可以设置为-1,函数会自动计算字符个数;否则需要指定字符个数。
  • 参数uFormat指定格式化文本的方法,常用的值及含义如下表所示。
宏常量含义
DT_TOP将文本对齐到矩形的顶部
DT_BUTTON将文本对齐到矩形的底部,该标志仅与DT_SINGLELINE单行文本一起使用
DT_VCENTER文本在矩形内垂直居中,该标志仅与DT_SINGLELINE单行文本一起使用
DT_LEFT文本在矩形内左对齐
DT_RIGHT文本在矩形内右对齐
DT_CENTER文本在矩形内水平居中
DT_SINGLELINE在单行上显示文本,回车和换行符也不能打断行
DT_WORDBREAK如果一个单词超过矩形的边界,则自动断开到下一行
DT_EXPANDTABS展开制表符\t,每个制表符的默认字符数是8

DrawTextEx函数的lpDTParams参数是用于指定扩展格式选项的DRAWTEXTPARAMS结构,可为NULL:

typedef struct tagDRAWTEXTPARAMS
{
	UINT	cbSize; //该结构的大小
	int iTabLength; //每个制表符的大小,单位等于平均字符宽度
	int	iLeftMargin; //左边距,逻辑单位
	int iRightMargin; //右边距,逻辑单位
	UINT	uiLengthDrawn; //返回函数处理的字符个数,包括空格字符,不包括字符串结束标志
}
DRAWTEXTPARAMS,FAR*LPDRAWTEXTPARAMS;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果函数执行成功,则返回值是以逻辑单位表示的文本高度,如果指定了DT_VCENTER 或DT_BOTTOM,则返回值是从lpRect->top到所绘制文本底部的偏移量;如果函数执行失败,则返回值为0。

下面使用DrawTextEx函数输出一个字符串看一下效果∶

	
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	TCHAR szText[] =
		_T("For displayed text, if the end of a string does not fit in the rectangle,"
			"it is truncated and ellipses are added.lf a word that is not at the end of"
		"the string goes beyond the limits of the rectangle, it is truncated without ellipses.");

	DRAWTEXTPARAMS dtp = {sizeof(DRAWTEXTPARAMS)};
	dtp.iLeftMargin = 10;
	dtp.iRightMargin = 10;
	RECT rect;
	
	...
else if (uMsg == WM_PAINT)
	{
	
		hdc = ::BeginPaint(hwnd, &ps);
		SetBkMode(hdc,TRANSPARENT);
		SetTextColor(hdc,RGB(0,0,255));
		GetClientRect(hwnd,&rect); //查客户区矩形
		DrawTextEx(hdc,szText,-1,&rect,DT_WORDBREAK,&dtp);
		::EndPaint(hwnd, &ps);
	}
}
  • 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

GetClientRect函数用于获取客户区的矩形坐标∶

BOOL WINAPl GetClientRect(
_In_ HWND hWnd, //窗口句柄
_out_ LPRECT lpRect //在这个RECT中返回客户区的坐标,以像素为单位
);
  • 1
  • 2
  • 3
  • 4

参数lpRect指向的RECT结构返回客户区的左上角和右下角坐标。因为客户区坐标是相对于窗口客户区左上角的,所以获取到的左上角的坐标是(0,0),即lpRect-right等于客户区宽度,lpRect-bottom等于客户区高度。

程序执行效果如下图所示。

TabbedTextOut函数在指定位置绘制字符串,并将制表符扩展到制表符位置数组中指定的位置︰

LONG TabbedTextOut(
	_In_	HDChDC, //设备坏境句柄
	_In_ int x //字符串起点的X坐标,逻辑单位
	_In_ int Y, //字符串起点的Y坐标,逻辑单位
	_In_ LPCTSTR lpString,//字符串指针,不要求以零结尾,参数nCount可以指定字符串长度
	_In_ int nCount,//字符串长度,可以使用_tcslen
	_In_ int nTabPositions,  //lpnTabStopPositions数组中数组元素的个数
    _In_ const LPINT lpnTabStopPositions, //指向包含制表符位置的数组,逻辑单位
	_In_ int nTabOrigin); //制表符开始位置的X坐标,逻辑单位,制表符的位置等于
	// nTabOrigin + lpnTabStopPositions[x]
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 如果将nTabPositions参数设置为0,并将lpnTabStopPositions参数设置为NULL,制表符将会按平均字符宽度的8倍来扩展

  • 如果将nTabPositions参数设置为1,则所有制表符按lpnTabStopPositions指向的数组中的第一个数组元素指定的距离来分隔。

最终, 如果函数执行成功,则返回值是字符串的宽度和高度(逻辑单位),高度值在高位字中,宽度值在低位字中﹔如果函数执行失败,则返回值为0。看一个示例︰

else if (uMsg == WM_PAINT)
{

	HDC hdc;
	PAINTSTRUCT ps;

	TCHAR szText[] = TEXT("姓名\t工作地点\t年龄");
	TCHAR szText2[] = TEXT("小王\t山东省济南市\t18");
	TCHAR szText3[] = TEXT("弗拉基米尔·弗拉基米罗维奇·科夫\t俄罗斯莫斯科\t68");
	INT nTabStopPosition[] = { 260,370 };
	LONG iRet = 0;

	hdc = ::BeginPaint(hwnd, &ps);
	SetBkMode(hdc,TRANSPARENT);
	SetTextColor(hdc,RGB(0,0,255));
	iRet = TabbedTextOut(hdc,0,0, szText,_tcslen(szText),2,nTabStopPosition,0);
	iRet = TabbedTextOut(hdc,0,HIWORD(iRet), szText2, _tcslen(szText2), 2, nTabStopPosition, 0);
	iRet = TabbedTextOut(hdc,0, HIWORD(iRet)*2, szText3,_tcslen(szText3),2,nTabStopPosition,0);
	

	::EndPaint(hwnd, &ps);
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

程序执行效果如下图所示。

在这里插入图片描述

ExtTextOut函数和TextOut一样可以输出文本,另外,该函数可以指定一个矩形用于裁剪或作为背景。

BOOL ExtTextOut(
_In_ HDC hdc, //设备环境句柄
_In_ int x, //字符串的开始位置X坐标,相对于客户区左上角,逻辑单位
_In_ int y, //字符串的开始位置Y坐标,相对于客户区左上角,逻辑单位
_In_ UINT fuOptions//指定如何使用lprc参数指定的矩形,可以设置为0
_In_ const RECT *prc, //指向可选RECT结构的指针,用于裁剪或作为背景,可为NULL
_In_ LPCTSTR lpString, //要绘制的字符串,因为有cbCount参数指定长度,所以不要求以零结尾
_In_ UINT cbCount,  //lpString指向的字符串长度,可以使用_tcslen,不得超过8192
_In_ const INT *lpDx
); //指向可选整型数组的指针,该数组指定相邻字符之间的间距
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 参数fuOptions指定如何使用lprc参数定义的矩形,常用的值如下表所示。
宏常量含义
ETO_CLIPPED文本将被裁剪到矩形范围内,就是说矩形范围以外的文本不会显示
ETO_OPAQUE使用当前背景色来填充矩形
  • 参数lpDx是指向可选数组的指针,该数组指定相邻字符之间的间距。如果设置为NULL,表示使用默认字符间距。

关于其他绘制文本函数的使用方法请自行参见MSDN

加入标准滚动条

在窗口中加入一个标准滚动条比较简单,只需要在CreateWindowEx函数的dwStyle参数中指定WS_HSCROLL / WS_VSCROLL样式即可。WS_HSCROLL表示加入一个水平滚动条,WS_VSCROLL表示加入一个垂直滚动条。之所以叫标准滚动条,是因为与之对应的还有一个滚动条控件。滚动条控件是子窗口,可以出现在父窗口客户区的任何位置,后面会讲滚动条控件。

每个滚动条都有相应的“范围"和"位置”。滚动条的范围是一对整数,分别代表滚动条的最小值和最大值。位置是指滑块在范围中所处的值。当滑块在滚动条的最顶端(或最左)时,滑块的位置是范围的最小值﹔当滑块在滚动条的最底部((或最右)时,滑块的位置是范围的最大值。

标准滚动条的默认范围是0~100,滚动条控件的默认范围为空(最小值和最大值均为0)。通过调用SetScrollRange函数,可以把范围改成对程序更有意义的值︰

BOOL SetScrollRange(
_In_ HWND hWnd, //滚动条所在窗口的窗口句柄_In_int nBar,l指定要设置的滚动条
_In_ int nMinPos,//最小滚动位置
_In_ int nMaxPos, //最大滚动位置
_In_ BOOL bRedraw //是否应该重新绘制滚动条以反映更改
); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

参数nBar指定要设置的滚动条,该参数如下表所示。

宏常量含义
SR_HORZ设置标准水平滚动条的范围
SR_VERT设置标准垂直滚动条的范围
SR_CTL设置滚动条控件的范围,这种情况下hWnd参数必须设置为滚动条控件的句柄

nMinPosnMaxPos参数指定的值之间的差异不得大于MAXLONG(OX7FFFFFFF).

SetScrollPos函数用于设置滑块在滚动条中的位置∶

int SetScrollPos(
_In_ HWND hWnd, //滚动条所属窗口的窗口句柄
_In_ int nBar, //指定要设置的滚动条,含义同SetScrollRange函数的nBar参数
_In_ int nPos, //滑块的新位置
_In_ BOOL bRedraw //是否重新绘制滚动条以反映新的滑块位置
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果函数执行成功,则返回值是滑块的前一个位置;如果函数执行失败,则返回值为0。

WM_SIZE消息

在WinMain调用ShowWindow函数时、在窗口大小更改后、在窗口最小化到任务栏或从任务栏恢复时, Windows都会发送WM_SIZE消息到窗口过程。对一个消息的处理,依赖于其wParam和lParam参数的含义,每个消息的wParam和lParam参数的含义通常都不相同,这合初学者很困惑!我的建议是,不必刻意记忆不同消息的wParam和lParam参数的含义,用的时候打开帮助文档或查询MSDN即可。常用的消息并不是很多,用得多了,自然也就熟悉了。学习Windows程序设计,只需要有人把读者领进门,面对Windows这座巨大迷宫,以后读者还是要依靠MSDN。

  • WM_SIZE消息的wParam参数表示请求的大小调整类型,常用的值如下表所示。
宏常量含义
SIZE_RESTORED窗口的大小已发生变化,包括从最小化或最大化恢复到原来的状态
SIZE_MINIMIZED窗口已最大化
SIZE MAXIMIZED窗口已最大化

WM_SIZE消息的IParam参数表示窗口客户区的新尺寸,lParam的低位字指定客户区的新宽度,IParam的高位字指定客户区的新高度。通常这样使用IParam参数∶

cxClient = LoWORD(IParam);//客户区的新宽度
cyClient = HIWORD(IParam);//客户区的新高度
  • 1
  • 2

随着窗口大小的改变,子窗口或子窗口控件通常也需要随之改变位置和大小,以适应新的客户区大小。例如,记事本程序客户区中用于编辑文本的部件就是一个编辑控件,如果窗口大小改变,程序就需要响应WM_SIZE消息,重新计算客户区大小,对编辑控件的大小作出改变。
窗口过程处理完WM_SIZE消息以后,应返回0。
如果不是在WM_SIZE消息中,可以通过调用GetClientRect函数获取客户区尺寸。

WM_HSCROLL消息

当窗口的标准水平滚动条中发生滚动事件时,窗口过程会收到
WM_HSCROLL消息﹔当窗口的标准垂直滚动条中发生滚动事件时,窗口过程会收到WM_VSCROLL消息。

WM_HSCROLL消息的wParam参数表示滑块的当前位置和用户的滚动请求。wParam的低位字表示用户的滚动请求,值如下表所示。

宏常量含义
SB_LINELEFT向左滚动一个单位
SB_LINERIGHT向右滚动一个单位
SB_PAGELEFT向左滚动一页(一个客户区宽度)
SB_PAGERIGHT向右滚动一页(一个客户区宽度)
SB_THUMBPOSITION用户拖动滑块并已释放鼠标,wParam的高位字指示拖动操作结束时滑块的新位置
SB_THUMBTRACK用户正在拖动滑块,该消息会不断发送,直到用户释放鼠标,wParam的高位字实时指示滑块被拖动到的新位置
SB_LEFT滚动到最左侧,这个暂时用不到
SB_RIGHT滚动到最右侧,这个暂时用不到
SB_ENDSCROLL滚动已结束,通常不使用

如果wParam参数的低位字是SB_THUMBPOSITION或SB_THUMBTRACK,则wParam参数的高位字表示滑块的当前位置,在其他情况下无意义。

如果消息是由滚动条控件发送的,则lParam参数是滚动条控件的句柄如果消息是由标准滚动条发送的,则lParam参数为NULL.
窗口过程处理完WM_HSCROLL消息以后,应返回0.

WM_VSCROLL消息

WM_VSCROLL消息的wParam参数表示滑块的当前位置和用户的滚动请求。wParam的低位字表示用户的滚动请求,值如下表所示。

宏常量含义
SB_LINEDOWN向下滚动一个单位
SB_LINEUP向上滚动一个单位
SB_PAGEDOWN向下滚动一页(一个客户区高度)
SB_PAGEUP向上滚动一页(一个客户区高度)
SB_THUMBPOSITION用户拖动滑块并已释放鼠标,wParam的高位字指示拖动操作结束时滑块的新位置
SB_THUMBTRACK用户正在拖动滑块,该消息会不断发送,直到用户释放鼠标,wParam的高位字实时指示滑块被拖动到的新位置
SB_TOP滚动到最上部,这个暂时用不到
SB_BOTTOM滚动到最底部,这个暂时用不到
SB_ENDSCROLLSB_ENDSCROLL滚动已结束,通常不使用

如果wParam参数的低位字是SB_THUMBPOSITION或SB_THUMBTRACK,则wParam参数的高位字表示滑块的当前位置,在其他情况下无意义。

如果消息是由滚动条控件发送的,则IParam参数是滚动条控件的句柄;如果消息是由标准滚动条发送的,则IParam参数为NULL. 窗口过程处理完WM_VSCROLL消息以后,应返回0。

用户单击或拖动滚动条的不同位置时的滚动请求如下图所示。

在这里插入图片描述

当用户按住水平或垂直滑块进行滑动时,程序通常处理的是
SB_THUMBTRACK请求,而不是SB_THUMBPOSITION请求,以便用户拖动过程中,客户区的内容可以实时发生改变。

是时候给下面的SystemMetrics2程序添加标准滚动条了,先添加一个垂直滚动条∶

#include <Windows.h>
#include <tchar.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]);

// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
    TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
    TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
    HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
    MSG msg;                                        // 消息循环所用的消息结构体
    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    RegisterClassEx(&wndclass);

    hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0) != 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    HFONT hFont, hFontOld;
    static BOOL IsCalcStr = TRUE;           // 只在第一次WM_PAINT消息中计算s_iCol1、s_iCol2、s_iHeight
    static int s_iCol1, s_iCol2, s_iHeight; // 第一列、第二列字符串的最大宽度,字符串高度
    static int s_cxClient, s_cyClient;      // 客户区宽度、高度
    static int s_iVscrollPos;               // 垂直滚动条当前位置
    TCHAR szBuf[10];
    int y;

    if (WM_CREATE == uMsg)
    {
        // 设置垂直滚动条的范围和初始位置
        SetScrollRange(hwnd, SB_VERT, 0, NUMLINES - 1, FALSE);
        SetScrollPos(hwnd, SB_VERT, s_iVscrollPos, TRUE);
        return 0;
    }
    else if (WM_SIZE == uMsg)
    {
        // 计算客户区宽度、高度,滚动条滚动一页的时候需要使用
        s_cxClient = LOWORD(lParam);
        s_cyClient = HIWORD(lParam);
    }
    else if (WM_VSCROLL == uMsg)
    {
        switch (LOWORD(wParam))
        {
        case SB_LINEUP:
            s_iVscrollPos -= 1;
            break;
        case SB_LINEDOWN:
            s_iVscrollPos += 1;
            break;
        case SB_PAGEUP:
            s_iVscrollPos -= s_cyClient / s_iHeight;
            break;
        case SB_PAGEDOWN:
            s_iVscrollPos += s_cyClient / s_iHeight;
            break;
        case SB_THUMBTRACK:
            s_iVscrollPos = HIWORD(wParam);
            break;
        }
        s_iVscrollPos = min(s_iVscrollPos, NUMLINES - 1);
        s_iVscrollPos = max(0, s_iVscrollPos);
        //查一下,如果当前位置不是开始,则更新滑块位置,并重绘客户区
        if (s_iVscrollPos != GetScrollPos(hwnd, SB_VERT))
        {
             SetScrollPos(hwnd, SB_VERT, s_iVscrollPos, TRUE);

             //产生一个无效区域,创建产生出来WM_PAINT消息,
             InvalidateRect(hwnd, NULL, TRUE);
             //直接把这个消息给到窗口过程
             UpdateWindow(hwnd);
        }
        return 0;
    }
    else if (WM_PAINT == uMsg)
    {
        hdc = BeginPaint(hwnd, &ps);
        SetBkMode(hdc, TRANSPARENT);
        hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
        hFontOld = (HFONT)SelectObject(hdc, hFont);

        if (IsCalcStr)
        {
            SIZE size = { 0 };
            //查每一行,对应列的最大cx,以便算出来文本最佳宽高
            for (int i = 0; i < NUMLINES; i++)
            {
                GetTextExtentPoint32(hdc, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel), &size);
                if (size.cx > s_iCol1)
                {
                    s_iCol1 = size.cx;
                }
                GetTextExtentPoint32(hdc, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc), &size);
                if (size.cx > s_iCol2)
                {
                    s_iCol2 = size.cx;
                }
            }
            s_iHeight = size.cy + 2;    // 留一点行间距
            IsCalcStr = FALSE;
        }

        //针对每一次拖动之后,重新绘制整个客户区文本。
        for (int i = 0; i < NUMLINES; i++)
        {
            y = s_iHeight * (i - s_iVscrollPos);
            TextOut(hdc, 0, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
            TextOut(hdc, s_iCol1, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
            TextOut(hdc, s_iCol1 + s_iCol2, y, szBuf, wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)));
        }

        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        EndPaint(hwnd, &ps);
        return 0;
    }
    else if (WM_DESTROY == uMsg)
    {
        PostQuitMessage(0);
        return 0;
    }

   return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
  • 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
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154

在这里插入图片描述

程序中有几个变量被定义为静态变量。静态变量保存在全局数据区,而不是保存在堆栈中,不会因为WindowProc函数的退出而被销毁,下一次消息处理的时候还可以继续使用上次保存的值。也可以定义为全局变量,但是原则上还是少使用全局变量。

程序需要初始化滚动条的范围和位置﹔处理滚动请求并更新滑块的位置,否则滑块会在用户松开鼠标以后回到原来的位置;并根据滚动条的变化更新客户区的内容。

在WM_CREATE消息中我们设置垂直滚动条的范围和初始位置,把垂直滚动条的范围设置为O~NUMLINES -1,也就是总行数。然后把滑块初始位置设置为0,s_iVscrollPos是静态变量,系统会自动设置未初始化的静态变量初始值为0。如果滚动条的位置是0,则第一行文字显示在客户区的顶部﹔如果位置是其他值,则其他行会显示在顶部﹔如果位置是NUMLINES -1,则最后一行显示在客户区的顶部。

看一下对WM PAINT消息的处理。如果是第一次执行WM PAINT,则
需要分别计算出第一列和第二列中最宽的字符串,这个宽度用于TextOut函数的X坐标,对于每一行Y坐标的计算如下:

y = s_iHeight * (i - s_iVscrollPos)
  • 1

令i = 0(也就是输出第一行) 时,假设垂直滚动条向下滚动了2行也就是s_iVscrollPos的值等于2。因为客户区左上角的坐标是(0,0),所以第一行和第二行实际上是跑到了客户区上部,只不过在客户区以外的内容是不可见的。

再看一下对WM VSCROLL消息的处理,我们分别处理了向上滚动一行SB_LINEUP、向下滚动一行SB_LINEDOWN、向上滚动一页SB_PAGEUP、向下滚动一页SB_PAGEDOWN和按住滑块拖动
SB_THUMBTRACK的情况。然后需要对滑块新位置s_iVscrollPos进行合理范围的判断,否则s iVscrollPos值会出现小于0或大于NUMLINES -1的情况。虽然滑块新位置s_iVscrollPos值已经计算好了,但是滑块真正的位置还没有变化,我们应该判断一下s_iVscrollPos和滑块的当前位置这两个值是否相等,如果不相等,再调用SetScrollPos函数设置滑块位置。

去掉对InvalidateRect函数的调用,看一下会是什么现象,是不是存在滚动条可以正常工作,但是客户区内容没有随之滚动的情况?如果最小化程序窗口,再将窗口恢复到原先的尺寸,导致客户区无效重绘,那么是不是客户区内容就更新过来了呢?

InvalidateRect函数向指定窗口的更新区域添加一个无效矩形

BOOL InvalidateRect(
_In_ HWND hWnd, // 窗口句柄
_In_ const RECT pRect,// 无效矩形,如果设置为NULL,表示整个客户区是无效矩形
_In_ BOOL bErase // 是否擦除更新区域的背景
); 
  • 1
  • 2
  • 3
  • 4
  • 5

InvalidateRect函数会导致客户区出现一个无效区域。如果客户区存在无效区域,Windows会发送WM_PAINT消息到窗口过程。

这里不可以使用UpdateWindow,前面说过:当窗口客户区的部分或全部变为"无效"且必须"更新"时,窗口过程会收到WM_PAINT消息,这也就意味着客户区必须被重绘。调用UpdateWindow(),Windows会检查客户区是否存在无效区域(或更新区域),如果存在,才会向窗口过程发送WM_PAINT消息。

调用UpdateWindow函数,Windows会检查客户区是否存在无效区域。如果存在,就把WM PAINT消息直接发送到指定窗口的窗口过程,绕过应用程序的消息队列,即UpdateWindow函数导致窗口过程WindowProc立即执行case WM PAINT逻辑,所以可以在InvalidateRect函数调用以后紧接着调用UpdateWindow函数立即刷新客户区。

与InvalidateRect函数对应的,还有一个ValidateRect函数,该函数可以从指定窗口的更新区域中删除一个矩形区域:

BOOL ValidateRect(
_In_ HWND hWnd, // 窗口句柄
_In_ const RECT*pRect // 使之有效的矩形,如果设置为NULL,表示整个客户区变为有效
); 
  • 1
  • 2
  • 3
  • 4

前面讲解获取显示DC句柄的时候,曾经提及,如果可能,我们最好是在WM_PAINT消息中处理绘制工作。SystemMetrics2程序就是这
样,绕了个弯,没有在WM VSCROLL消息中进行重绘,而是通过调
用InvalidateRect函数生成一个无效区域进而生成WM_PAINT消息 在WM_PAINT消息中统一进行重绘。这不是舍近求远,而是一举两得。在学习Windows以前,我以为滚动条是自动的,但实际上需要我们自己处理各种滚动请求并作出更新、重绘,可以理解,Windows程序设计本来就是比较底层的东西。

SetScrolllnfo和GetScrolllnfo函数

SystemMetrics2程序工作正常,但是滑块的大小不能反映一页内容占据总内容的比例。假设总内容总共是3页,那么滑块大小应该是滚动条长度的1/3才可以。实际上,SetScrollRange和SetScrollPos函数是Windows向后兼容Win16的产物,微软建议我们使用新函数SetScrolllnfo。之所以介绍一些老函数,一方面是因为这些函数简单易用,很多资深的程序员还在使用;另一方面,我的目标是既能做开发,又能做逆向,所以有些过时的东西我们还需要去了解。

int SetScrolllnfo(
_In_ HWND hwnd, // 滚动条所属窗口的句柄,如果fnBar 指定为SB_CTL、则是滚动条控件句柄
_In_ int fnBar, // 指定要设置的滚动条,含义同SetScrollRange函数的nBar参数
_In_ LPCSCROLLINFO lpsi, // 在这个SCROLLINFO结构中指定滚动条的参数
_In_ BOOL fRedraw // 是否重新绘制滚动条以反映对滚动条的更改
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

除了lpsi参数,其他的我们都已了解。lpsi参数是一个指向SCROLLINFO结构的指针,在这个结构中指定滚动条的参数。在WinUserh头文件中SCROLLINFO结构的定义如下:

typedef struct tagSCROLLINFO
{
	UINT cbSize; // 该结构的大小,sizeof( SCROLLINFO)
	UINT fMask; //要设置或获取哪些滚动条参数
	int nMin; //最小滚动位置
	int nMax; //最大滚动位置
	UINT nPage; // 页面大小(客户区高度或宽度),滚动条使用这个字段确定滑块的适当大小
	int nPos;	// 滑块的位置
	int nTrackPos; // 用户正在拖动滑块时的即时位置,可以在处理SB_THUMBTRACK请求时使用该字段
} SCROLLINFO,FAR *LPSCROLLINFO:
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

fMask字段指定要设置 (SetScrolllnfo) 或获取 (GetScrolllnfo) 哪些滚动条参数,值如下表所示。

常量宏含义
SIF_PAGEnPage 成员包含比例滚动条的页面大小。
SIF_POSnPos 成员包含滚动框位置,当用户拖动滚动框时不会更新该位置。
SIF_RANGEnMinnMax 成员包含滚动范围的最小值和最大值。
SIF_ALLSIF_PAGE、SIF_POS、SIF_RANGE和SIF_TRACKPOS的组合。
SIF_DISABLENOSCROLL此值仅在设置滚动条的参数时使用。 如果滚动条的新参数使滚动条变得不必要,请禁用滚动条,而不是将其删除。
SIF_TRACKPOSnTrackPos成员包含用户拖动滚动框时的当前位置。

调用SetScrolllnfo函数设置滚动条参数的时候,把fMask字段设置为需要设置的标志,并在相应的字段中指定新
的参数值,函数返回值是滑块的当前位置。

SetScrolllnfo函数会对SCROLLINFO结构的nPage和nPos字段指定的值进行范围检查。

  • nPage字段必须指定为0~nMax - nMin + 1的值;
  • nPos字段必须指定为介于nMin~nMax - nPage + 1的值。

假设一共有95行,我们把范围设置为0~94,一页可以显示35行,nPos字段最大值为94 - 35 + 1 = 60。如果nPos等于60,客户区显示61~95行,那么最后一行在最底部,而不是最顶部。如果上述字段的值超出范围,则函数会将其设置为刚好在范围内的值。

GetScrolllnfo函数可以获取滚动条的参数,包括滚动条的最小和最大滚动范围、页面大小、滑块位置以及滑块即时位置(处理SB_THUMBTRACK请求时)

BOOL GetScrolllnfo(
_In_ HWND hwnd,
_In_ int fnBar,
_Inout_ LPSCROLLINFO Ipsi
);
  • 1
  • 2
  • 3
  • 4
  • 5

调用GetScrolllnfo函数获取滚动条参数时,SCROLLINFO.fMask字段只能使用SIF_PAGE SIF_POS SIF_RANGESIF_TRACKPOS这几个标志。为了简洁,通常直接指定为SIF_ALL标志。函数执行成功,会
将指定的滚动条参数复制到SCROLLINFO结构的相关字段中。

在处理WM_HSCROLL/WM_VSCROLL消息时,对于SB_THUMBPOSITIONI SB_THUMBTRACK滚动请求使用
HIWORD(wParam)获取到的是16位位置数据,即说最大值为65535;而使用SetScrolllnfo / GetScrolllnfo可以设置/获取的范围是32位数据,因为SCROLLINFO结构的范围和位置参数都是int类型.

接下来,我们使用SetScrolllnfo和GetScrolllnfo函数改写SystemMetrics2项目,WinMain函数除了在CreateWindowEx函数调用中加了一个WS HSCROLL样式以添加水平滚动条以外,没有任何
变化。下面仅列出WindowProc窗口过程。这里笔者将这个程序称为SystemMetrics3项目

#include <Windows.h>
#include <tchar.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]);

// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
    TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
    TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
    HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
    MSG msg;                                        // 消息循环所用的消息结构体

    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    RegisterClassEx(&wndclass);

    //采用水平滚动条、垂直滚动条
    hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0) != 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    TEXTMETRIC tm;
    SCROLLINFO si;
    HFONT hFont, hFontOld;
    static int s_iCol1, s_iCol2, s_iCol3, s_iHeight;// 第1~3列字符串的最大宽度,字符串高度
    static int s_cxClient, s_cyClient;              // 客户区宽度、高度
    static int s_cxChar;                            // 平均字符宽度,用于水平滚动条滚动单位
    int iVertPos, iHorzPos;                         // 垂直、水平滚动条的当前位置
    SIZE size = { 0 };
    int x, y;
    TCHAR szBuf[10];

    if (uMsg == WM_CREATE)
    {
        hdc = GetDC(hwnd);
        hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
        hFontOld = (HFONT)SelectObject(hdc, hFont);

        //查到要渲染数据的每一行,最宽x.
        for (int i = 0; i < NUMLINES; i++)
        {
            GetTextExtentPoint32(hdc, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel), &size);
            if (size.cx > s_iCol1)
            {
                s_iCol1 = size.cx;
            }

            GetTextExtentPoint32(hdc, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc), &size);
            if (size.cx > s_iCol2)
            {
                s_iCol2 = size.cx;
            }
            GetTextExtentPoint32(hdc, szBuf,
                wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)), &size);
            if (size.cx > s_iCol3)
            {
                s_iCol3 = size.cx;
            }
        }

        //高度,加2px,搞多一点点行间距 
        s_iHeight = size.cy + 2;

        //查文本的水平宽度
        GetTextMetrics(hdc, &tm);
        s_cxChar = tm.tmAveCharWidth;

        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        ReleaseDC(hwnd, hdc);
        return 0;
    }
    else if (uMsg == WM_SIZE)
    {
        // 查客户区宽度、高度
        s_cxClient = LOWORD(lParam);
        s_cyClient = HIWORD(lParam);

        // 设置垂直滚动条的范围和页面大小
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_RANGE | SIF_PAGE;
        si.nMin = 0;
        si.nMax = NUMLINES - 1;
        si.nPage = s_cyClient / s_iHeight;
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);


        // 设置水平滚动条的范围和页面大小
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_RANGE | SIF_PAGE;
        si.nMin = 0;
        si.nMax = (s_iCol1 + s_iCol2 + s_iCol3) / s_cxChar - 1;
        si.nPage = s_cxClient / s_cxChar;
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
        return 0;

    }
    else if (uMsg == WM_VSCROLL)
    {

        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_ALL;

        GetScrollInfo(hwnd, SB_VERT, &si);
        iVertPos = si.nPos; //查垂直滚动条的当前位置
        
        //根据用户行为先更新一波垂直滚动条的当前位置
        switch (LOWORD(wParam))
        {
        case SB_LINEUP:
            si.nPos -= 1;
            break;
        case SB_LINEDOWN:
            si.nPos += 1;
            break;
        case SB_PAGEUP:
            si.nPos -= si.nPage;
            break;
        case SB_PAGEDOWN:
            si.nPos += si.nPage;
            break;
        case SB_THUMBTRACK:
            si.nPos = si.nTrackPos;
            break;
        }
        // 设置位置,然后获取位置,如果si.nPos越界,Windows不会设置
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
       

        //当前位置 设置进来
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);

        //再查当下位置
        GetScrollInfo(hwnd, SB_VERT, &si);

        // 如果Windows更新了滚动条位置,我们更新客户区
        if (iVertPos != si.nPos)
        {
            InvalidateRect(hwnd, NULL, TRUE);
        }
        return 0;

    }
    else if (uMsg == WM_HSCROLL)
    {
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_ALL;
        GetScrollInfo(hwnd, SB_HORZ, &si);
        iHorzPos = si.nPos;
        switch (LOWORD(wParam))
        {
        case SB_LINELEFT:
            si.nPos -= 1;
            break;
        case SB_LINERIGHT:
            si.nPos += 1;
            break;
        case SB_PAGELEFT:
            si.nPos -= si.nPage;
            break;
        case SB_PAGERIGHT:
            si.nPos += si.nPage;
            break;
        case SB_THUMBTRACK:
            si.nPos = si.nTrackPos;
            break;
        }
        // 设置位置,然后获取位置,如果si.nPos越界,Windows不会设置
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
        GetScrollInfo(hwnd, SB_HORZ, &si);

        // 如果Windows更新了滚动条位置,我们更新客户区
        if (iHorzPos != si.nPos)
        {
            InvalidateRect(hwnd, NULL, TRUE);
        }
        return 0;
    }
    else if (uMsg == WM_PAINT)
    {
        hdc = BeginPaint(hwnd, &ps);

        // 获取垂直滚动条、水平滚动条位置
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        GetScrollInfo(hwnd, SB_VERT, &si);
        iVertPos = si.nPos;


        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        GetScrollInfo(hwnd, SB_HORZ, &si);
        iHorzPos = si.nPos;

        SetBkMode(hdc, TRANSPARENT);
        hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
        hFontOld = (HFONT)SelectObject(hdc, hFont);

        for (int i = 0; i < NUMLINES; i++)
        {
            x = s_cxChar * (-iHorzPos);
            y = s_iHeight * (i - iVertPos);
            TextOut(hdc, x, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
            TextOut(hdc, x + s_iCol1, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
            TextOut(hdc, x + s_iCol1 + s_iCol2, y, szBuf, wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)));
        }

        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        EndPaint(hwnd, &ps);
        return 0;

    }
    else if (uMsg == WM_DESTROY)
    {

        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
  • 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
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255

在这里插入图片描述

编译运行,可以发现,因为WM SIZE消息中SetScrolllnfo函数的SCROLLINFO结构的fMask字段没有指定SIF_DISABLENOSCROLL标志,所以在不需要水平滚动条的时候,水平滚动条是不显示的。

SystemMetrics3程序最先执行的是WM_CREATE消息,然后是WM_SIZEWM_PAINT。因为在WM_SIZE消息中需要使用s_iCol1 s_iCol2s s_iCol3 s_iHeight s_cxChar这些变量,所以我们需要在
WM_CREATE消息中提前获取这些值。看一下WM_VSCROLL消息的处理,首先调用GetScrolllnfo函数获取
滚动以前的位置。然后根据滚动请求更新si.nPos的值,调用SetScrolllnfo函数更新滚动条位置,再次调用GetScrolllnfo函数看一下滚动条位置是否真的变化了,如果变化了,就调用InvalidateRect函数使整个客户区无效。

为什么绕这么大一个弯呢?前面说过,SetScrolllnfo函数会对SCROLLINFO结构的nPage和nPos字段指定
的值进行范围检查。

nPage字段必须指定为介于0~nMax - nMin + 1的值;nPos字段必须指定为介于nMin~nMax - max(nPage -1,0)的值。如果任一值超出其范围,则函数将其设置为刚好在范围内的值。假设现在垂直滚动条滑块在位置0处
则向上滚动一个单位,执行SB_LINEUP请求,si.nPos的值变为-1 SetScrolllnfo函数是不会向上滚动一个单位的,就是说滑块位置不会变化。

只刷新无效区域

前面的SystemMetrics2和SystemMetrics3程序,不管客户区是滚动了一行还是一页,都统统宣布整个客户区无效。如果刷新客户区的代码很麻烦且耗时的话,就会造成程序界面卡顿。前面曾多次提及无效区域的概念,发生滚动条滚动请求以后,我们可以仅让新出现的那些行无效,WM PAINT只需要刷新这些行所占的区域即可,这样的逻辑无疑会更高效。当然了,对于代码逻辑很简单的情况,在当今强大的CPU面前,我们应该更注重代码简洁与易于理解。下面我们就只刷新无效区域,仅列出WindowProc中代码发生变化的几个消息 :

#include <Windows.h>
#include <tchar.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]);

// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
    TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
    TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
    HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
    MSG msg;                                        // 消息循环所用的消息结构体

    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    RegisterClassEx(&wndclass);

    //采用水平滚动条、垂直滚动条
    hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0) != 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    TEXTMETRIC tm;
    SCROLLINFO si;
    HFONT hFont, hFontOld;
    static int s_iCol1, s_iCol2, s_iCol3, s_iHeight;// 第1~3列字符串的最大宽度,字符串高度
    static int s_cxClient, s_cyClient;              // 客户区宽度、高度
    static int s_cxChar;                            // 平均字符宽度,用于水平滚动条滚动单位
    int iVertPos, iHorzPos;                         // 垂直、水平滚动条的当前位置
    SIZE size = { 0 };
    int x, y;
    TCHAR szBuf[10];

    if (uMsg == WM_CREATE)
    {
        hdc = GetDC(hwnd);
        hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
        hFontOld = (HFONT)SelectObject(hdc, hFont);        //绘画采用宋体
        //匹配使用最佳的每一列的cx
        for (int i = 0; i < NUMLINES; i++)
        {
            GetTextExtentPoint32(hdc, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel), &size);
            if (size.cx > s_iCol1)
            {
                s_iCol1 = size.cx;
            }

            GetTextExtentPoint32(hdc, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc), &size);
            if (size.cx > s_iCol2)
            {
                s_iCol2 = size.cx;
            }
            GetTextExtentPoint32(hdc, szBuf,
                wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)), &size);
            if (size.cx > s_iCol3)
            {
                s_iCol3 = size.cx;
            }
        }
        s_iHeight = size.cy + 2;             //高度,加2px,搞多一点点行间距 
        GetTextMetrics(hdc, &tm);
        s_cxChar = tm.tmAveCharWidth;        //查文本的平均字符宽度

        //恢复预设
        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        ReleaseDC(hwnd, hdc);
        return 0;
    }
    else if (uMsg == WM_SIZE)
    {
 
        s_cxClient = LOWORD(lParam);
        s_cyClient = HIWORD(lParam);  // 查客户区宽度、高度

        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_RANGE | SIF_PAGE;
        si.nMin = 0;
        si.nMax = NUMLINES - 1;
        si.nPage = s_cyClient / s_iHeight; 
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);         // 设置垂直滚动条的范围和页面大小


        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_RANGE | SIF_PAGE;
        si.nMin = 0;
        si.nMax = (s_iCol1 + s_iCol2 + s_iCol3) / s_cxChar - 1;
        si.nPage = s_cxClient / s_cxChar;
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);         // 设置水平滚动条的范围和页面大小
      
        return 0;

    }
    else if (uMsg == WM_VSCROLL)
    {

        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_ALL;

        GetScrollInfo(hwnd, SB_VERT, &si);
        iVertPos = si.nPos; //查垂直滚动条的当前位置
        
        //根据用户行为先更新一波垂直滚动条的当前位置
        switch (LOWORD(wParam))
        {
        case SB_LINEUP:
            si.nPos -= 1;
            break;
        case SB_LINEDOWN:
            si.nPos += 1;
            break;
        case SB_PAGEUP:
            si.nPos -= si.nPage;
            break;
        case SB_PAGEDOWN:
            si.nPos += si.nPage;
            break;
        case SB_THUMBTRACK:
            si.nPos = si.nTrackPos;
            break;
        }
       
        // 设置位置,然后获取位置,如果si.nPos越界,Windows不会重绘无效区
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
        GetScrollInfo(hwnd, SB_VERT, &si);
        // 如果Windows更新了滚动条位置,我们更新客户区
        if (iVertPos != si.nPos)
        {
            ::ScrollWindow(hwnd, 0, s_iHeight * (iVertPos - si.nPos), NULL, NULL);
            ::UpdateWindow(hwnd);
        }
        return 0;

    }
    else if (uMsg == WM_HSCROLL)
    {
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_ALL;
        GetScrollInfo(hwnd, SB_HORZ, &si);
        iHorzPos = si.nPos;
        switch (LOWORD(wParam))
        {
        case SB_LINELEFT:
            si.nPos -= 1;
            break;
        case SB_LINERIGHT:
            si.nPos += 1;
            break;
        case SB_PAGELEFT:
            si.nPos -= si.nPage;
            break;
        case SB_PAGERIGHT:
            si.nPos += si.nPage;
            break;
        case SB_THUMBTRACK:
            si.nPos = si.nTrackPos;
            break;
        }
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS| SIF_DISABLENOSCROLL;
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
        GetScrollInfo(hwnd, SB_HORZ, &si);
        if (iHorzPos != si.nPos)
        {
            ::ScrollWindow(hwnd, s_cxChar* (iHorzPos - si.nPos), 0, NULL, NULL);
            ::UpdateWindow(hwnd);

        }
        return 0;
    }
    else if (uMsg == WM_PAINT)
    {
        hdc = BeginPaint(hwnd, &ps);

        // 获取垂直滚动条、水平滚动条位置
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        GetScrollInfo(hwnd, SB_VERT, &si);
        iVertPos = si.nPos;


        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        GetScrollInfo(hwnd, SB_HORZ, &si);
        iHorzPos = si.nPos;

        SetBkMode(hdc, TRANSPARENT);
        hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
        hFontOld = (HFONT)SelectObject(hdc, hFont);

        //获取无效区域
        int  nPaintBeg = max(0, iVertPos + ps.rcPaint.top / s_iHeight);
        int nPaintEnd = min(NUMLINES - 1, iVertPos + ps.rcPaint.bottom / s_iHeight);

        //只对无效区域做重绘。
        for (int i = nPaintBeg; i <= nPaintEnd; i++)
        {
            x = s_cxChar * (-iHorzPos);
            y = s_iHeight * (i - iVertPos);
            TextOut(hdc, x, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
            TextOut(hdc, x + s_iCol1, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
            TextOut(hdc, x + s_iCol1 + s_iCol2, y, szBuf, wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)));
        }

        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        EndPaint(hwnd, &ps);
        return 0;

    }
    else if (uMsg == WM_DESTROY)
    {

        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
  • 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
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250

在这里插入图片描述

读者可以在PaintBeg = max(0, iVertPos + ps.rcPaint.top / s iHeight);这一行按F9键设置断点,查看发生滚动事件时PAINTSTRUCT结构的无效矩形。

ScrollWindow函数滚动指定窗口的客户区:

BOOL ScrollWindow(
_In_ HWND hWnd,	// 窗口句柄
_In_ int XAmount,// 水平滚动的量
_In_ int YAmount, // 垂直滚动的量
_In_ const RECT*pRect,// 将要滚动的客户区部分的RECT结构设置为NULL,则滚动整个客户区
_In_ const RECTpClipRect //裁剪矩形RECT结构,矩形外部的区域不会被绘制
); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

lpRect和lpClipRect这两个参数挺有意思的,读者可以试着设置一下这两个参数看一看效果。
Windows会自动将新滚动出现的区域无效化,从而产生一条WM_PAINT消息,因此不需要调用InvalidateRect函数。

在这里插入图片描述

在我的1920 1080分辨率的笔记本上,CreateWindowEx函数的宽度和高度参数指定为CW_USEDEFAULT,SystemMetrics3程序运行效果如图3下图所示。

在这里插入图片描述

可以看到客户区右边还有一大块空白,不是很美观。我们希望窗口宽度正好容纳3列文本,即根据3列文本的宽度之和计算窗口宽度,窗口宽度包括客户区宽度、滚动条宽度和边框宽度等,计算起来不是很方便。在WM_CREATE消息中s_cxChar = tm.tmAveCharWidth;语句的下面添加如下语句:

#include <Windows.h>
#include <tchar.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]);

// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
    TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
    TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
    HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
    MSG msg;                                        // 消息循环所用的消息结构体

    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    RegisterClassEx(&wndclass);

    //采用水平滚动条、垂直滚动条
    hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0) != 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    TEXTMETRIC tm;
    SCROLLINFO si;
    HFONT hFont, hFontOld;
    static int s_iCol1, s_iCol2, s_iCol3, s_iHeight;// 第1~3列字符串的最大宽度,字符串高度
    static int s_cxClient, s_cyClient;              // 客户区宽度、高度
    static int s_cxChar;                            // 平均字符宽度,用于水平滚动条滚动单位
    int iVertPos, iHorzPos;                         // 垂直、水平滚动条的当前位置
    SIZE size = { 0 };
    int x, y;
    RECT rect;
    TCHAR szBuf[10];

    if (uMsg == WM_CREATE)
    {
        hdc = GetDC(hwnd);
        hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
        hFontOld = (HFONT)SelectObject(hdc, hFont);        //绘画采用宋体
        //匹配使用最佳的每一列的cx
        for (int i = 0; i < NUMLINES; i++)
        {
            GetTextExtentPoint32(hdc, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel), &size);
            if (size.cx > s_iCol1)
            {
                s_iCol1 = size.cx;
            }

            GetTextExtentPoint32(hdc, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc), &size);
            if (size.cx > s_iCol2)
            {
                s_iCol2 = size.cx;
            }
            GetTextExtentPoint32(hdc, szBuf,
                wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)), &size);
            if (size.cx > s_iCol3)
            {
                s_iCol3 = size.cx;
            }
        }
        s_iHeight = size.cy + 2;             //高度,加2px,搞多一点点行间距 
        GetTextMetrics(hdc, &tm);
        s_cxChar = tm.tmAveCharWidth;        //查文本的平均字符宽度
        
        //加上这几行哦~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        GetClientRect(hwnd,&rect);
        rect.right = s_iCol1 + s_iCol2 + s_iCol3 + GetSystemMetrics(SM_CXVSCROLL);
        AdjustWindowRectEx(&rect,GetWindowLongPtr(hwnd,GWL_STYLE),GetMenu(hwnd) != NULL,GetWindowLongPtr(hwnd,GWL_EXSTYLE));
        SetWindowPos(hwnd,NULL,0,0,rect.right- rect.left,rect.bottom - rect.top,SWP_NOZORDER|SWP_NOMOVE);
        //加上这几行哦~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


        //恢复预设
        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        ReleaseDC(hwnd, hdc);
        return 0;
    }
    else if (uMsg == WM_SIZE)
    {
 
        s_cxClient = LOWORD(lParam);
        s_cyClient = HIWORD(lParam);  // 查客户区宽度、高度

        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_RANGE | SIF_PAGE;
        si.nMin = 0;
        si.nMax = NUMLINES - 1;
        si.nPage = s_cyClient / s_iHeight; 
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);         // 设置垂直滚动条的范围和页面大小


        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_RANGE | SIF_PAGE;
        si.nMin = 0;
        si.nMax = (s_iCol1 + s_iCol2 + s_iCol3) / s_cxChar - 1;
        si.nPage = s_cxClient / s_cxChar;
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);         // 设置水平滚动条的范围和页面大小
      
        return 0;

    }
    else if (uMsg == WM_VSCROLL)
    {

        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_ALL;

        GetScrollInfo(hwnd, SB_VERT, &si);
        iVertPos = si.nPos; //查垂直滚动条的当前位置
        
        //根据用户行为先更新一波垂直滚动条的当前位置
        switch (LOWORD(wParam))
        {
        case SB_LINEUP:
            si.nPos -= 1;
            break;
        case SB_LINEDOWN:
            si.nPos += 1;
            break;
        case SB_PAGEUP:
            si.nPos -= si.nPage;
            break;
        case SB_PAGEDOWN:
            si.nPos += si.nPage;
            break;
        case SB_THUMBTRACK:
            si.nPos = si.nTrackPos;
            break;
        }
       
        // 设置位置,然后获取位置,如果si.nPos越界,Windows不会重绘无效区
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
        GetScrollInfo(hwnd, SB_VERT, &si);
        // 如果Windows更新了滚动条位置,我们更新客户区
        if (iVertPos != si.nPos)
        {
            ::ScrollWindow(hwnd, 0, s_iHeight * (iVertPos - si.nPos), NULL, NULL);
            ::UpdateWindow(hwnd);
        }
        return 0;

    }
    else if (uMsg == WM_HSCROLL)
    {
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_ALL;
        GetScrollInfo(hwnd, SB_HORZ, &si);
        iHorzPos = si.nPos;
        switch (LOWORD(wParam))
        {
        case SB_LINELEFT:
            si.nPos -= 1;
            break;
        case SB_LINERIGHT:
            si.nPos += 1;
            break;
        case SB_PAGELEFT:
            si.nPos -= si.nPage;
            break;
        case SB_PAGERIGHT:
            si.nPos += si.nPage;
            break;
        case SB_THUMBTRACK:
            si.nPos = si.nTrackPos;
            break;
        }
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS| SIF_DISABLENOSCROLL;
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
        GetScrollInfo(hwnd, SB_HORZ, &si);
        if (iHorzPos != si.nPos)
        {
            ::ScrollWindow(hwnd, s_cxChar* (iHorzPos - si.nPos), 0, NULL, NULL);
            ::UpdateWindow(hwnd);

        }
        return 0;
    }
    else if (uMsg == WM_PAINT)
    {
        hdc = BeginPaint(hwnd, &ps);

        // 获取垂直滚动条、水平滚动条位置
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        GetScrollInfo(hwnd, SB_VERT, &si);
        iVertPos = si.nPos;


        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        GetScrollInfo(hwnd, SB_HORZ, &si);
        iHorzPos = si.nPos;

        SetBkMode(hdc, TRANSPARENT);
        hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
        hFontOld = (HFONT)SelectObject(hdc, hFont);

        //获取无效区域
        int  nPaintBeg = max(0, iVertPos + ps.rcPaint.top / s_iHeight);
        int nPaintEnd = min(NUMLINES - 1, iVertPos + ps.rcPaint.bottom / s_iHeight);

        //只对无效区域做重绘。
        for (int i = nPaintBeg; i <= nPaintEnd; i++)
        {
            x = s_cxChar * (-iHorzPos);
            y = s_iHeight * (i - iVertPos);
            TextOut(hdc, x, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
            TextOut(hdc, x + s_iCol1, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
            TextOut(hdc, x + s_iCol1 + s_iCol2, y, szBuf, wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)));
        }

        SelectObject(hdc, hFontOld);
        DeleteObject(hFont);
        EndPaint(hwnd, &ps);
        return 0;

    }
    else if (uMsg == WM_DESTROY)
    {
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
  • 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
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258

在这里插入图片描述

窗口高度我们没有改变。首先通过调用GetClientRect函数获取客户区坐标,客户区坐标是相对于窗口客户区左上角的,因此获取到的左上角的坐标是(0,0),即rect.right等于客户区宽度,rect.bottom等于客户区高度;

把客户区的宽度重新设置为三列文本宽度之和再加上垂直滚动条的宽度;

为AdjustWindowRectEx函数指定窗口样式、扩展窗口样式以及是否有菜单栏,该函数可以根据客户区坐标计算窗口坐标,但是计算出的窗口坐标不包括滚动条,所以前面客户区的宽度我们又加上了一个垂直滚动条的宽度,需要注意的是,计算出来的窗口坐标是相对于客户区左上角的。窗口坐标的左上角并不是(0,0),所以
窗口宽度等于rect.right-rect.left,窗口高度等于rect.bottom-rect.top;最后调用SetWindowPos函数设置窗口大
小。
GetMenu函数获取指定窗口的菜单句柄,如果函数执行成功,则返回值是菜单的句柄;如果这个窗口没有菜单,则返回NULL,这个函数很简单。接下来介绍一下GetWindowLongPtr AdjustWindowRectEx
和SetWindowPos这3个函数。

要介绍GetWindowLongPtr函数,不得不先介绍一下SetWindowLongPtr函数。SetWindowLongPtr是SetWindowLong函数的升级版本,指针和句柄在32位Windows上为32位,在64位Windows上为64位。使用SetWindowLong函数设置指针或句柄只能设置32位的,要编写32位和64位版本兼容的代码,应该使用
SetWindowLongPtr函数。如果编译为32位程序,则对SetWindowLongPtr函数的调用实际上还是调用SetWindowLong。

SetWindowLongPtr函数设置窗口类中与每个窗口关联的额外内存的数据,或设置指定窗口的属性:

LONG PTR WINAPI SetWindowLongPtr(
_In_ HWND hWnd, // 窗口句柄
_In_ int nIndex, // 要设置哪一项
_In_ LONG_PTR dwNewLong // 新值
);
  • 1
  • 2
  • 3
  • 4
  • 5

参数nIndex指定要设置哪一项。WNDCLASSEX结构有一个cbWndExtra段,该字段用于指定紧跟在窗口实例后面的的附加数据字节数,用来存放自定义数据,即与每个窗口相关联的附加自定义数据,假设我们设置wndclass.cbWndExtra = 16,16字节可以存放2个 int64型数据或4个int型数据,nlndex参数指定为0 8分别表
示要设置第1-2个 int64型数据。如果存放的是int型数据,则可以设置为0 4 8 12。如果要设置窗口的一些属性,值如下表所示。

常用宏含义
GWL_EXSTYLE设置扩展窗口样式
GWL_STYLE设置窗口样式
GWLP_HINSTANCE设置应用程序的实例句柄
GWLP_ID设置窗口的ID,用于子窗口
GWLP_USERDATA设置与窗口关联的用户数据
GWLP_WNDPROC设置指向窗口过程的指针

如果函数执行成功,则返回值是指定偏移量处或窗口属性的先前值;
如果函数执行失败,则返回值为0。

GetWindowLong和GetWindowLongPtr函数可以获取指定窗口的自
定义数据或窗口的一些属性:

LONG_PTR WINAPI GetWindowLongPtr(
_In_ HWND hWnd, // 窗口句柄
_In_ int nlndex // 要获取哪一项
); 
  • 1
  • 2
  • 3
  • 4

如果函数执行成功,则返回所请求的值;如果函数执行失败,则返回值为0。

AdjustWindowRectEx函数根据客户区的大小计算所需的窗口大小:

BOOL WINAPI AdjustWindowRectEx(
_Inout LPRECTIpRect, // 提供客户区坐标的RECT结构,函数在这个结构中返回所需的窗口坐标
_In_ DWORD dwStyle, // 窗口的窗口样式
_In_ BOOL bMenu, //窗口是否有菜单
_In_ DWORD dwExStyle); // 窗口的扩展窗口样式
  • 1
  • 2
  • 3
  • 4
  • 5

SetWindowPos函数可以更改一个子窗口、顶级窗口的大小、位置和Z顺序,其中Z顺序是指窗口的前后顺序。假设桌面上有很多程序窗口,互相重叠覆盖,更改顺序可以确定哪个窗口在前或在后:

BOOL WINAPI SetWindowPos(
_In_ HWND hWnd, //要调整大小、位置或顺序的窗口的窗口句柄
_In_opt_ HWND hWndInsertAfter,// 指定一个窗口句柄或一些预定义值
_In_ int X, //窗口新位置的X坐标,以像素为单位
_In_ int Y, //窗口新位置的Y坐标,以像素为单位
_In_ int cx, //窗口的新宽度,以像素为单位
_In_ int cy, //窗口的新高度,以像素为单位
_In_ UINT uFlags //窗口的大小和定位标志
);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 参数hWndInsertAfter指定一个窗口句柄,hWnd窗口将位于这个窗口之前,即hWndlnsertAfter窗口作为定位窗口,可以设置为NULL。参数hWndlnsertAfter也可以指定为下表所示的值。
宏常量含义
HWND_TOP把窗口放置在Z顺序的顶部
HWND_BOTTOM把窗口放置在Z顺序的底部
HWND_TOPMOST窗口始终保持为最顶部的窗口,即使该窗口没有被激活
HWND_NOTOPMOST取消始终保持为最顶部的窗口
  • 参数X和Y指定窗口新位置的X和Y坐标。如果hWnd参数指定的窗口是顶级窗口,则相对于屏幕左上角;如果是子窗口,则相对于父窗口客户区的左上角。
  • 参数uFlags指定窗口的大小和定位标志,常用的值如下表所示
宏定义含义
SWP_NOZORDER维持当前Z序 (忽略hWndInsertAfter参数)
SWP_NOMOVE维持当前位置(忽略X和Y参数)
SWP_NOSIZE维持当前尺寸(忽略cx和cy参数)
SWP_HIDEWINDOW隐藏窗口
SWP_SHOWWINDOW显示窗口

例如上面的示例使用SWP NOZORDERISWP NOMOVE标志,表示忽略hWndInsertAfter X和Y参数,保持Z顺序和窗口位置不变,仅改变窗口大小。有时候我们希望把一个窗口设置为始终保持为最顶部
可以这样使用:

SetWindowPos(hwnd, HWND TOPMOST,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE);
  • 1

MoveWindow函数可以更改一个子窗口、顶级窗口的位置和尺寸。实际上SetWindowPos函数具有MoveWindow的全部功能。MoveWindow函数通常用于设置子窗口

BOOL WINAPI MoveWindow(
_In_HWND hWnd, // 要调整大小、位置的窗口的窗口句柄
_In_ int X, // 窗口新位置的X坐标,以像素为单位
_In_ int Y, //窗口新位置的Y坐标,以像素为单位
_In_ int nWidth, // 窗口的新宽度,以像素为单位
_In_int nHeight, // 窗口的新高度,以像素为单位
_In_BOOL bRepaint);// 是否要重新绘制窗口,通常指定为TRUE
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

总结一下,窗口过程在什么时候会收到WM_PAINT消息?

  • 当程序窗口被首次创建时,整个客户区都是无效的,因为此时应用程序尚未在该窗口上绘制任何东西,此时窗口过程会收到第一条 WM_PAINT消息。

  • 在调整程序窗口的尺寸时,客户区也会变为无效。我们把WNDCLASSEX结构的style字段设置为
    CS_HREDRAW|CS_VREDRAW,表示当程序窗口尺寸发生变化时整个窗口客户区都应宣布无效,窗口过程会接收到一条WM_PAINT消息。

  • 如果先最小化程序窗口,然后将窗口恢复到原先的尺寸,那么Windows并不会保存原先客户区的内容,窗口过程接收到WM_PAINT消息后需要自行恢复客户区的内容。

  • 程序调用InvalidateRect或InvalidateRgn函数向客户区添加无效区域,会生成WM_PAINT消息。

  • 在屏幕中拖动程序窗口的全部或一部分到屏幕以外,然后又拖动回屏幕中的时候,窗口被标记为无效,窗口过程会收到一条WM_PAINT 消息,并对客户区的内容进行重绘.

  • 程序调用ScrollWindowScrollDC函数滚动客户区。

保存设备环境

调用GetDC或BeginPaint函数以后,会返回一个DC句柄,DC的所有属性都被设定为默认值。如果程序需要使用非默认的DC属性,可以在获取到DC句柄以后设置相关DC属性;在调用ReleaseDC或EndPaint
函数以后,系统将恢复DC属性的默认值,对属性所做的任何改变都会丢失。例如:

case WM_PAINT:
    hdc = BeginPaint(hwnd, &ps);
    //设置设备环境属性
    // 绘制代码
    EndPaint(hwnd, &ps);
    return 0;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

有没有办法在释放DC时保存对属性所做的更改,以便在下次调用GetDC或BeginPaint函数时这些属性仍然有效呢?还记得WNDCLASSEX结构的第2个字段style吗?这个字段指定窗口类样式,其中CS_OWNDC表示为窗口类的每个窗口分配唯一的DC。可以按如下方式设置style字段.

wndclass.style = CS_HREDRAWI|CS_VREDRAWI|CS_OWNDC:
  • 1

现在,每个基于这个窗口类创建的窗口都有它私有的专用DC。使用CS_OWNDC样式以后,只需要初始化DC属性一次,例如,在处理WM_CREATE消息时:

case WM_CREATE:
    hdc = GetDC(hwnd);
    //设置设备环境属性
    ReleaseDC(hwnd, hdc);
	return 0;
  • 1
  • 2
  • 3
  • 4
  • 5

在窗口的生命周期内,除非再次改变DC的属性值,原DC属性会一直有效。对于SystemMetrics程序,我们可以在WM_CREATE消息中设置背景模式和字体,这样一来就不需要每一次都在WM_PAINT消息中
设置了。对于客户区需要大量绘图操作的情况,指定CS_OWNDC样式可以提高程序性能。
需要注意的是,指定CS OWNDC样式仅影响通过GetDC和BeginPaint函数获取的DC句柄,通过其他函数 (例如
GetWindowDC) 获取的DC是不受影响的。

#include <Windows.h>
#include <tchar.h>
#include "Metrics.h"

const int NUMLINES = sizeof(METRICS) / sizeof(METRICS[0]);

// 函数声明,窗口过程
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndclass;                            // RegisterClassEx函数用的WNDCLASSEX结构
    TCHAR szClassName[] = TEXT("MyWindow");         // RegisterClassEx函数注册的窗口类的名称
    TCHAR szAppName[] = TEXT("GetSystemMetrics");   // 窗口标题
    HWND hwnd;                                      // CreateWindowEx函数创建的窗口的句柄
    MSG msg;                                        // 消息循环所用的消息结构体

    wndclass.cbSize = sizeof(WNDCLASSEX);
    wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; //激活~~~~令窗口分配唯一的DC实例,而不是每一次都创建新的
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)(COLOR_3DFACE + 1);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    RegisterClassEx(&wndclass);

    //采用水平滚动条、垂直滚动条
    hwnd = CreateWindowEx(0, szClassName, szAppName, WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    while (GetMessage(&msg, NULL, 0, 0) != 0)
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    TEXTMETRIC tm;
    SCROLLINFO si;
    HFONT hFont, hFontOld;
    static int s_iCol1, s_iCol2, s_iCol3, s_iHeight;// 第1~3列字符串的最大宽度,字符串高度
    static int s_cxClient, s_cyClient;              // 客户区宽度、高度
    static int s_cxChar;                            // 平均字符宽度,用于水平滚动条滚动单位
    int iVertPos, iHorzPos;                         // 垂直、水平滚动条的当前位置
    SIZE size = { 0 };
    int x, y;
    RECT rect;
    TCHAR szBuf[10];

    if (uMsg == WM_CREATE)
    {
        hdc = GetDC(hwnd);

        //设置预设的设备对象
        hFont = CreateFont(12, 0, 0, 0, 0, 0, 0, 0, GB2312_CHARSET, 0, 0, 0, 0, TEXT("宋体"));
        hFontOld = (HFONT)SelectObject(hdc, hFont);        //绘画采用宋体
        SetBkMode(hdc, TRANSPARENT);

        
     
        //匹配使用最佳的每一列的cx
        for (int i = 0; i < NUMLINES; i++)
        {
            GetTextExtentPoint32(hdc, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel), &size);
            if (size.cx > s_iCol1)
            {
                s_iCol1 = size.cx;
            }

            GetTextExtentPoint32(hdc, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc), &size);
            if (size.cx > s_iCol2)
            {
                s_iCol2 = size.cx;
            }
            GetTextExtentPoint32(hdc, szBuf,
                wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)), &size);
            if (size.cx > s_iCol3)
            {
                s_iCol3 = size.cx;
            }
        }
        s_iHeight = size.cy + 2;             //高度,加2px,搞多一点点行间距 
        GetTextMetrics(hdc, &tm);
        s_cxChar = tm.tmAveCharWidth;        //查文本的平均字符宽度
        

        //更新窗口大小适配渲染文本
        GetClientRect(hwnd,&rect);
        rect.right = s_iCol1 + s_iCol2 + s_iCol3 + GetSystemMetrics(SM_CXVSCROLL);
        AdjustWindowRectEx(&rect,GetWindowLongPtr(hwnd,GWL_STYLE),GetMenu(hwnd) != NULL,GetWindowLongPtr(hwnd,GWL_EXSTYLE));
        SetWindowPos(hwnd,NULL,0,0,rect.right- rect.left,rect.bottom - rect.top,SWP_NOZORDER|SWP_NOMOVE);
 

        DeleteObject(hFont);
        ReleaseDC(hwnd, hdc);
        return 0;
    }
    else if (uMsg == WM_SIZE)
    {
 
        s_cxClient = LOWORD(lParam);
        s_cyClient = HIWORD(lParam);  // 查客户区宽度、高度

        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_RANGE | SIF_PAGE;
        si.nMin = 0;
        si.nMax = NUMLINES - 1;
        si.nPage = s_cyClient / s_iHeight; 
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);         // 设置垂直滚动条的范围和页面大小


        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_RANGE | SIF_PAGE;
        si.nMin = 0;
        si.nMax = (s_iCol1 + s_iCol2 + s_iCol3) / s_cxChar - 1;
        si.nPage = s_cxClient / s_cxChar;
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);         // 设置水平滚动条的范围和页面大小
      
        return 0;

    }
    else if (uMsg == WM_VSCROLL)
    {

        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_ALL;

        GetScrollInfo(hwnd, SB_VERT, &si);
        iVertPos = si.nPos; //查垂直滚动条的当前位置
        
        //根据用户行为先更新一波垂直滚动条的当前位置
        switch (LOWORD(wParam))
        {
        case SB_LINEUP:
            si.nPos -= 1;
            break;
        case SB_LINEDOWN:
            si.nPos += 1;
            break;
        case SB_PAGEUP:
            si.nPos -= si.nPage;
            break;
        case SB_PAGEDOWN:
            si.nPos += si.nPage;
            break;
        case SB_THUMBTRACK:
            si.nPos = si.nTrackPos;
            break;
        }
       
        // 设置位置,然后获取位置,如果si.nPos越界,Windows不会重绘无效区
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
        GetScrollInfo(hwnd, SB_VERT, &si);
        // 如果Windows更新了滚动条位置,我们更新客户区
        if (iVertPos != si.nPos)
        {
            ::ScrollWindow(hwnd, 0, s_iHeight * (iVertPos - si.nPos), NULL, NULL);
            ::UpdateWindow(hwnd);
        }
        return 0;

    }
    else if (uMsg == WM_HSCROLL)
    {
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_ALL;
        GetScrollInfo(hwnd, SB_HORZ, &si);
        iHorzPos = si.nPos;
        switch (LOWORD(wParam))
        {
        case SB_LINELEFT:
            si.nPos -= 1;
            break;
        case SB_LINERIGHT:
            si.nPos += 1;
            break;
        case SB_PAGELEFT:
            si.nPos -= si.nPage;
            break;
        case SB_PAGERIGHT:
            si.nPos += si.nPage;
            break;
        case SB_THUMBTRACK:
            si.nPos = si.nTrackPos;
            break;
        }
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS| SIF_DISABLENOSCROLL;
        SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
        GetScrollInfo(hwnd, SB_HORZ, &si);
        if (iHorzPos != si.nPos)
        {
            ::ScrollWindow(hwnd, s_cxChar* (iHorzPos - si.nPos), 0, NULL, NULL);
            ::UpdateWindow(hwnd);

        }
        return 0;
    }
    else if (uMsg == WM_PAINT)
    {
        hdc = BeginPaint(hwnd, &ps);

        // 获取垂直滚动条、水平滚动条位置
        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        GetScrollInfo(hwnd, SB_VERT, &si);
        iVertPos = si.nPos;

        si.cbSize = sizeof(SCROLLINFO);
        si.fMask = SIF_POS;
        GetScrollInfo(hwnd, SB_HORZ, &si);
        iHorzPos = si.nPos;

     
        //获取无效区域
        int  nPaintBeg = max(0, iVertPos + ps.rcPaint.top / s_iHeight);
        int nPaintEnd = min(NUMLINES - 1, iVertPos + ps.rcPaint.bottom / s_iHeight);

        //只对无效区域做重绘。
        for (int i = nPaintBeg; i <= nPaintEnd; i++)
        {
            x = s_cxChar * (-iHorzPos);
            y = s_iHeight * (i - iVertPos);
            TextOut(hdc, x, y, METRICS[i].m_pLabel, _tcslen(METRICS[i].m_pLabel));
            TextOut(hdc, x + s_iCol1, y, METRICS[i].m_pDesc, _tcslen(METRICS[i].m_pDesc));
            TextOut(hdc, x + s_iCol1 + s_iCol2, y, szBuf, wsprintf(szBuf, TEXT("%d"), GetSystemMetrics(METRICS[i].m_nIndex)));
        }


        EndPaint(hwnd, &ps);
        return 0;

    }
    else if (uMsg == WM_DESTROY)
    {
        PostQuitMessage(0);
        return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
  • 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
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257

有时候可能想改变某些DC属性,然后使用改变后的属性进行绘制,接着再恢复原来的DC属性,可以使用SaveDC和RestoreDC函数来保存和恢复DC状态。
SaveDC函数通过把DC属性压入DC堆栈来保存指定DC的当前状态

int SaveDC( In HDC hdc); // 要保存其状态的设备环境柄
  • 1

如果函数执行成功,则返回值将标识保存的状态;如果函数执行失败,则返回值为0。

RestoreDC函数通过从DC堆栈中弹出状态信息来恢复DC到指定状态:

BOOL RestoreDC(
_In_ HDC hdc, // 要恢复其状态的设备环境柄
_In_ int nSavedDC // 要还原的保存状态
);
  • 1
  • 2
  • 3
  • 4
  • 参数nSavedDC指定要还原的保存状态,可以指定为SaveDC函数的返回值,或者指定为负数,例如-1表示最近保存的状态,-2表示最近保存的状态的前一次。
  • 同一状态不能多次恢复,恢复状态后保存的所有状态将被弹出销毁。
    另外还有一点需要注意,请看代码:
//设置设备环境属性,然后保存
nDC1 = SaveDC(hdc);

//再次设置设备环境属性,然后保存
nDC2 = SaveDC(hdc):
...........
RestoreDC(hdc, nDC1);

//使用状态1进行绘图
RestoreDC(hdc,nDC2); // 恢复失败

//使用状态2进行绘图
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

调用RestoreDC函数把DC状态恢复到nDC1,DC堆栈会弹出nDC1及以后压入堆栈的内容。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/665422
推荐阅读
相关标签
  

闽ICP备14008679号