7.Windows消息处理机制

如果用过C++搞过windows上的程序设计的话 对于windows的消息机制 估计大部分的人都知道 但是由于这边文章是写在C#中的 所以估计接触过的人并不是很多(- -!、、说得来感觉自己知道好多似得)、、

在windows上应用程序是基于事件(消息)驱动的 由系统或者其他应用去通知应用程序执行相关程序 比如我在窗体上点击一个按钮的时候系统就会去通知应用程序执行button_click事件

来一个抽象一点的说明 消息暂时理解为手机短信的那个消息 在系统中运行着的程序是员工 windows是boss 所有的下属都听着boss的指挥 既然要指挥那必然得通过某种方式来指挥 而这个方式就是【消息】、、当boss在办公司各种忙碌 然后要通知某个下属做某事 就给他发一条短信过去 下属收到这个短信就老老实实的工作、、当然boss可能一次发送好几条短信到下属手机里面 然后下属一个一个的去完成 所以还有信箱这个概念 Windows中这个叫做【消息队列】 系统不停的发送消息到【目标程序】的消息队列 然后【目标程序】在自己的消息队列中取出去消息完成相应工作 所以每个程序都会有自己的消息队列用来储存系统发来的消息【进程消息队列】(很多人并不认同进程消息队列一说 因为进程只是一个容器 线程才是执行单元 被他们说成线程消息队列 而一些人则把进程消息队列称之为程序所有线程的消息队列总称) 然后系统自身也有一个【系统消息队列】就好比boss也需要接受别人比如合伙人的短信通知 而boss再把这些事情交给相应的下属去处理

假设现在我在某个程序上点击键盘或者鼠标啥的 我这一系列的动作最先被系统接收 系统一个一个的处理我的这些动作 然后再把这个动作封装成消息投递给目标程序的消息队列 比如在窗体上点下按键就会产生一个WM_KEYDOW消息 点下鼠标就会产生WM_LBUTTONDOWN/WM_RBUTTONDOWN(L左 R右)消息 然后这些消息在.NET中被封装成了KeyDown和MouseDown事件 而我们写程序就只需要绑定这些事件就可以了

在程序中这个【消息】是长这样的

typedef struct tagMSG {
    HWND    hwnd;       //由哪个窗口来接收此消息 
    UINT    message;    //消息的类型(WM_LBUTTONDOWN等)
    WPARAM  wParam;     //消息附加参数 根据消息类型决定值
    LPARAM  lParam;     //消息附加参数 根据消息类型决定值
    DWORD   time;       //消息投递时间
    POINT   pt;         //消息投递时光标位于屏幕上的位置
} MSG, *PMSG; 

被保存在一个MSG结构中 boss需要下属做什么的时候就发送一条这样的消息过去

这里需要注意的是上面的W/LParam两个参数 他是消息的附带参数 什么意思 假设我现在在我的窗体上鼠标左键点下 那么就会产生一个WM_LBUTTONDOWN消息 在.NET中就会触发MouseDown事件 而事件中有一个事件参数e 通过e.Location可以获取到鼠标点下的位置 那么这个坐标信息事件是怎么知道的?这些附带信息就保存在这两个附加参数中 下面是一个WM_LBUTTONDOWN消息的说明

坐标信息被保存在LParam中(W/LParam是一个四字节的数字)低位两字节保存X坐标高位Y坐标

而上面是一个KeyDown消息的说明 附带参数保存的就是按键的编码之类的信息了 当然如果是一些其他类型的消息 并不需要附带参数 可能这两个就用不到了 所以说这两个参数的值是更具uMsg来决定的

这里直接上一个C++的一段代码上来 这是一段再经典不过的Hello World了 一个从创建一个窗口到显示并处理一些消息的整个过程:

//消息处理函数
LRESULT CALLBACK WndProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam){
    HDC hdc;
    RECT rect;
    switch (uMsg)       //所有感兴趣的消息 自己处理
    {
    case WM_PAINT:      //绘制窗口消息
        PAINTSTRUCT ps;
        hdc = BeginPaint(hWnd,&ps);
        GetClientRect(hWnd,&rect);
        DrawText(hdc,"博主最帅",-1,&rect,DT_SINGLELINE | DT_VCENTER | DT_CENTER);
        EndPaint(hWnd,&ps);
        return 0;
    case WM_CLOSE:      //关闭窗口消息
        if(MessageBox(hWnd,"Exit?","Question",MB_YESNO | MB_ICONQUESTION) == IDYES){
            PostQuitMessage(NULL);   //WinMain中的GetMessage将返回0从而跳出循环
        }
        return 0;
    default:            //不感兴趣的消息 就调用默认处理
        return DefWindowProc(hWnd,uMsg,wParam,lParam);
    }
}

