C# - 等待WinForms消息循环

7
我需要编写一个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()即可。此外,需要一个真实的窗口,您必须有一个有效的句柄。 - Hans Passant
由于我不想为每个热键启动一个消息循环,所以我必须停止并重新启动现有的消息循环,在同一线程中调用RegisterHotKey()。而且,由于我的代码正在工作,我认为我不需要一个真正的窗口,有效的句柄是通过NativeWindow.CreateHandle(CreateParams)创建的。 - Jonas W
4个回答

5

因此,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方法自动提供的相同效果。请注意保留HTML标记。

我想避免使用笨重的Form类,而是使用轻量级的NativeWindow类。但这似乎是最干净的解决方案,以获得使用Invoke方法的机会...谢谢! - Jonas W
@Jonas:我已经根据你的另一个评论怀疑了。我更新了我的答案:基本上,你应该能够使用手动消息传递技术来模仿“Invoke”。 - Brian Gideon

1

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


1
感谢您的快速回复!我已经尝试使用AutoResetEvent类来实现,但在这种情况下,我必须在Application.Run()之前调用AutoResetEvent.Set(),这意味着在使用AutoResetEvent.WaitOne()等待后仍然可能调用ApplicationContext.ExitThread()... - Jonas W

1
您可以尝试等待应用程序的Idle事件被触发,以允许用户调用RegisterHotKey。

谢谢您的回复!如果该库在另一个具有自己消息循环的WinForms应用程序中使用,这不会成为问题吗? - Jonas W
可以在第二个线程上创建一个消息泵并显示一个窗体,这是完全可能的。该线程可以独立于第一个线程处理事件。这可能取决于您的API的使用方式。 - CommonSense
我尝试了你的方法,它很有效,只有一个问题:Application.Idle事件只触发一次,并且无法识别消息泵的重新启动:S - Jonas W
这是因为您正在使用默认的GetMessage循环。尝试编写自己的PeekMessage循环。 - CommonSense
我从未在C#中编写过自己的消息循环 - 像这个线程中所做的那样这样做是否安全:http://www.codeguru.com/forum/showthread.php?t=296129?在DoEvents()调用之间的时间间隔应该是多久? - Jonas W

0

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

我解决了所有这些问题,下面是可工作的代码。我本来想把它放在答案的评论中,但我没有足够的声望。在做完这些之后,我有点不确定以这种方式强制创建窗体并调用Application.Run的有效性,或者在线程上运行'new Form',然后将其传递给另一个线程进行完全创建(特别是因为这可以轻松避免):

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();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接