C# - Waiting for WinForms Message Loop
Asked Answered
R

4

7

I have to write an C# API for registering global hotkeys. To receive the WM_HOTKEY message, I use a System.Windows.Forms.NativeWindow and run an own message loop with System.Windows.Forms.Application.Run(ApplicationContext). When the user wants to register a hotkey, he has to run a method called RegisterHotkey() which stops the message loop with System.Windows.Forms.ApplicationContext.ExitThread(), registers the hotkey with the RegisterHotKey() (P/Invoke) function and starts the message loop again. This is required, because RegisterHotKey() must be called within the same thread that created the window, which again must be instantiated within the same thread that runs the message loop.

The problem is, that if the user calls the RegisterHotkey() method shortly after starting the thread which is running the message loop, ApplicationContext.ExitThread() gets called before Application.Run(ApplicationContext) and therefore the application blocks indefinitely. Does anybody know an approach for waiting for a message loop to be started?

Thanks in advance!

Rashida answered 12/9, 2011 at 18:58 Comment(2)
This doesn't make a lot of sense. Just call RegisterHotkey() before you start the message loop. Also, a real window is required, you must have a valid handle.Neutralization
Since i don´t want to start one message loop per hotkey, i have to stop and restart the existing one to call RegisterHotKey() within the same thread. And since my code is working, I don´t think that i need a real window, the valid handle is created by NativeWindow.CreateHandle(CreateParams) instead.Rashida
O
5

So RegisterHotKey needs to be called from the same thread that created the window and started the message loop. Why not inject the execution of RegisterHotKey into your custom message loop thread? That way you do not need to stop and restart the message loop. You can just reuse the first one you started and avoid the strange race conditions at the same time.

You can inject a delegate onto another thread using ISynchronizeInvoke.Invoke which will marshal that delegate onto the thread hosting the ISynchronizeInvoke instance. Here is how it might be done.

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

I do not know...maybe you will want to call UnregisterHotKey as well depending on the behavior you are after. I am not that familiar with these APIs so I cannot comment on how they might be used.

If you do not want that arbitrary Form instance created then you could probably get away with submitting a custom message to the thread via SendMessage and the like and processing it in NativeWindow.WndProc to get the same effect that the ISynchronizeInvoke methods provide automatically.

Occultism answered 12/9, 2011 at 19:58 Comment(2)
I wanted to avoid using the heavyweight Form class and use the lightweight NativeWindow class instead. But this seems to be the cleanest solution to get the opportunity to use the Invoke approach... Thanks!Rashida
@Jonas: I already suspected that based another one of your comments. I updated my answer: Basically, you should be able to mimic Invoke using manual message passing techniques.Occultism
K
1

I don't know if there's a better way, but you could use a Mutex and reset it when you call Application.Run and use Mutex.Wait() when calling Applicationcontext.ExitThread().

Kasey answered 12/9, 2011 at 19:4 Comment(1)
Thanks for your quick reply! I already tried this with the AutoResetEvent class, but in this case I´d have to call AutoResetEvent.Set() before Application.Run(), which means that a call to ApplicationContext.ExitThread() after waiting with AutoResetEvent.WaitOne() is still possible...Rashida
E
1

You might try waiting until the Application.Idle Event is fired to allow the user to call RegisterHotKey.

Estrogen answered 12/9, 2011 at 19:12 Comment(5)
Thanks for your reply! Wouldnt this be a problem if the library gets used in another winforms application with its own message loop?Rashida
It's certainly possible to create a message pump on a secondary thread and display a form. That thread can then process events independently of the first thread. It may depend on how your API is used.Estrogen
I´ve tried your approach and it works great, except one thing: The Application.Idle event only fires once and doesn´t recognize the restart of the message pump :SRashida
That's because you're using a default GetMessage loop. Try writing your own PeekMessage loop.Estrogen
I´ve never written an own message loop in C# - is it safe to do this like they did in this thread: codeguru.com/forum/showthread.php?t=296129? How long should the interval between the DoEvents() calls be?Rashida
R
0

I know this thread is old, but I came here when trying to resolve an issue and spent some time looking at the accepted answer. It is of course basically correct, but as it stands the code doesn't compile, doesn't start the thread it creates, and even if we fix that it calls f.Invoke before f is created, so throws exceptions.

I fixed all that up and working code is below. I'd have put it in a comment on the answer but I don't have sufficient rep. Having done that, I'm a little unsure of the validity of forcing the form to be created before calling Application.Run in this way, or of doing 'new Form' on one thread and then passing it to another to be fully created (particularly since this is easily avoided):

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();
}
Rebel answered 17/1, 2016 at 13:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.