int WINAPI WinMain(     //程序入口 main 函数
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nShowCmd){
        MSG msg;
        HWND hWnd;
        WNDCLASS wc;                //窗口类 包含一些窗体样式
        memset(&wc,0,sizeof(wc));
        wc.hInstance	    = hInstance;
        wc.hbrBackground    = (HBRUSH)(COLOR_WINDOW);
        wc.hIcon            = LoadIcon(NULL,IDI_APPLICATION);
        wc.hCursor          = LoadCursor(NULL,IDC_ARROW);
        wc.lpfnWndProc      = WndProc;             //窗口过程 消息处理函数
        wc.lpszClassName    = "WinMain";           //取一个类名
        wc.style            = CS_HREDRAW | CS_VREDRAW;
        if(!RegisterClass(&wc)){    //组册窗口类
            MessageBox(NULL,"RegisterClass Error","Error",MB_OK);
            return 0;
        }
        hWnd = CreateWindow(        //创建窗口
            "WinMain","Crystal_lz", //窗口类名(上面wc.lpszClassName="WinMain")和标题
            WS_OVERLAPPEDWINDOW,    //窗口样式  一个普通的窗口
            CW_USEDEFAULT,CW_USEDEFAULT,250,250,    //窗口初始坐标及尺寸
            NULL,NULL,hInstance,NULL);
        if(!hWnd){
            MessageBox(NULL,"CreateWindow Error","Error",MB_OK);
            return 0;
        }
        ShowWindow(hWnd,nShowCmd);  //显示窗口
        UpdateWindow(hWnd);         //首次刷新窗口
        while (GetMessage(&msg,NULL,0,0))   //进入消息循环 如果消息队列没有消息会阻塞而不是返回false
        {
            TranslateMessage(&msg); //翻译消息 用于转换虚拟键
            DispatchMessage(&msg);  //投递消息给窗口处理过程WndProc [wc.lpfnWndProc = WndProc]
        }
        return 0;
}

注意上面PostQuitMessage会让GetMessage返回false从而跳出循环 然后整个应用程序退出 因为Main函数结束 .NET也是一样Main退出 整个程序退出 而在.NET中的Main是这样的

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}
通过Application.Run来进入一个消息循环 所以说Form1一关闭整个程序退出 所以同理上面我C++创建出来的那个窗体等同于Form1是整个应用程序的主窗体

如果看不懂C++的代码木有事情 看上去语法和C#的也差不了多少只是估计看着那些风骚的大写有些不习惯 在前面几篇中常量和结构的时候已经见识过这样的大写了 MSG WNDCLASS是结构 而WM_PAINT WM_CLOSE是常量 表示一个消息 还有估计看着写出来的函数似乎有些别扭 因为通常理解的不都是【返回类型 函数名(参数列表){}】而这里返回类型后面似乎多了一个 那个是调用约定 不知道的百度什么是调用约定 这里不多说 其实这个程序的运行效果是:

就是一个普通的窗口 然后中间画了一段文字 关闭的时候提示一下 然后没有别的了 非常简单 - -!、、估计你会觉得在C#中几句代码就搞定了 这里怎么这么多 - -!、的确 正如你看到的窗体都是自己用代码通过调用Win32Api创建的 在上面看到的所有的函数调用都是Win32的Api 所以你也可以用C#通过调用API写一个和上面一样的程序 根据前几篇的你会觉得C#调用都有申明的 这里为什么没有?因为这里省略了一句 最顶上的【#include <windows.h>】这个里面声明有、、- -!、、扯远了 继续回来  可以看到代码 主要有两大块 一个是WndProc 和入口WinMain

