Windows 消息机制


Windows 和消息

消息和消息队列

名称 说明
消息和消息队列 本部分介绍消息和消息队列,以及如何在应用程序中使用它们。
关于消息和消息队列 本部分讨论 Windows 消息和消息队列。
使用消息和消息队列 以下代码示例演示如何执行与 Windows 消息和消息队列关联的以下任务。
消息引用 包含 API 引用。

Windows消息类型

根据消息的来源进行分类,可以分为:

根据消息的路由方式分类,可以分为:

Windows系统的整个消息系统分为3个层级

  1. Windows内核的系统消息队列

    Windows内核维护着一个全局的系统消息队列;按照线程的不同,系统消息队列中的消息会分发到应用程序的UI线程的消息队列中;

  2. 应用程序UI线程消息队列

    应用程序的每一个UI线程都有自己的消息循环,会不停地从自己的消息队列取出消息,并发送给Windows窗体对象(WinForm控件);

  3. 每个控件自己的消息处理函数

    WinForm 中的 void WndProc(ref Message m) 消息处理函数

img

img

img

img

img

在这里插入图片描述

Q&A

  1. 进程消息队列(应用程序消息队列)和主线程的消息队列是一回事吗?

在 WinForm 程序中,进程消息队列指的是主线程的消息队列。WinForm 应用程序是单线程的,所有的 UI 操作都必须在主线程中执行。当用户与应用程序的界面交互时,操作系统会将相关的消息发送到应用程序的消息队列中,然后主线程从消息队列中逐个获取并处理这些消息,以更新界面状态和响应用户操作。因此,进程消息队列实际上就是主线程的消息队列

  1. 线程中消息队列消息的结构是怎样的?
typedef struct tagMSG {
// 接收消息的窗口的句柄。 当消息是线程消息时,此成员为 NULL 。
HWND hwnd;
// 消息的标识符。 应用程序只能使用低字;高字由系统保留。
UINT message;
// 消息的附加信息。 确切含义取决于 消息 成员的值。
WPARAM wParam;
// 消息的附加信息。 确切含义取决于 消息 成员的值。
LPARAM lParam;
// 消息的发布时间。
DWORD time;
// 发布消息时的光标位置(以屏幕坐标表示)。
POINT pt;
//
DWORD lPrivate;
} MSG, *PMSG, *NPMSG, *LPMSG;

msg 结构 (winuser.h)

  1. 在WinForm程序中线程Id和托管线程Id是一回事吗?

线程Id和托管线程Id是不一样的!

可以通过以下方法获取线程的Id

/// <summary>
/// 获取操作系统线程ID,Windows 10下的偏移量为0x022C(x64-bit-Application)和0x0160(x32-bit-Application):
/// https://www.ibckf.com/questions/1679243
/// </summary>
/// <param name="thread"></param>
/// <returns></returns>
public static int GetNativeThreadId(Thread thread)
{
var f = typeof(Thread).GetField("DONT_USE_InternalThread",
BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance);
var pInternalThread = (IntPtr)f.GetValue(thread);
var nativeId = Marshal.ReadInt32(pInternalThread, (IntPtr.Size == 8) ? 0x022C : 0x0160); // found by analyzing the memory
return nativeId;
}

获取当前线程的托管线程Id

public static string CurrentThreadId
{
get
{
var thread = Thread.CurrentThread;
var threadId = GetNativeThreadId(thread);
return $"{thread.ManagedThreadId} - ({threadId}, 0x{threadId.ToString("x2")})";
}
}
  1. 知道控件的句柄如何获取该控件所在的进程和线程信息?
public static class Extensions
{
// 导入 Windows API 函数
/// <summary>
/// 检索创建指定窗口的线程的标识符,以及创建该窗口的进程(可选)的标识符。
/// GetWindowThreadProcessId 函数 : https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getwindowthreadprocessid
/// </summary>
/// <param name="hWnd">控件句柄</param>
/// <param name="processId">控件所属进程Id</param>
/// <returns>控件所属线程Id</returns>
[DllImport("user32.dll")]
private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int processId);
public static void ConsoleHandleInfo(this Control ctrl, string name)
{
var handle = ctrl.Handle;
var threadId = GetWindowThreadProcessId(handle, out int processId);
Console.WriteLine($"{ThreadInfo.CurrentThreadId} -> 创建 {name} 的进程Id = {processId},线程Id = {threadId}");
}
}

WinForm程序中消息处理的相关方法

