Windows桌面开发之窗口
日常使用windows操作系统,会打开很多个应用程序。每一个应用程序都有很多窗口,提供给用户操作。但是窗体这个概念其实是很深刻的一个概念,这篇文章从windows开发的角度,介绍窗体。
窗体可以理解成屏幕上的一块儿矩形区域,应用程序使用这片区域接收用户输入,并将结果输出到这块儿矩形区域。
一个应用程序启动时,必须至少有一个窗体作为主窗体。
注意,虽然操作系统可以同时打开多个窗口,但某一具体时刻,只能有一个窗口接收用户输入。
窗体提供以下能力
- Functions
- 提供一些函数,可以创建、销毁、移动、拉伸窗体
- 常见的API
- CreateWindow, CloseWindow, DestroyWindow
- MoveWindow, SetWindowPos
- Messages
- 提供一些消息,发送这些消息给窗体,可以获得窗体的一些信息,也可以改变窗体的一些设置
- 常见的消息
- WM_GETTEXT
- WM_SETTEXT
- Notifications
- 窗体自身发生改变,主动发送消息,告知应用程序。例如窗体被移动、拉伸时,会发送消息
- 常见的消息
- WM_CREATE, WM_CLOSE, WM_DESTROY
- WM_MOVE, WM_MOVING
- WM_SIZE, WM_SIZING
窗体具有一个唯一标识符,通过这标识符(integer identifier),可以找到窗体,并向其发送消息,指示其活动。
窗体的结构图
窗体的位置始终基于屏幕的左上角定位
窗体重要概念
- 窗体句柄
- 创建完窗体之后,函数返回一个句柄,用来唯一标识此窗体
- 窗体句柄的类型是HWND
- 通过方法FindWindow可以找到符合条件的所有窗体的句柄
- 通过方法IsWindow可传入句柄,判断是否指向一个合法窗体
窗体类
- 窗体类像是模板一样,构造一个新的窗体只能从已经注册过的类去构建。但是可以注册自定义的窗体类。
- 窗体类分为以下三大类
-
-
- 系统类
- 应用程序启动时,系统类自动复制到当前应用程序,供应用程序使用,但不是所有类都能创建窗体
- 这些系统类可以使用
-
- Button
- ComboBox
- Edit
- ListBox
- MDIClient
- ScrollBar
- Static
- 应用全局类
- 系统类
-
-
- 一个dll使用RegisterClassEx方法,并添加CS_GLOBALCLASS样式,不需要时使用UnregisterClass方法
- 应用本地类
- 注册它的模块销毁时,此类自动被销毁
-
-
应用顺序
- 先在应用本地类列表中查找
- 如未找到,再到应用全局类查找
- 如未找到,再到系统类中查找
窗体类的属性
- Class Name
- 使用类名作为唯一区分的标志,WNDCLASSEX结构中的lpszClassName
- 一个进程内部,类名是唯一的
- Window Procedure Address
- 系统把消息发到这里,窗体需要处理,例如绘制客户端区域内容
- 例子,一个简单的处理消息函数
-
LRESULT CALLBACK MainWndProc(HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam) // second message parameter { switch (uMsg) { case WM_CREATE: // Initialize the window. return 0; case WM_PAINT: // Paint the window's client area. return 0; case WM_SIZE: // Set the size and position of the window. return 0; case WM_DESTROY: // Clean up window-specific data objects. return 0; default: return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; }
- Instance Handle
- WNDCLASSEX结构中的hInstance,用来指向应用程序
- Class Cursor
- 鼠标移动进入窗体的客户区域时,cursor变成什么样子
- 先使用LoadCursor方法加载特定的游标,然后设置WNDCLASSEX结构中的hInstance
- 也可以在接收到WM_MOUSEMOVE消息的时候,调用SetCursor方法改变cursor
- Class Icons
- 图标是指切换当前应用的缩略图和任务栏下面显示的图片
- Class Background Brush
- Class Menu
- Class Styles
- WNDCLASSEXA结构
- cbSize
- style
- lpfnWndProc
- cbClsExtra
- cbWndExtra
- hInstance
- hIcon
- hCursor
- hbrBackground
- lpszMenuName
- lpszClassName
- hIconSm
- Classes and Device Contexts
- Extra Class Memory
- Extra Window Memory
窗体的分类
应用程序可以同时打开多个窗体,窗体之间的关系如下:- Overlapped Window
- 顶级窗体,作为应用程序的主窗体。可以在使用 CreateWindowEx 方法构造窗体时,设置 WS_OVERLAPPED,或者WS_OVERLAPPEDWINDOW的样式,来构造这种窗体。
- Popup Window
- 这是一种特殊的Overlapped Window,显示在主窗体之外的临时窗体。通常用做对话框或者消息框。
- Child Window
- 应用程序除过Overlapped Window,剩下的就是Child Window。这种窗体必须具有父窗体。
- 与此同时,它们没有顶级窗体所具有的Title Bar Min Max Button,只有Client区域。 它具有的样式是WS_CHILD。
窗体之间的关系
- 父子关系
- 一个应用首先打开顶级窗体,随后打开子窗体,子窗体可能又会打开子窗体。它们之间的关系就是父子关系。
- Parent窗体和Child窗体具有如下关系:
- 一个父窗体可以拥有多个子窗体,但子窗体只能属于一个父窗体。如果将当前窗体的父窗体指定为空,它将脱离应用程序,成为桌面的子窗体。
- 可以通过GetParent,SetParent 修改当前窗体的父窗体。
- EnumChildWindows可以用来枚举当前窗体的直接子窗体,子窗体可以继续枚举属于它的子窗体。
- 父窗体将会裁剪子窗体超出自己可视区域的部分
- 行为特点
- 父窗体可见时,子窗体才有可能可见。并且是父窗体先显示出来,接着子窗体才显示出来
- 父窗体隐藏时,先把所有子窗体都隐藏掉,最后再隐藏父窗体
- 父窗体移动时,子窗体跟着移动,移动完之后子窗体重新绘制内容区域,子窗体永远基于父窗体左上角定位
- 父窗体z-order增大时,子窗体的z-order也跟着增大
- 父窗体销毁时,优先销毁掉所有子窗体,然后再销毁父窗体
- 父窗体通过给子窗体发送消息,使得子窗体状态改变,从而产生变化。
- 子窗体与父窗体重叠的区域,windows只会把消息发送子窗体,除非子窗体被禁用。反过来,子窗体在自身状态改变时,又会主动发消息给父窗体,告知其状态变更,以便父窗体采取行动。
- 没有父窗体的窗体叫做顶级窗体,本质上来说,顶级窗体的父窗体就是桌面窗体,通过EnumWindows方法可以找到所有屏幕上的顶级窗体
- 从属关系
- 两个窗体之间还可以建立拥有和从属的关,关系服从以下约束
- 被拥有者的z-order比拥有者大
- 拥有者销毁时,被拥有者也自动被销毁
- 拥有者隐藏时,被拥有者也被隐藏
- 只有Overlapped Window可以拥有窗体
- 前台与后台关系
- 用户正在操作的那个窗口,处于激活状态,是前台窗口,反之就是后台窗口
- 用户可以通过 Alt + Tab 切换前台窗口
- 可以通过API GetForegroundWindow,SetForegroundWindow 获取和设置前台窗口
- 位置关系
- 操作系统维护z-order列表,z-order大的窗口将遮挡住z-order小的窗口
- 可以通过API移动z-order,BringWindowToTop, SetWindowPos, DeferWindowPos
- 用户也可以通过激活窗口,调整z-order
窗体状态
- Acitve
- 被激活的顶级窗口,也是前台窗口
- Disabled
- EnableWindow 用来禁用一个窗口
- IsWindowEnabled 用来判断一个窗口是否是禁用状态
- 当父窗体禁用时,无法获得键盘焦点。即使此时子窗体已经获得键盘焦点,也会立刻失去
- Visible
- 通过设置 WS_VISIBLE 样式来控制,设置完这个样式,系统先发送WM_SHOWWINDOW消息给窗体,接着显示窗体
- IsWindowVisible可以用来检测窗体是否可见,方法ShowWindow可以控制窗体显示隐藏
- 可见不一定意味着屏幕中能看到它,有可能此窗体被其它窗体遮挡,也有可能它的父窗体不可见,还有可能是它的位置在屏幕外侧
- Minimized
- 拥有WS_MAXIMIZE 样式
- Maximized
- 拥有WS_MINIMIZE 样式
- 关闭就是最小化,因此可以调用CloseWindow来使得窗体最小化
- Restored
- 恢复到原先的大小和位置
小贴士
系统在执行最大化、最小化、恢复之前,会先发送WM_QUERYOPEN 消息给窗体,如果窗体处理函数返回false,则不执行这个命令构造窗体时,通过指定CW_USEDEFAULT样式,让系统决定窗体位置和大小策略如下:针对于顶级窗体,如果当前应用程序还没有构造过一个顶级窗体,那么就基于屏幕的左上角进行定位。如果已经构造过一个顶级窗体,就根据这个顶级窗体的左上角进行定位。宽高也是,如果已经构造过顶级窗体,则使用这个顶级窗体的宽高。如果为构造过,则系统计算一个合适的宽高应用给这个顶级窗体。对于子窗体或者弹出式窗体,系统会给一个默认最小的宽高。系统检测到用户点击了最大化、最小化等等按钮时,发送WM_SYSCOMMAND消息,并且能够拿到以下commandSC_CLOSE SC_MAXIMIZE SC_MINIMIZE SC_MOVE SC_RESTORE SC_SIZE窗体大小位置
可以使用以下方法操作窗体- SetWindowPlacement 窗体最大化、最小化的位置、恢复时的位置和大小、窗体状态
- MoveWindow 改变窗体位置
- SetWindowPos 改变窗体位置,同时还可以改变窗体的显示状态
- GetWindowRect 返回窗体的矩形区域大小,所有窗体都是基于屏幕左上角
- ScreenToClient 和 MapWindowPoints 可以求出相对于父窗体的位置信息
- GetClientRect 返回窗体的可视区域,位置始终是0,0,宽高是有效的
窗体改变时,系统发送以下消息给窗体- WM_GETMINMAXINFO 设置了WS_THICKFRAME和WS_CAPTION样式,当窗体被用户最大化、最小化等操作时,系统发送此消息
- WM_WINDOWPOSCHANGING 窗体位置、大小、z-order、显示状态即将发生改变时,系统发送此消息
- 在这个时机,可以从消息中拿出WINDOWPOS,进行更改
- WM_WINDOWPOSCHANGED 窗体位置、大小、z-order、显示状态已经发生改变时,系统发送此消息
- 这个时机,调用DefWindowProc方法,将WM_WINDOWPOSCHANGED消息传递进去,系统接着会发送WM_SIZE和WM_MOVE消息
窗体构造与销毁
- 通过DestroyWindow 方法销毁
- 销毁时,先发送WM_DESTROY消息给当前窗体,接着发送给子窗体。随后销毁当前窗体和子窗体