而在WinMain里面 首先是创建一个窗口类(WNDCLASS)wc然后给他各种赋值然后通过RegisterClass注册类 如果成功就用注册的类名调用CreateWindow函数来创建窗体 如果创建成功调用ShowWindow函数把窗体显示出来 显示出来后执行一次刷新调用UpdateWindow(你会不会在想在C#中直接一个new Form().Show()一个窗口就出来了?那只是帮你封装了而已)然后可以看到进入了一个循环【消息循环】 这里要说的就是这个循环 也就是进入了一个下属等待boss发消息来通知自己做什么的时候了 如果收到一个消息那么就处理 而在循环里面看到有两句代码 第一句暂且可以不用管 重点在于第二句 第二句就是将这个消息扔给WndProc这个函数去处理

什么意思呢 假设鼠标此时在窗体上左键点下 那么此时就产生了一个【左键点下WM_LBUTTOMDOWN】的消息 而这个消息保存在MSG结构中由GetMessage从自身的消息队列把这个消息取出 然后把这个消息由DispatchMessage投递给相应的消息处理函数WndProc 而在WndProc的四个参数其实就是MSG结构的信息 通过uMsg来判断是什么消息如果是WM_CLOSE表示用户在窗体上点了关闭按钮而产生的消息 如果是WM_LBUTTONDOWN就表示鼠标左键点下 所以在switch里面处理各种各样你感兴趣的消息 你不感兴趣的消息可以通过调用默认的消息处理函数(DefWindowProc)去处理

随便画了一张图来说明:

(- -!、、图片很丑 不要在意细节)先看最上面的首先有外部输入或者系统产生一个消息 比如现在在某个【程序A】上面点下左键 那么这个动作最先是被系统所接收的 然后系统判断出这个点击的动作是发生在【程序A】上面的 然后把这个点击的动作投递到【程序A】的消息队列中 然后被GetMessage取出这个消息投递给消息处理函数WndProc(每个窗口都有一个消息处理函数)进行处理 一个点击的动作就是这么被处理了的 当然有许许多多的各种消息 WM_XXXX 、、、

或许你会觉得奇怪 这咋讲到C++去了 不是说好的C#的么、、上面那些C++代码也没见过也看不懂啊这和C#有什么关系?因为如果用C#的代码来说我不知道要怎么描述 所以用点原始的方式从原理上来描述 而上面的代码就比较原始 从创建一个窗口到显示出来到处理一些动作的整个过程 虽然运行出来的程序功能很简陋 麻雀虽小五张俱全 无论你是C#还是VB什么的写出来的GUI应用程序 只要是运行在windows上的 必然都是上面的一个过程 在.NET中被封装了你没有看到而已 因为上面C++代码里面那些所谓的消息在.NET中以事件的形式给出了 比如上面的WM_PAINT在C#中有Paint事件 WM_CLOSE有FormClosing事件 所以你只知道事件的存在不知道消息这个东西 而这些事件背后的本质就是各种各样的消息 在.NET中其实也有上面你看到的那个WndProc函数 比如:

public partial class Form1 : Form
{
    public Form1() {
        InitializeComponent();
        this.FormClosing += (s, e) => MessageBox.Show("FormClosing");
    }

    private const uint WM_PAINT = 0x0F;
    private const uint WM_CLOSE = 0x10;

    protected override void WndProc(ref Message m) {
        switch ((uint)m.Msg) {
            case WM_PAINT:
                using (Graphics g = Graphics.FromHwnd(m.HWnd)) {
                    g.DrawString("博主最帅", this.Font, Brushes.Black, Point.Empty);
                }
                break;
            case WM_CLOSE:
                MessageBox.Show("WM_CLOSE");//消息优先于事件
                return; //直接返回不执行base你会发现你绑定的FormClosing事件没有执行
        }
        base.WndProc(ref m);
    }
}

看看这个代码是否似成相识?和上面C++的、、而这里的override的WndProc就等同于上面的C++中的WndProc(函数名字随意

下面是我反编译出来WndProc的代码

可以看到在case中有个0x200消息(WM_MOUSEMOVE) 而里面的代码是调用一个WmMouseMove的函数 跟踪进去发现调用的则是OnMouseMove 也就是.NET中的MouseMove事件(可能你看到我上面反编译的是Control的代码 算下来的话Form是集成至Control的 而且前面的文章中我也说了 Form和Control他们对于windows来说都是窗口 只是他们的样式(Class)不一样而已 你同样可以用上面的CreateWindow创建一个按钮出来)

刚才上面我提到了.NET通过Application.Run来进入主窗体的消息循环 那么就来反编译一下

最后一直跟下去发现了这个 调用了ThreadContext.LocalModalMessageLoop

所以我一直强调.Net封装的东西太多了 消息在.NET中变成了事件的形式 而在.NET中你也可以不使用Form这个类 自己封装一个Form类出来 当然你要写的代码如同上面C++那样的差不多 自己用Api什么的来创建窗体 然后处理消息封装成事件 - -!、、当然这个做法很蛋疼、、但你要知道这是怎么回事、、并不是说用C++是那样去写程序.NET这样去写程序 感觉上是不同的两个东西 但落实到原理上他们都是一样的 因为写出来的程序都是运行在Windows上面的 那就得按Windows的规矩来 那就是处理消息、、

如果你继续反编译 你会发现Control是由CreateWindowEx函数创建出来的

还有一点 上面看到代码中只有一个消息循环 并不是说一个程序只有一个消息循环 上面的只是一个主窗体的消息循环用来阻塞Main函数的 每一个窗口都会有一个消息循环 而窗口不只是窗体一个控件也是窗口

说道消息当然相关的API上面已经看了三个了 还有两个很常用的SendMessage和PostMessage这两个函数签名和用法上完全一样所以就只放一个的说明上来:

函数功能:
    该函数将指定的消息发送到一个或多个窗口。此函数为指定的窗口调用窗口程序,
    直到窗口程序处理完消息再返回。而函数PostMessage不同,将一个消息寄送到一个线程的消息队列后立即返回。
函数原型:
    LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM IParam);
参数:
hWnd:
    其窗口程序将接收消息的窗口的句柄。如果此参数为HWND_BROADCAST,
    则消息将被发送到系统中所有顶层窗口,包括无效或不可见的非自身拥有的窗口、
    被覆盖的窗口和弹出式窗口,但消息不被发送到子窗口。
Msg:指定被发送的消息。
wParam:指定附加的消息指定信息。
IParam:指定附加的消息指定信息。
返回值:返回值指定消息处理的结果,依赖于所发送的消息。
其实签名看上去和WndProc的处理函数的签名差不多 - -!、、那是当然WndProc是负责处理消息 而Send/PostMessage是负责发送一个消息来让WndProc处理的 就想寄包裹一样把东西给WndProc所以参数都一样

而Send和Post的却别在于Post只把消息投递到到【目标程序】的消息队列就返回不管了让【目标程序】的GetMessage自己获取去 而Send则要等到【目标程序】把消息处理了函数才返回

看到上面两个函数你可以知道 产生消息什么的并不是只有系统才能用的专利 自己的程序也可以用这两个函数去模拟消息的发送 当然消息就太多了WM_开头的都是(当然不止WM开头的) 比如下面的列子 给自己的窗体发送一个WM_CLOSE消息 也是可以关闭窗体:

public partial class Form1 : Form
{
    public Form1() {
        InitializeComponent();
        this.FormClosing += (s, e) => MessageBox.Show("FormClosing");
    }

    [DllImport("user32.dll")]
    public extern static int SendMessage(
                              IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);

    private const uint WM_CLOSE = 0x10;

    private void button1_Click(object sender, EventArgs e) {
        SendMessage(this.Handle, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
    }
}

对于消息你可以用Post/SendMessage来向别的程序或者自己的程序发送一个消息 消息的类型有很多 用的最多的就是WM开头的 可以根据文档查询相关的消息 比如下面列举一些消息的前缀

WM          General window(一般的窗口)
ABM         Application desktop toolbar (应用程序桌面工具条)
BM          Button control (按钮控件)
CB          Combo box control (组合框控件)
CBEM        Extended combo box control(扩展的组合框控件)
CDM         Common dialog box (普通的对话框)
DBT         Device (设备)
DL          Drag list box (下拉列表)
DM          Default push button control (默认按钮控件)
DTM         Date and time picker control(日期和时间选择控件)
EM          Edit control (编辑控件)
HDM         Header control (表头控件)
HKM         Hot key control (热键控件)
IPM         IP address control (IP地址控件)
LB          List box control  (列表框控件)
LVM         List view control (列表视图控件)
MCM         Month calendar control (数学日历控件)
PBM         Progress bar (进度条控件)
PGM         Pager control ()
PSM         Property sheet (属性页)
RB          Rebar control (分隔条控件)
SB          Status bar window (状态条控件)
SBM         Scroll bar control (滚动条控件)
STM         Static control (静态控件)
TB          Toolbar (工具条)
TBM         Trackbar (跟踪栏)
TCM         Tab control (选项卡控件)
。。。
当然消息不止上面这些 而且你还可以自定义消息Window有定义个常量WM_USER在c++代码中他是这样的
/*
 * NOTE: All Message Numbers below 0x0400 are RESERVED.
 *
 * Private Window Messages Start Here:
 */
#define WM_USER                         0x0400
msdn上这样说明的

WM_USER开始后面的数字都被保留用于用户自定义消息 比如我想定义一个自己消息我可以这样

public const uint WM_USER = 0x0400;
public const uint WM_MY = WM_USER + 1;

如果你用自定义消息那么就应该是从WM_USER开始 防止你定义的值和系统内定的那些值冲突 当然你自定义的消息别的程序 也不一定认识 自己承程序之间还是可以用的 比如两个程序都是自己的 需要跨进成通知另一个程序做什么事情的时候

虽然在.NET中消息被封装成了事件的 但并不是所有的消息都被封装成了事件 只有常用的是被封装了的 有些还是需要自己通过重写消息处理函数去处理一些消息 比如下一篇就来做一个需要重写消息处理函数的例子 还有如果你有需求希望你的代码优先级高于事件的时候 你也需要重写消息处理函数自己去处理、、


添加时间:2014-06-16 04:59:04 编辑时间:2016-12-02 17:47:16 阅读:3492 
C#Windows编程 C#Win32
Unblue - 2015-01-22 21:29:54
测试留言,博主牛人,看了一遍你博客,崇拜
  • 编写评论

      我觉得区分大小写是一个码农的基本素质
[访问统计] 今天:58 总数:262956 提示:未成年人 请在大人陪同下浏览本站内容 还有:世界上最帅的码农 -> 石头 RSS:http://www.st233.com/rss Powered by -> Crystal_lz