按调用顺序依次介绍,可以对消息进行拦截与处理

  1. IMessageFilter的消息处理

    IMessageFilter 接口

    一般处理逻辑,在构造函数中调用Application.AddMessageFilter(this);添加消息筛选器,在析构函数中调用Application.RemoveMessageFilter(this);移除消息筛选器,然后重写PreProcessMessage消息处理函数。

    返回值:如果消息已处理,则为 true;否则为 false

    影响级别:应用程序级

  2. Control.PreProcessMessage

    在调度键盘或输入消息之前,在消息循环内对它们进行预处理(键盘消息预处理函数)。

    返回值:如果消息已由控件处理,则为 true;否则为 false

    影响级别:应用程序级

    重写 PreProcessMessage时,控件返回 true 以指示它已处理消息。 对于控件未处理的消息,应返回 base.PreProcessMessage 的结果 。 控件通常会替代更专用的方法之一,例如 IsInputCharIsInputKeyProcessCmdKeyProcessDialogCharProcessDialogKey ,而不是重写 PreProcessMessage

    Control.PreProcessMessage(Message) 方法

  3. 键盘消息的其他处理函数,如 protected override bool IsInputKey(Keys keyData)

  4. 控件的消息处理函数

    protected override void WndProc(ref Message m)
    {
    // 控件内消息处理函数
    //Console.WriteLine($"WndProc-> {m}");
    base.WndProc(ref m);
    }

其他代码片段

bool PreProcessMessage(ref Message msg)实现

/// <summary>
/// This method is called by the application's message loop to pre-process input messages before they
/// are dispatched. If this method processes the message it must return true, in which case the message
/// loop will not dispatch the message.
/// </summary>
/// <remarks>
/// <para>
/// The messages that this method handles are WM_KEYDOWN, WM_SYSKEYDOWN, WM_CHAR, and WM_SYSCHAR.
/// </para>
/// <para>
/// For WM_KEYDOWN and WM_SYSKEYDOWN messages, this first calls <see cref="ProcessCmdKey(ref Message, Keys)"/>
/// to check for command keys such as accelerators and menu shortcuts. If it doesn't process the message, then
/// <see cref="IsInputKey(Keys)"/> is called to check whether the key message represents an input key for the
/// control. Finally, if <see cref="IsInputKey(Keys)"/> indicates that the control isn't interested in the key
/// message, then <see cref="ProcessDialogKey(Keys)"/> is called to check for dialog keys such as TAB, arrow
/// keys, and mnemonics.
/// </para>
/// <para>
/// For WM_CHAR messages, <see cref="IsInputChar(char)"/> is first called to check whether the character
/// message represents an input character for the control. If <see cref="IsInputChar(char)"/> indicates that
/// the control isn't interested in the character message, then <see cref="ProcessDialogChar(char)"/> is
/// called to check for dialog characters such as mnemonics.
/// </para>
/// <para>
/// For WM_SYSCHAR messages, this calls <see cref="ProcessDialogChar(char)"/> to check for dialog characters
/// such as mnemonics.
/// </para>
/// <para>
/// When overriding this method, a control should return true to indicate that it has processed the message.
/// For messages that aren't processed by the control, the result of "base.PreProcessMessage()" should be
/// returned.
/// </para>
/// <para>
/// Controls will typically override one of the more specialized methods (<see cref="IsInputChar(char)"/>,
/// <see cref="IsInputKey(Keys)"/>, <see cref="ProcessCmdKey(ref Message, Keys)"/>, <see cref="ProcessDialogChar(char)"/>,
/// or <see cref="ProcessDialogKey(Keys)"/>) instead of overriding this method.
/// </para>
/// </remarks>
public virtual bool PreProcessMessage(ref Message msg)
{
bool result;
if (msg.MsgInternal == PInvoke.WM_KEYDOWN || msg.MsgInternal == PInvoke.WM_SYSKEYDOWN)
{
if (!GetExtendedState(ExtendedStates.UiCues))
{
ProcessUICues(ref msg);
}
Keys keyData = (Keys)(nint)msg.WParamInternal | ModifierKeys;
if (ProcessCmdKey(ref msg, keyData))
{
result = true;
}
else if (IsInputKey(keyData))
{
SetExtendedState(ExtendedStates.InputKey, true);
result = false;
}
else
{
result = ProcessDialogKey(keyData);
}
}
else if (msg.MsgInternal == PInvoke.WM_CHAR || msg.MsgInternal == PInvoke.WM_SYSCHAR)
{
if (msg.MsgInternal == PInvoke.WM_CHAR && IsInputChar((char)(nint)msg.WParamInternal))
{
SetExtendedState(ExtendedStates.InputChar, true);
result = false;
}
else
{
result = ProcessDialogChar((char)(nint)msg.WParamInternal);
}
}
else
{
result = false;
}
return result;
}

相关参考

  1. msg 结构 (winuser.h)
  2. Message 结构
  3. 关于消息和消息队列
  4. 使用消息和消息队列
  5. Windows消息机制(MFC深度详解)
  6. windows消息机制深入详解
  7. WINDOWS消息,消息的解析可以参考此篇
  8. 线程与消息队列,介绍线程间相互通信相关的函数
posted @   lanwah  阅读(165)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示