窗口、线程、超类、子类

转自:http://szzhliu.blog.163.com/blog/static/3513089520104773355152/

这篇文章本来只是想介绍一下子类化和超类化这两个比较“生僻”的名词。为了叙述的完整性而讨论了Windows的窗口和消息,也简要讨论了进程和线程。子类化(Subclassing)和超类化(Superclassing)是伴随Windows窗口机制而产生的两个复用代码的方法。不要把“子类化、超类化”与面向对象语言中的派生类、基类混淆起来。“子类化、超类化”中的“类”是指Windows的窗口类。

0 运行程序
希望读者在阅读本节前先看看"谈谈Windows程序中的字符编码"开头的第0节和附录0。第0节介绍了Windows系统的几个重要模块。附录0概述了Windows的启动过程,从上电到启动Explorer.exe。本节介绍的是运行程序时发生的事情。

0.1 程序的启动
当我们通过Explorer.exe运行一个程序时,Explorer.exe会调用CreateProcess函数请求系统为这个程序创建进程。当然,其它程序也可以调用CreateProcess函数创建进程。

系统在为进程分配内部资源,建立独立的地址空间后,会为进程创建一个主线程。我们可以把进程看作单位,把线程看作员工。进程拥有资源,但真正在CPU上运行和调度的是线程。系统以挂起状态创建主线程,即主线程创建好,不会立即运行,而是等待系统调度。系统向Win32子系统的管理员csrss.exe登记新创建的进程和线程。登记结束后,系统通知挂起的主线程可以运行,新程序才开始运行。

这时,在创建进程中CreateProcess函数返回;在被创建进程中,主线程在完成最后的初始化后进入程序的入口函数(Entry-point)。创建进程与被创建进程在各自的地址空间独立运行。这时,即使我们结束创建进程,也不会影响被创建进程。

0.2 程序的执行
可执行文件(PE文件)的文件头结构包含入口函数的地址。入口函数一般是Windows在运行时库中提供的,我们在编译时可以根据程序类型设定。在VC中编译、运行程序的小知识点讨论了Entry-point,读者可以参考。

入口函数前的过程可以被看作程序的装载过程。在装载时,系统已经做过全局和静态变量(在编译时可以确定地址)的初始化,有初值的全局变量拥有了它们的初值,没有初值的变量被设为0,我们可以在入口函数处设置断点确认这一点。

进入入口函数后,程序继续运行环境的建立,例如调用所有全局对象的构造函数。在一切就绪后,程序调用我们提供的主函数。主函数名是入口函数决定的,例如main或WinMain。如果我们没有提供入口函数要求的主函数,编译时就会产生链接错误。

0.3 进程和线程
我们通常把存储介质(例如硬盘)上的可执行文件称作程序。程序被装载、运行后就成为进程。系统会为每个进程创建一个主线程,主线程通过入口函数进入我们提供的主函数。我们可以在程序中创建其它线程。

线程可以创建一个或多个窗口,也可以不创建窗口。系统会为有窗口的线程建立消息队列。有消息队列的线程就可以接收消息,例如我们可以用PostThreadMessage函数向线程发送消息。

没有窗口的线程只要调用了PeekMessage或GetMessage,系统也会为它创建消息队列。

1 窗口和消息
1.1 线程的消息队列
每个运行的程序就是一个进程。每个进程有一个或多个线程。有的线程没有窗口,有的线程有一个或多个窗口。

我们可以向线程发送消息,但大多数消息都是发给窗口的。发给窗口的消息同样放在线程的消息队列中。我们可以把线程的消息队列看作信箱,把窗口看作收信人。我们在向指定窗口发送消息时,系统会找到该窗口所属的线程,然后把消息放到该线程的消息队列中。

线程消息队列是系统内部的数据结构,我们在程序中看不到这个结构。但我们可以通过Windows的API向消息队列发送、投递消息;从消息队列接收消息;转换和分派接收到的消息。

1.2 最小的Windows程序
Windows的程序员大概都看过这么一个最小的Windows程序:

// 例程1

#include "windows.h"

static const char m_szName[] = "窗口";

 

// 主窗口回调函数 如果直接用 DefWindowProc, 关闭窗口时不会结束消息循环

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

