前言:
学习笔记,随时更新。如有谬误,欢迎指正。
说明:
- 红色字体为较为重要部分。
- 绿色字体为个人理解部分。
原文链接:https://learn.microsoft.com/en-us/windows/win32/winmsg/window-classes
1 窗口类
- 窗口类是一组属性,系统将其用作模板来创建窗口实例。
- 每个窗口都是窗口类的成员。
- 所有窗口类都是特定于进程的。
1.1 窗口类概述
- 每个窗口类都有一个由所有该窗口类对象所共享的窗口程序来处理该类的所有窗口实例的消息,以控制其行为和外观。
- 进程必须先注册窗口类,然后才能创建该类的窗口。注册窗口类会将窗口程序、类样式和其他类属性与类名(因此类名是比较重要的,是 Key,创建窗口类对象时都是使用类名字符串来查找窗口类)相关联。
1.1.1 关于窗口类
1.1.1.1 窗口类的类型
- 窗口类有三种类型:系统类,应用程序全局类,应用程序本地类。这些类型的区别在于使用范围、注册和销毁的时间和方式。
1.1.1.1.1 系统类
- 系统类是由系统注册的窗口类。一部分系统类可供所有进程使用,而另一部分系统类仅供系统内部使用。
- 由于系统注册这些类,因此进程无法销毁它们。
- 在进程的一个线程第一次调用用户或 GDI 函数时,系统会为该进程注册系统类。每个应用程序都接收自己的系统类副本。
- 可供所有进程使用的系统类如下:
- Button :按钮类。
- ComboBox :组合框类。
- Edit :编辑控件类
- ListBox :列表框类
- MDIClient :MDI 客户端窗口类。
- ScrollBar :滚动条类。
- Static :静态控件类。
1.1.1.1.2 应用程序全局类
- 应用程序全局类可供进程中的所有其他模块使用,其类样式必须指定 CS_GLOBALCLASS 样式,由可执行文件或 dll 进行注册。
- 若要创建可在每个进程中使用的类可通过以下方式:在 dll 中创建窗口类,在 dll 初始化逻辑中注册该类,并在每个进程中加载该 dll (要实现在每个进程中加载该 dll ,可以将 dll 名称添加到 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows 注册表项中的 AppInit_DLLs 的值中,每当进程启动时,系统就会在新启动的进程上下文中加载该指定的 dll ,然后调用其入口点函数)。
- 使用 UnregisterClass 函数可以删除应用程序全局类并释放与其关联的存储。
1.1.1.1.3 应用程序本地类
- 应用程序本地类是可执行文件或 .dll 注册以独占使用的任何窗口类。
- 注册应用程序本地类的模块关闭时,系统会销毁本地类(自动销毁)。 也可以调用 UnregisterClass 函数删除本地类并释放与其关联的存储。
1.1.1.2 系统如何定位窗口类
- 当应用程序调用 CreateWindow 或 CreateWindowEx 函数来创建具有指定类的窗口时,系统将通过此优先级来查找窗口类:
- 首先,在应用程序本地类列表中搜索具有指定的名称类、且实例句柄与模块实例句柄相匹配的窗口类(也就是说多个模块可以使用同一名称在同一个进程中注册本地类,但由于模块句柄不同, 使用时可以通过传入不同的模块句柄来精准分辨。)。
- 其次,如果应用程序本地类列表中无法匹配到,则搜索应用程序全局类的列表。
- 最后,如果应用程序全局类列表中无法匹配到,则搜索系统类的列表。
- 程序创建的所有窗口都使用此顺序来查找窗口类,包括系统代表应用程序创建的窗口,例如对话框。因此可以在不影响其他应用程序的情况下替代系统类。也就是说应用程序可以注册与系统类同名的应用程序本地类来替换应用程序上下文中的系统类,这样可以替换掉当前应用程序中对于该系统类的使用,而不影响其他应用程序对该系统类的使用。
1.1.1.3 注册窗口类
- 注册窗口类的第一步是使用窗口类信息填充 WNDCLASSEX 结构。窗口类相关信息见窗口类的元素。然后将结构传递给 RegisterClassEx 函数即。
- 注册应用程序全局类时请在 WNDCLASSEX 结构的样式成员变量中指定 CS_GLOBALCLASS 样式。注册应用程序本地类时不要指定 CS_GLOBALCLASS 样式。
- 如果使用 ANSI版本的窗口类注册函数( RegisterClassA 、 RegisterClassExA )注册窗口类,应用程序会请求系统使用 ANSI 字符集将消息的文本参数传递到所创建类的窗口;如果使用 Unicode 版本的窗口类注册函数( RegisterClassW 、 RegisterClassExW )注册窗口类,则应用程序会请求系统使用 Unicode 字符集将消息的文本参数传递到所创建类的窗口。
- 窗口类的所有者是注册该窗口类的 dll 或者可执行文件,系统从传递给窗口类注册函数函数的 WNDCLASSEX 结构的 hInstance 变量来确定类所有权。 对于 dll , hInstance 必须是 .dll 实例的句柄。
- 在拥有某窗口类的 .dll 被卸载时,不会销毁该类(???好像与前面应用程序本地类的说明不一致)。因此再.dll 卸载之后,如果系统调用该类窗口实例的窗口程序,将导致访问冲突,因为包含窗口程序的.dll不再位于内存中。在卸载 .dll 并调用 UnregisterClass 函数之前,进程必须销毁所有使用该窗口类创建的窗口实例。
1.1.1.4 窗口类的元素
- 窗口类的元素定义了属于该类的窗口的默认行为。
- GetClassInfoEx 和 GetClassLong 函数可以获取有关给定窗口类的信息。
- SetClassLong 函数可以更改应用程序已注册的本地类或全局类的元素。
- 管完整的窗口类由许多元素组成,但使用时其实只需要应用程序提供类名、窗口过程地址和实例句柄等必要数据就够了,其他元素使用窗口类的默认属性。必须将 WNDCLASSEX 结构中任何未使用的成员变量初始化为零或 NULL 。
1.1.1.4.1 类名
- 每个窗口类都需要一个类名来区分一个类。
- 通过将 WNDCLASSEX 结构的 lpszClassName 成员变量设置为以 null 结尾的指定字符串的地址来指定类名。
- 由于窗口类特定于进程,因此窗口类名称仅需保证在同一进程中唯一即可。
- 由于类名在系统的专用原子表中占用空间,因此应尽可能短地保留类名字符串。
- GetClassName 函数可以查询给定窗口所属的类的名称。
1.1.1.4.2 窗口程序地址
- 每个类都需要一个窗口程序地址来定义用于处理类中窗口的所有消息的窗口程序的入口点。 当系统要求窗口执行任务(例如绘制其工作区或响应用户输入)时,系统会将消息传递给窗口程序。
- 进程将窗口程序地址设置到 WNDCLASSEX 结构的 lpfnWndProc 成员变量中来将窗口程序分配给窗口类。
1.1.1.4.3 实例句柄
- 每一个窗口类都需要一个实例句柄来标识注册该类的 .dll 或可执行文件。系统需要实例句柄来跟踪所有模块。
- 系统为正在运行的可执行文件或 .dll 的每个副本分配一个句柄。系统将实例句柄传递给每个可执行文件的入口点函数(请参阅 WinMain )和 .dll (请参阅 DllMain )。可执行文件或 .dll 通过将此实例句柄复制到 WNDCLASSEX 结构的 hInstance 成员变量中来将其分配给窗口类。
1.1.1.4.4 类光标
- 类光标定义了鼠标位于该类窗口实例的工作区时光标的形状。
- 光标形状分配给窗口类时,请先使用 LoadCursor 函数加载预定义的光标形状(或提供自定义光标资源,并使用 LoadCursor 函数从应用程序的资源加载它),然后将返回的光标句柄分配给 WNDCLASSEX 结构的 hCursor 成员变量。
- 如果应用程序将 WNDCLASSEX 结构的 hCursor 成员变量设置为 NULL ,则不会定义类光标。 系统假定(也就是说开发者就得这么做,否则就没有光标显示出来)每次光标移动到窗口时,窗口都会设置光标形状。每当窗口收到 WM_MOUSEMOVE 消息时,窗口都可以通过调用 SetCursor 函数来设置光标形状。
1.1.1.4.5 类图标
- 类图标是系统用来表示特定类的窗口的图片。
- 一个应用程序可以有两个类图标,一个大图标和一个小图标。在用户按 Alt+TAB 显示的任务切换窗口时、以及在任务栏和资源管理器的大图标视图中系统会显示窗口类的大图标。在窗口的标题栏中、以及任务栏和资源管理器的小图标视图中系统会显示窗口类的小图标。
- 通过在 WNDCLASSEX 结构的 hIcon 和 hIconSm 成员变量中指定图标的句柄来设置窗口类的大图标和小图标。
- 在 GetSystemMetrics 函数中分别传入 SM_CXICON 和 SM_CYICON 可以查询窗口类大图标的尺寸,分别传入 SM_CXSMICON 和 SM_CYSMICON 值可以查询窗口类小图标的尺寸。
- 如果应用程序将 WNDCLASSEX 结构的 hIcon 和 hIconSm 成员变量设置为 NULL,则系统会使用默认应用程序图标作为窗口类的大图标和小图标。如果仅指定窗口类大图标而不指定小图标,则系统会基于大图标创建一个小图标。但如果仅指定窗口类的小图标而不指定大图标,系统会使用默认应用程序图标作为大图标,将指定的图标用作小图标。
- 使用 WM_SETICON 消息可以替换特定窗口的大图标或小图标。
- 使用 WM_GETICON 消息可以查询当前窗口的大图标或小图标。
1.1.1.4.6 类背景画笔
- 类背景画笔为应用程序后续的绘图准备窗口的工作区。系统使用纯色或图案填充的画笔来填充工作区,从而从该位置删除所有以前的图像(无论它们是否属于窗口)。
- 系统通过向窗口发送 WM_ERASEBKGND 消息来通知窗口应绘制其背景。
- 若要将背景画笔分配给类,请使用适当的 GDI 函数创建画笔,并将返回的画笔句柄分配给 WNDCLASSEX 结构的 hbrBackground 成员变量。
- hbrBackground 可以是标准系统颜色值。可使用 GetSysColors 函数来查询标准系统颜色值。
- 类背景画笔不是一定需要设置。如果此参数设置为 NULL ,则每当收到 WM_ERASEBKGND 消息时,窗口都必须自己l来绘制背景。
1.1.1.4.7 类菜单
- 类菜单指定了类中窗口使用的默认菜单(如果在创建窗口时提供了提供菜单,则使用创建时指定的菜单)。
- 将 WNDCLASSEX 结构的 lpszMenuName 成员变量设置为指定菜单的资源名称的以 null 结尾的字符串的地址,可以将菜单分配给类。系统在需要时自动加载菜单。如果菜单资源由整数来标识,则可以通过 MAKEINTRESOURCE 将相关标识处理后再设置给 lpszMenuName 。
- 如果 lpszMenuName 被设置为 NULL ,则类中的窗口没有菜单栏。即使未提供类菜单,但仍可以在创建窗口时为窗口定义菜单栏。
- 如果设置了菜单的窗口类被创建了子窗口对象,则菜单将会被忽略。
1.1.1.4.8 类样式
- 类样式定义窗口类的其他元素。可以使用按位或运算符进行组合。
- 通过将样式分配给 WNDCLASSEX 结构的 style 成员变量变量来设置类样式。
- 设备上下文是应用程序用于绘制其窗口工作区的一组特殊值。系统要求显示的每个窗口都有一个设备上下文,但在系统如何存储和处理该设备上下文方面允许一些灵活性。如果未显式指定设备上下文样式,则系统假定每个窗口使用从系统维护的上下文池中检索的设备上下文。 在这种情况下,每个窗口必须在绘制之前检索和初始化设备上下文,并在绘制后释放设备上下文。为了避免每次在窗口内绘制时都需要检索设备上下文,应用程序可以为窗口类指定 CS_OWNDC 样式。 此类样式指示系统创建专用设备上下文,即为类中的每个窗口分配唯一的设备上下文。 应用程序只需检索上下文一次,然后将其用于所有后续绘制。
1.1.1.4.9 额外类内存
- 操作系统在内部为系统中的每个窗口类维护 WNDCLASSEX 结构。当应用程序注册窗口类时,可以指示操作系统分配一些额外的内存字节并将其追加到 WNDCLASSEX 结构的末尾。 此内存称为额外类内存 ,由属于该类的所有窗口共享。 使用额外的类内存来存储与类相关的任何信息。
- 额外的内存是(自动)从系统的本地堆中分配的,其大小应该小于40个字节。如果请求的额外类内存量大于 40 字节, 则 RegisterClassEx 函数将失败。如果确实需要超过 40 个字节的额外类内存,则应自己申请内存,并将指向该内存的指针存储在额外类内存中。
- SetClassWord 和 SetClassLong 函数可以将数据复制到额外类内存。若要从额外类内存中获取值,请使用 GetClassWord 和 GetClassLong 函数。
- 通过WNDCLASSEX 结构的 cbClsExtra 成员变量指定要分配的额外类内存量。不使用额外类内存的应用程序必须将 cbClsExtra 成员变量初始化为零。
1.1.1.4.10 额外窗口内存
- 系统为每个窗口维护一个内部数据结构。注册窗口类时,应用程序可以指定一些额外的内存字节,称为额外窗口内存。 创建类的窗口时,系统会分配指定的额外窗口内存量并将其追加到窗口结构的末尾。应用程序可以使用此内存来存储特定于窗口的数据。
- 额外的内存是(自动)从系统的本地堆中分配的,其大小应该小于40个字节。如果请求的额外类内存量大于 40 字节, 则 RegisterClassEx 函数将失败。如果确实需要超过 40 个字节的额外类内存,则应自己申请内存,并将指向该内存的指针存储在额外的窗口内存中。
- SetWindowLong 函数可以将数据复制到额外的内存中。 GetWindowLong 函数可以从额外内存中检索值。
- 通过WNDCLASSEX 结构的 cbWndExtra 成员指定要分配的额外窗口内存量。不使用内存的应用程序必须将 cbWndExtra 初始化为零。