【网络程序设计】Windows 窗口编程

550 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情


相关原理

1. 窗口和句柄

窗口是屏幕上的一块矩形区域,是 Windows 应用程序与用户进行交互的接口。利用窗口可以接收用户的输入、以及显示输出。

在 Windows 应用程序中, 窗口是通过窗口句柄( HWND) 来标识的。

句柄( HANDLE) 是 Windows 程序中一个重要的概念, 在 Windows 程序中, 有各种各样的资源( 窗口、 图标、光标,画刷等), 系统在创建这些资源时会为它们分配内存, 并返回标识这些资源的标识号, 即句柄。

 2.消息驱动机制

Windows 程序设计是一种完全不同于传统的 DOS 方式的程序设计方法。它是一种事件驱动方式的程序设计模式,主要是基于消息的。

每一个 Windows 应用程序开始执行后,系统都会为该程序创建一个消息队列, 这个消息队列用来存放该程序创建的窗口的消息。

例如,当用户在窗口中画图的时候,按下鼠标左键,此时,操作系统会感知到这一事件,于是将这个事件包装成一个消息,投递到应用程序的消息队列中,等待应用程序的处理。然后应用程序通过一个消息循环不断地从消息队列中取出消息,并进行响应。

在这个处理过程中,操作系统也会给应用程序“ 发送消息”。所谓“ 发送消息”,实际上是操作系统调用程序中一个专门负责处理消息的函数,这个函数称为窗口过程。

 3.消息处理过程

①在设计窗口类的时候,将窗口过程函数的地址赋值给lpfnWndProc成员变量;

②调用RegisterClass(&wndclass)注册窗口类,那么系统就有了我们所编写的窗口过程函数的地址。

③当应用程序接收到某一窗口的消息时,调用DispatchMessage(&msg)将对消息回传给系统。系统则利用先前注册窗口类时得到的函数指针,调用窗口过程函数对消息进行处理。

一个Windows程序可以包含多个窗口过程函数,一个窗口过程总是与某一个特定的窗口类相关联(通过WNDCLASS结构体中的lpfnWndProc成员变量指定),基于该窗口类创建的窗口使用同一个窗口过程。

image.png

4.多线程运行机制

Windows中一个进程可以包含多个线程,由多个线程组成。

在Windows应用程序中,窗体是由一种称为“UI线程(User Interface Thread)”的特殊类型的线程创建的。一个UI线程包含一个消息循环队列。在窗口运行过程中,按下控件或对窗口进行一些操作等同于向消息循环队列插入消息;然后由系统调用相应消息的响应函数,使用户操作得到响应。

当线程调用函数来建立某个对象时,则该对象就归这个线程的进程所拥有。这样,当进程结束时,如果没有明确删除这个对象,则操作系统会自动删除这个对象。对窗口和挂钩( h o o k )这两种U s e r对象,它们分别由建立窗口和安装挂钩的线程所拥有。

如果一个线程建立一个窗口或安装一个挂钩,线程结束,操作系统会自动删除窗口或卸载挂钩。这意味:建立窗口的线程必须为窗口处理所有消息。这也意味着每个线程,如果至少建立了一个窗口,都由系统对它分配一个消息队列。这个队列用于窗口消息的派送( d i s p a t c h)。为了使窗口接收这些消息,线程必须有它自己的消息循环。

5.windows编程模型

创建一个完整的窗口程序需要四个步骤:

1.设计一个窗口类

2.注册窗口类

3.创建窗口

4.显示及更新窗口


操作步骤

1. 应用程序中的MainWin Proc 函数

// 窗口函数的函数原形

LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY WinMain(HINSTANCE hInstance,

                     HINSTANCE hPrevInstance,

                     LPSTR     lpCmdLine,

                     int       nCmdShow)



char szClassName[] = "MainWClass";

WNDCLASSEX wndclass;

2.设计窗口类 填充WNDCLASSEX结构

// 用描述主窗口的参数填充WNDCLASSEX结构

wndclass.cbSize = sizeof(wndclass); // 结构的大小

wndclass.style = CS_HREDRAW|CS_VREDRAW; // 指定如果大小改变就重画

