【问题标题】:C# - Waiting for WinForms Message LoopC# - 等待 WinForms 消息循环
【发布时间】:2011-09-12 18:58:15
【问题描述】:

我必须编写一个 C# API 来注册全局热键。为了接收 WM_HOTKEY 消息,我使用System.Windows.Forms.NativeWindow 并使用System.Windows.Forms.Application.Run(ApplicationContext) 运行自己的消息循环。当用户想要注册一个热键时,他必须运行一个名为RegisterHotkey() 的方法,该方法用System.Windows.Forms.ApplicationContext.ExitThread() 停止消息循环,用RegisterHotKey() (P/Invoke) 函数注册热键并再次启动消息循环。这是必需的,因为必须在创建窗口的同一线程中调用 RegisterHotKey(),而必须在运行消息循环的同一线程中再次实例化该窗口。

问题是,如果用户在启动运行消息循环的线程后不久调用RegisterHotkey() 方法,ApplicationContext.ExitThread() 会在Application.Run(ApplicationContext) 之前被调用,因此应用程序会无限期地阻塞。有人知道等待消息循环启动的方法吗?

提前致谢!

【问题讨论】:

  • 这没有多大意义。只需在 开始消息循环之前调用 RegisterHotkey()。另外,需要一个真实的窗口,你必须有一个有效的句柄。
  • 由于我不想为每个热键启动一个消息循环,我必须停止并重新启动现有的循环以在同一线程中调用 RegisterHotKey()。而且由于我的代码正在运行,我认为我不需要一个真正的窗口,而是由NativeWindow.CreateHandle(CreateParams) 创建有效句柄。

标签: c# .net winforms multithreading message-loop


【解决方案1】:

所以RegisterHotKey 需要从创建窗口并启动消息循环的同一线程中调用。为什么不将RegisterHotKey 的执行注入到您的自定义消息循环线程中?这样您就不需要停止并重新启动消息循环。你可以重复使用你开始的第一个,同时避免奇怪的竞争条件。

您可以使用ISynchronizeInvoke.Invoke 将委托注入另一个线程,这会将委托编组到托管ISynchronizeInvoke 实例的线程上。以下是可能的完成方式。

void Main()
{
  var f = new Form();

  // Start your custom message loop here.
  new Thread(
    () =>
    {
      var nw = NativeWindow.FromHandle(f.Handle);
      Application.Run(new ApplicationContext(f));
    }

  // This can be called from any thread.
  f.Invoke(
    (Action)(() =>
    {
      RegisterHotKey(/*...*/);
    }), null);
}

我不知道...也许您也想致电UnregisterHotKey,具体取决于您所追求的行为。我对这些 API 不太熟悉,因此无法评论它们的使用方式。

如果您不希望创建任意的Form 实例,那么您可以通过SendMessage 等向线程提交自定义消息并在NativeWindow.WndProc 中处理它以获得与ISynchronizeInvoke 方法自动提供。

【讨论】:

  • 我想避免使用重量级的 Form 类,而是使用轻量级的 NativeWindow 类。但这似乎是获得使用 Invoke 方法的机会的最干净的解决方案......谢谢!
  • @Jonas:我已经怀疑这是基于您的另一个 cmets。我更新了我的答案:基本上,您应该能够使用手动消息传递技术模仿Invoke
【解决方案2】:

我不知道是否有更好的方法,但是您可以使用Mutex 并在调用Application.Run 时重置它,并在调用Applicationcontext.ExitThread() 时使用Mutex.Wait()

【讨论】:

  • 感谢您的快速回复!我已经用AutoResetEvent 类尝试过这个,但在这种情况下,我必须在Application.Run() 之前调用AutoResetEvent.Set(),这意味着在等待AutoResetEvent.WaitOne() 之后调用ApplicationContext.ExitThread() 仍然是可能的......
【解决方案3】:

您可以尝试等到 Application.Idle 事件被触发以允许用户调用 RegisterHotKey。

【讨论】:

  • 感谢您的回复!如果该库在另一个具有自己的消息循环的 winforms 应用程序中使用,这会不会是一个问题?
  • 当然可以在辅助线程上创建消息泵并显示表单。然后,该线程可以独立于第一个线程处理事件。这可能取决于您的 API 的使用方式。
  • 我试过你的方法,效果很好,除了一件事:Application.Idle 事件只触发一次并且不识别消息泵的重新启动:S
  • 那是因为您使用的是默认的 GetMessage 循环。尝试编写自己的 PeekMessage 循环。
  • 我从未在 C# 中编写过自己的消息循环 - 像他们在此线程中所做的那样安全吗:codeguru.com/forum/showthread.php?t=296129? DoEvents() 调用之间的间隔应该是多长时间?
【解决方案4】:

我知道这个帖子很旧,但我在尝试解决问题时来到这里并花了一些时间查看接受的答案。这当然基本上是正确的,但就目前而言,代码不会编译,不会启动它创建的线程,即使我们修复它在创建 f 之前调用 f.Invoke,也会引发异常。

我修复了所有这些,工作代码如下。我会把它放在对答案的评论中,但我没有足够的代表。这样做之后,我有点不确定在以这种方式调用 Application.Run 之前强制创建表单的有效性,或者在一个线程上执行“新表单”然后将其传递给另一个线程以完全创建(特别是因为这很容易避免):

static void Main(string[] args)
{
    AutoResetEvent are = new AutoResetEvent(false);
    Form f = new Form();
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);

    new Thread(
        () =>
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
            var nw = NativeWindow.FromHandle(f.Handle);
            are.Set();
            Application.Run(f);
        }).Start();

    are.WaitOne(new TimeSpan(0, 0, 2));

    f.Invoke(
        (Action)(() =>
        {
            Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        }), null);

    Console.ReadLine();
}

【讨论】:

    猜你喜欢
    • 2011-06-09
    • 1970-01-01
    • 2010-11-03
    • 1970-01-01
    • 1970-01-01
    • 2022-01-23
    • 2017-08-17
    • 2015-05-13
    • 1970-01-01
    相关资源
    最近更新 更多