switch (uMsg)

{

case WM_DESTROY:   PostQuitMessage(0); // 关闭窗口时发送WM_QUIT消息结束消息循环  

break;

default:   return DefWindowProc(hWnd, uMsg, wParam, lParam);

}

return 0;

}

 

// 主函数

int __stdcall WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow)

{

 WNDCLASS wc;

memset(&wc, 0, sizeof(WNDCLASS));

wc.style = CS_VREDRAW|CS_HREDRAW;

wc.lpfnWndProc = (WNDPROC)WindowProc;

wc.hCursor = LoadCursor(NULL, IDC_ARROW);

wc.hbrBackground = (HBRUSH)(COLOR_WINDOW);

wc.lpszClassName = m_szName;

RegisterClass(&wc);   // 登记窗口类

HWND hWnd;

hWnd = CreateWindow(m_szName,m_szName,WS_OVERLAPPEDWINDOW,100,100,320,240,   NULL,NULL,hInstance,NULL); // 创建窗口

ShowWindow(hWnd, nCmdShow);   // 显示窗口

MSG sMsg;

while (int ret=GetMessage(&sMsg, NULL, 0, 0))

{  

// 消息循环  

if (ret != -1)

{   

TranslateMessage(&sMsg);   

DispatchMessage(&sMsg);  

}

}

return 0;

}

 

这个程序虽然只显示一个窗口,但经常被用来说明Windows程序的基本结构。在MFC框架内部我们同样可以找到类似的程序结构。这个程序包含以下基本概念:

窗口类、窗口和窗口过程  消息循环
下面分别介绍。

1.3 窗口类、窗口和窗口过程
创建窗口时要提供窗口类的名字。窗口类相当于窗口的模板,我们可以基于同一个窗口类创建多个窗口我们可以使用Windows预先登记好的窗口类。但在更多的情况下,我们要登记自己的窗口类。在登记窗口类时,我们要登记名称、风格、图标、光标、菜单等项,其中最重要的就是窗口过程的地址。

窗口过程是一个函数。窗口收到的所有消息都会被送到这个函数处理。那么,发到线程消息队列的消息是怎么被送到窗口的呢?

1.4 消息循环
熟悉嵌入式多任务程序的程序员,都知道任务(相当于Windows的线程)的结构基本上都是:

while (1) {   等待信号;   处理信号; }   任务收到信号就处理,否则就挂起,让其它任务运行。这就是消息驱动程序的基本结构。Windows程序通常也是这样:

while (int ret=GetMessage(&sMsg, NULL, 0, 0)) {   // 消息循环   if (ret != -1) {    TranslateMessage(&sMsg);    DispatchMessage(&sMsg);   } }

GetMessage从消息队列接收消息;TranslateMessage根据按键产生WM_CHAR消息,放入消息队列DispatchMessage根据消息中的窗口句柄将消息分发到窗口,即调用窗口过程函数处理消息

1.5 通过消息通信
创建窗口的函数会返回一个窗口句柄。窗口句柄在系统范围内(不是进程范围)标识一个唯一的窗口实例。通过向窗口发送消息,我们可以实现进程内和进程间的通信。

我们可以用SendMessage或PostMessage向窗口发送或投递消息。SendMessage必须等到目标窗口处理过消息才会返回。我试过:如果向一个没有消息循环的窗口SendMessage,SendMessage函数永远不会返回。PostMessage在把消息放入线程的消息队列后立即返回。

其实只有投递的消息才是通过DispatchMessage分派到窗口过程的。通过SendMessage发送的消息,在线程GetMessage时,就已经被分派到窗口过程了,不经过DispatchMessage。

1.5.1 窗口程序与控制台程序的通信实例
大家是不是觉得“例程1”没什么意思,让我们用它来做个小游戏:让“例程1”和一个控制台程序做一次亲密接触。我们首先将“例程1”的窗口过程修改为:

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

static DWORD tid = 0;

switch (uMsg)

{

case WM_DESTROY:  

PostQuitMessage(0); // 关闭窗口时发送WM_QUIT消息结束消息循环  

break;

case WM_USER:  

tid = wParam; // 保存控制台程序的线程ID  

SetWindowText(hWnd, "收到");  

break;

case WM_CHAR:  

if (tid)

{   

  switch(wParam)

    {   

      case '1':     PostThreadMessage(tid, WM_USER+1, 0, 0); // 向控制台程序发送消息1    

      break;   

      case '2':     PostThreadMessage(tid, WM_USER+2, 0, 0); // 向控制台程序发送消息2    

      break;   

    }  

}  

break;

default:  

return DefWindowProc(hWnd, uMsg, wParam, lParam);

}

return 0;

}然后,我们创建一个控制台程序,代码如下:

#include "windows.h

"#include "stdio.h"

static HWND m_hWnd = 0;

void process_msg(UINT msg, WPARAM wp, LPARAM lp)

{

char buf[100];

static int i = 1;

if (!m_hWnd)

{   return; }

switch (msg)

{

case WM_USER+1:  

SendMessage(m_hWnd, WM_GETTEXT, sizeof(buf), (LPARAM)buf);  

printf("你现在叫:%s\n\n", buf);   // 读取、显示对方的名字  

break;

case WM_USER+2:  

sprintf(buf, "我是窗口%d", i++);  

SendMessage(m_hWnd, WM_SETTEXT, sizeof(buf), (LPARAM)buf); // 修改对方名字  

printf("给你改名\n\n");  

break;

}

}

 

int main()

{

  MSG sMsg;

  printf("Start with thread id %d\n", GetCurrentThreadId());

  m_hWnd = FindWindow(NULL,"窗口");

  if (m_hWnd)

  {  

    printf("找到窗口%x\n\n", m_hWnd);  

    SendMessage(m_hWnd, WM_USER, GetCurrentThreadId(), 0);

 }

else

{  

  printf("没有找到窗口\n\n");

}

while (int ret=GetMessage(&sMsg, NULL, 0, 0))

{  

    // 消息循环  

    if (ret != -1)

    {   

        process_msg(sMsg.message, sMsg.wParam, sMsg.lParam);  

    }

}

return 0;

}

大家能看懂这游戏怎么玩吗?首先运行“例程1”wnd,然后运行控制台程序msg。msg会找到wnd的窗口,并将自己的主线程ID发给wnd。wnd收到msg的消息后,会显示收到。这时,wnd和msg已经建立了通信的渠道:wnd可以向msg的主线程发消息,msg可以向wnd的窗口发消息。

我们如果在wnd窗口按下键'1',wnd会向msg发送消息1,msg收到后会通过WM_GETTEXT消息获得wnd的窗口名称并显示。我们如果在wnd窗口按下键'2',wnd会向msg发送消息2,msg收到后会通过WM_SETTEXT消息修改wnd的窗口名称。

这个小例子演示了控制台程序的消息循环,向线程发消息,以及进程间的消息通信。

1.5.2 地址空间的问题
不同的进程拥有独立的地址空间,如果我们在消息参数中包含一个进程A的地址,然后发送到进程B。进程B如果在自己的地址空间里操作这个地址,就会发生错误。那么,为什么上例中的WM_GETTEXT和WM_SETEXT可以正常工作?

这是因为WM_GETTEXT和WM_SETEXT都是Windows自己定义的消息,Windows知道参数的含义,并作了特殊的处理,即在进程B的空间分配一块内存作为中转,并在进程A和进程B的缓冲区之间复制数据。例如:在1.5.1节的例子中,如果我们设置断点观察,就会发现msg发送的WM_SETTEXT消息中的lParam不等于wnd接收到的WM_SETTEXT消息中的lParam。

如果我们在自己定义的消息中传递内存地址,系统不会做任何特殊处理,所以必然发生错误。

Windows提供了WM_COPYDATA消息用来向窗口传递数据,Windows同样会为这个消息作特殊处理。

在进程间发送这些需要额外分配内存的消息时,我们应该用SendMessage,而不是PostMessage。因为SendMessage会等待接收方处理完后再返回,这样系统才有机会额外释放分配的内存。在这种场合使用PostMessage,系统会忽略要求投递的消息,读者可以在msg程序中试验一下。

2 子类化和超类化
窗口类是窗口的模板,窗口是窗口类的实例。窗口类和

暂)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值