wndclass.lpfnWndProc = MainWndProc; // 窗口函数指针

wndclass.cbClsExtra = 0; // 没有额外的类内存

wndclass.cbWndExtra = 0; // 没有额外的窗口内存

wndclass.hInstance = hInstance; // 实例句柄

wndclass.hIcon = ::LoadIcon(NULL,

IDI_APPLICATION); // 使用预定义图标

wndclass.hCursor = ::LoadCursor(NULL,

IDC_ARROW); // 使用预定义的光标

wndclass.hbrBackground = (HBRUSH)

::GetStockObject(WHITE_BRUSH); // 使用白色背景画刷

wndclass.lpszMenuName = NULL; // 不指定菜单

wndclass.lpszClassName = szClassName ; // 窗口类的名称

wndclass.hIconSm = NULL; // 没有类的小图标

3.注册窗口类 设计RegisterClassEx()函数

// 注册这个窗口类

::RegisterClassEx(&wndclass);

4.创建主窗口 设计CreateWindowEx()函数

// 创建主窗口

HWND hwnd = ::CreateWindowEx(

0, // dwExStyle,扩展样式

szClassName, // lpClassName,类名

"My first Window!", // lpWindowName,标题

WS_OVERLAPPEDWINDOW, // dwStyle,窗口风格

CW_USEDEFAULT, // X,初始 X 坐标

CW_USEDEFAULT, // Y,初始 Y 坐标

CW_USEDEFAULT, // nWidth,宽度

CW_USEDEFAULT, // nHeight,高度

NULL, // hWndParent,父窗口句柄

NULL, // hMenu,菜单句柄

hInstance, // hlnstance,程序实例句柄

NULL) ; // lpParam,用户数据

5.显示并更新窗口,设计ShowWindow()和UpdateWindow()函数

// 显示窗口,刷新窗口客户区

::ShowWindow(hwnd, nCmdShow);

::UpdateWindow(hwnd);

6.实现消息循环,从消息队列中取出消息交给窗口过程函数处理,设计GetMessage函数

// 从消息堆中取出消息

MSG msg;

while(::GetMessage(&msg, NULL, 0, 0))

{

// 转化键盘消息

::TranslateMessage(&msg);

// 将消息发送到相应的窗口函数

::DispatchMessage(&msg);

}

// 当GetMessage返回0时程序结束

return msg.wParam;

}

7.实现窗口过程函数,查看MainWndProc函数

LRESULT CALLBACK MainWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{

char szText[] = "最简单的窗口程序!";

switch (message)

{  

case WM_PAINT: // 窗口客户区需要重画

{

HDC hdc;

PAINTSTRUCT ps;

 

// 使无效的客户区变的有效,并取得设备环境句柄

hdc = ::BeginPaint (hwnd, &ps) ;

// 显示文字

::TextOut(hdc, 10, 10, szText, strlen(szText));

::EndPaint(hwnd, &ps);

return 0;

}

case WM_DESTROY: // 正在销毁窗口

// 向消息队列投递一个WM_QUIT消息,促使GetMessage函数返回0,结束消息循环

::PostQuitMessage(0) ;

return 0 ;

}

// 将我们不处理的消息交给系统做默认处理

return ::DefWindowProc(hwnd, message, wParam, lParam);

}

流程解析

image.png


操作结果

image.png

image.png


一些解答

消息处理函数中处理的消息值是什么?引发该消息的事件是什么?

 

Windows程序是事件驱动的,每个窗口都有一个消息处理函数。在消息处理函数中,处理的消息值是传入的消息。系统内还有它自己的缺省消息处理函数。

客户写一个消息处理函数,在窗口建立前,将消息处理函数与窗口关联。这样,每当有消息产生时,就会去调用这个消息处理函数。通常情况下,客户都不会处理全部的消息,而是只处理自己感兴趣的消息,其他的,则送回到系统的缺省消息处理函数中去。

系统会将针对这个程序的消息依次放到程序的“消息队列”中,由程序自己依次取出消息,在分发到对应的窗口中去。
因此,建立窗口后,将进入一个循环。
在循环中,取出消息、派发消息,循环往复,直到取得的消息是退出消息。