开发者

WinForm-style Invoke() in unmanaged C++

开发者 https://www.devze.com 2022-12-26 08:57 出处:网络
I\'ve been playing with a DataBus-type design for a hobby project, and I ran into an issue. Back-end components need to notify the UI that something has happened. My implementation of the bus delivers

I've been playing with a DataBus-type design for a hobby project, and I ran into an issue. Back-end components need to notify the UI that something has happened. My implementation of the bus delivers the messages synchronously with respect to the sender. In other words, when you call Send(), the method blocks until all the handlers have called. (This allows callers to use stack memory management for event objects.)

However, consider the case where an event handler updates the GUI in response to an event. If the handler is called, and the message sender lives on another thread, then the handler cannot update the GUI due to Win32's GUI elements having thread affinity. More dynamic platforms such as .NET allow you to handle this by calling a special Invoke() method to move the method call (and the arguments) to the UI thread. I'm guessing they use the .NET parking window or the like for these sorts of things.

A morbid curiosity was born: can we do this in C++, even if we limit the scope of the problem? Can we make it nicer than existing solutions? I know Qt does something similar with the moveToThread() function.

By nicer, I'll mention that I'm specifically trying to avoid code of the following form:

if(开发者_JAVA百科! this->IsUIThread())
{
    Invoke(MainWindowPresenter::OnTracksAdded, e);
    return;
}

being at the top of every UI method. This dance was common in WinForms when dealing with this issue. I think this sort of concern should be isolated from the domain-specific code and a wrapper object made to deal with it.

My implementation consists of:

  • DeferredFunction - functor that stores the target method in a FastDelegate, and deep copies the single event argument. This is the object that is sent across thread boundaries.

  • UIEventHandler - responsible for dispatching a single event from the bus. When the Execute() method is called, it checks the thread ID. If it does not match the UI thread ID (set at construction time), a DeferredFunction is allocated on the heap with the instance, method, and event argument. A pointer to it is sent to the UI thread via PostThreadMessage().

  • Finally, a hook function for the thread's message pump is used to call the DeferredFunction and de-allocate it. Alternatively, I can use a message loop filter, since my UI framework (WTL) supports them.

Ultimately, is this a good idea? The whole message hooking thing makes me leery. The intent is certainly noble, but are there are any pitfalls I should know about? Or is there an easier way to do this?


I have been out of the Win32 game for a long time now, but the way we used to achieve this was by using PostMessage to post a windows message back to the UI thread and then handle the call from there, passing the additional info you need in wParam/lParam.

In fact I wouldn't be surprised if that is how .NET handles this in Control.Invoke.

Update: I was currios so I checked with reflector and this is what I found.

Control.Invoke calls MarshaledInvoke which does a bunch of checkes etc. but the interesting calls are to RegisterWindowMessage and PostMessage. So things have not changed that much :)


A little bit of follow-up info:

There are a few ways you can do this, each of which has advantages and disadvantages:

  • The easiest way is probably the QueueUserAPC() call. APCs are a bit too in-depth to explain, but the only drawback is they may run when you're not ready for them if the thread gets put into an alertable wait state accidently. Because of this, I avoided them. For short applications, this is probably OK.

  • The second way involves using PostThreadMessage(), as previously mentioned. This is better than QueueUserAPC() in that your callbacks aren't sensitive to the UI thread being in an alertable wait state, but using this API has the problem of your callbacks not being run at all. See Raymond Chen's discussion on this. To get around this, you need to put a hook on the thread's message queue.

  • The third way involves setting up an invisible, message-only window whose WndProc calls the deferred call, and using PostMessage() for your callback data. Because it is directed at a specific window, the messages won't get eaten in modal UI situations. Also, message-only windows are immune to system message broadcasts (thus preventing message ID collisions). The downside is it requires more code than the other options.

0

精彩评论

暂无评论...
验证码 换一张
取 消