Date: | March 1, 2005 / year-entry #50 |
Tags: | code;modality |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20050301-00/?p=36333 |
Comments: | 34 |
Summary: | As we noted at the end of part 3, now that you know the conventions surrounding the WM_QUIT message you can put them to your advantage. The more robust you want the TimedMessageBox function to be, the more work you need to do. Here's the cheap version, based on the sample in the Knowledge Base, but... |
As we noted at the end of part 3,
now that you know the conventions surrounding
the
The more robust you want the static BOOL s_fTimedOut; static HWND s_hwndMBOwnerEnable; void CALLBACK CheapMsgBoxTooLateProc(HWND hWnd, UINT uiMsg, UINT_PTR idEvent, DWORD dwTime) { s_fTimedOut = TRUE; if (s_hwndMBOwnerEnable) EnableWindow(s_hwndMBOwnerEnable, TRUE); PostQuitMessage(42); // value not important } // Warning! Not thread-safe! See discussion. int CheapTimedMessageBox(HWND hwndOwner, LPCTSTR ptszText, LPCTSTR ptszCaption, UINT uType, DWORD dwTimeout) { s_fTimedOut = FALSE; s_hwndMBOwnerEnable = NULL; if (hwndOwner && IsWindowEnabled(hwndOwner)) { s_hwndMBOwnerEnable = hwndOwner; } UINT idTimer = SetTimer(NULL, 0, dwTimeout, CheapMsgBoxTooLateProc); int iResult = MessageBox(hwndOwner, ptszText, ptszCaption, uType); if (idTimer) KillTimer(NULL, idTimer); if (s_fTimedOut) { // We timed out MSG msg; // Eat the fake WM_QUIT message we generated PeekMessage(&msg, NULL, WM_QUIT, WM_QUIT, PM_REMOVE); iResult = -1; } return iResult; }
This Do you see how it works?
The global static variable
Note that we remove the Note also that when we decide that the timeout has occurred, we re-enable the original owner window before we cause the message box to bail out of its message loop by posting a quit message. Those are the rules for the correct order for disabling and enabling windows. Note also that we used a thread timer rather than a window timer. That's because we don't own the window being passed in and therefore don't know what timer IDs are safe to use. Any timer ID we pick might happen to collide with a timer ID being used by that window, resulting in erratic behavior.
Recall that when you pass Well, this is one scenario where this is exactly what you want. Next comes the job of making the function a tad more robust. But before we do that, we'll need two quick sidebars. |
Comments (34)
Comments are closed. |
Why doesn’t MessageBox re-enable the parent window? After all, it disabled it…
Could you cause "collataral damage" to other message pumps? For instance, consider if you were in an STA, and the implementation of another timer function called an out-of-proc COM server. In that case, you might abort the COM call message pump, as well as the MessageBox message pump.
Is this really safe?
—
Actually, I just tested this and it appears as if the COM message pump just ignores the quit message. Ie: the COM message pump doesn’t exit just because a WM_QUIT message showed up. So, the message box doesn’t get dismissed until the out-of-proc COM call is completed.
I also tested CoWaitForMultipleHandles(). As I expected (since this method uses the COM modal loop), it also ignored the WM_QUIT message.
That’s interesting behavior… since if I was trying to write my own message pumping waiting routing, I would probably respond to WM_QUIT, thinking that’s what a good citizen does. That’s exactly the advice that you give in your "Modality, part 3" :"if ever […] you get a WM_QUIT message, you must not only exit your modal loop, but…". Apparently, this isn’t always true. You can delay exiting your modal loop if you feel that you’re important enough.
This is a really interesting series… thanks!!!
Oops. I made a mistake in my earlier posting. Actually, after further experimenting, it appears that a COM modal loop doesn’t process WM_TIMER messages. Therefore, the WM_TIMER message that invokes WM_QUIT doesn’t actually occur until after the COM modal loop is ended. So, my test was flawed.
I tried the following code:
HANDLE hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
DWORD index;
PostQuitMessage(42);
HRESULT hr = CoWaitForMultipleHandles(COWAIT_WAITALL,INFINITE,1,&hEvent,&index);
which deadlocks the threads. This implies that CoWaitForMultipleHandles doesn’t handle WM_QUIT… (I think, unless I missed something again)
G. Man: there are three different timers in the .NET Framework for three different purposes.
System.Threading.Timer is a wrapper for a dedicated timer thread. As such it will fire events on a thread-pool thread – it does not fire on the calling thread. The nearest Win32 API equivalent is CreateTimerQueueTimer.
System.Timers.Timer is a wrapper for System.Threading.Timer that endeavours to get back onto the correct thread, if the SynchronizingObject property is set to an object that implements ISynchronizeInvoke.
System.Windows.Forms.Timer wraps SetTimer. It uses a thread timer rather than a window timer.
The Win32 API also has a Waitable Timer kernel object.
To G. Man:
I think the documentation is no longer ambiguous, though it’s still confusing. I’d agree that the SetTimer API tries to do too much. The worst thing about allowing the user to specify the timer ID for window timers is that there’s no way to avoid stepping on existing timers. You just have to know somehow that (say) listview windows use timer IDs 42 and 43 internally to help manage their in-place editing windows (IIRC).
And what do you do when you receive a WM_TIMER for a timer ID you recognize as your own? Do you pass it on to the previous wndproc? Given that you may have usurped that timer (and disregarding the frequency issue), you might think you should, but then you find that (say) listview kills your timer unconditionally, no matter what its ID is.
So, WM_TIMER is not a message that can be harmlessly passed to the superclass. I think these are the rules. You must only handle IDs you recognize, and you mustn’t pass them to the superclass unless you wrote the superclass and know what it does. On the other hand, you must forward WM_TIMER for IDs you don’t recognize to the superclass with no additional processing. And when subclassing a window, you must determine its WM_TIMER IDs, which may not be documented, so that you don’t overwrite them.
"There should be another SetTimer API function that doesnt use window handles."
There are many timer functions that don’t use window handles. Feel free to use those if you like.
As a user, I like self-destructing message boxes because I don’t have to waste a click dismissing them. There’s a situation, however, in which they irritate me.
Say the message box has a long timeout (especially since I’m a speed-reader) and I want to click its OK button to dismiss. So I click, but between my brain’s command to click and my finger executing it, the message box has self-destructed. Where does my click go? To the window behind it, of course. And what if there was a control behind it? Then I just clicked the wrong button. I have the same problem with pop-up windows stealing the focus.
One solution would be to have the message box start fading a few seconds before its timeout, warning me of its imminent demise. Another would be to disable clicks for a second or so after its destruction. I wish the OS had options for these situations.
Raymond> Sorry for the OT question, but is there a MessageBox-like function that also has a "don’t show me this again" checkbox?
I was skimming MSDN a few months ago and came across such a function, but I didn’t write down the name, and now I can’t find it again. It wasn’t ShellMessageBox or SoftwareUpdateMessageBox, but something similar.
G. Man,
Passing NULL to SetTimer is valid(read MSDN)! The consequence is that the nIDEvent passed is ignored.
So, Windows wont send a WM_TIMER, but call the function specified in last arg.
From MSDN:
If the function succeeds and the hWnd parameter is NULL, the return value is an integer identifying the new timer. An application can pass this value to the KillTimer function to destroy the timer.
If the function succeeds and the hWnd parameter is not NULL, then the return value is a nonzero integer. An application can pass the value of the nIDEvent parameter to the KillTimer function to destroy the timer.
Best regards,
Nelson Faria
Dave, if a message is not important enough to stay on the screen until you read it then it should not be a modal message box.
Mike: SHMessageBoxCheck.
Why would your code be not thread-safe? As far as I understand even timer-less timers are internally dispatched using messages so there shouldn’t be problems imo
Consider if two threads call CheapTimedMessageBox at the same time.
Mike: try this one instead http://www.kkow.net/etep/code/mb/ (and specifically how to make a messagebox with a checkbox http://www.kkow.net/etep/code/assert.html).
Jonathan> Sweet, thanks!
"Passing NULL to SetTimer is valid(read MSDN)! The consequence is that the nIDEvent passed is ignored.
So, Windows wont send a WM_TIMER, but call the function specified in last arg. "
That’s if you specify a function in the last arg – if you don’t, WM_TIMER is posted as normal but with a null hwnd, so you need to process it in the GetMessage() loop.
SetTimer is just a broken API function, period.
SetTimer should not take an event id parameter as input. It should only return an event id. Then, it would be clear that I need to use the returned ID to reference the timer in future calls.
As it is, I can’t tell what’s up with the id input vs. the id returned. Is it safe to assume they are the same? Apparently not, since then it wouldn’t need to return an id. But if they are different, which id do I use in which circumstances?
Also, passing NULL as and hwnd should be illegal. There should be another SetTimer API function that doesnt use window handles.
It looks like an effort to solve this in .NET, but now there are three different "timer" classes just in the BCL. Don’t *even* get me started on that.
3/1/2005 9:26 AM Dave Goodman
> Another would be to disable clicks for a
> second or so after its destruction.
Bingo. That would catch a lot of situations where windows either suddenly appear (stealing focus) or disappear, causing the user’s click to go to a window different from the one the user intended. Mr. Chen, please see if you can get that feature added. Of course 1 second will be too long for some people and too short for others.
In the base note:
> Any timer ID we pick might happen to collide
> with a timer ID being used by that window,
How is it guaranteed that the timer ID picked by Windows won’t also collide with a timer ID being used by that window? Even if Windows carefully chooses a timer ID different from all the ones that the window is using at this moment, the code for that window might still be intending to use a colliding timer ID a few seconds later.
Jonathan, Mike:
I’ve searched MSDN and Includes directory, but didn’t find such function.
@CoMargo:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/shlwapi/others/SHMessageBoxCheck.asp
Right. The implementor of the window class owns most aspects of a window: Messages in the WM_USER range, timers, window and class extra bytes…
3/1/2005 7:47 PM Raymond Chen
>> Disable input for one second after focus
>> changes.
>
> (I suspect however that the answer will
> be "no". How often do you sit there with
> your finger poised waiting for a dialog box
> to appear so you can immediately type
The problem is exactly the opposite. How often do you sit there typing quickly into Outlook, Word, Internet Explorer, etc., and suddenly find that your keystroke has been stolen by a dialog box? Some time ago you replied to another commenter showing that you are aware of this problem. Perhaps less often you’ve been doing another opposite, not having your finger poised but happening to notice that a dialog box popped up a few minutes ago, and when you want to click you happen to be a microsecond too late because it was a Timed MessageBox.
Here Mr. Goodman proposed a solution, and it sure looks like a good solution.
> Personally I do it a lot.
As frequently as your ordinary keystrokes into Outlook, Word, etc.? Or maybe not, but your priorities say you want to take the risk in order to speed up the response in these situations. Well, that is why I said that 1 second would be too slow for some people. But I’ll bet Mr. Goodman and I are not the only people who have been hit by keystrokes or clicks getting stolen by the wrong app, and we’re not the only ones who would like to get a 1-second (or settable) block when focus changes.
There are sometimes you want to block input after a focus change, and sometimes you don’t. In the Alt+FXN case I don’t want input blocked – that ruins typeahead.
If computers could read your mind, maybe it could block input based on your mental state.
Nevertheless, I forwarded the suggestion you sent to me via email to the people who are responsible for input and focus changes for their consideration.
I’m surprised the WM_TIMER message even gets dispatched. DispatchMessage can’t dispatch messages posted to no window, so you should never rely on this trick working for any modal loop. The documentation recommends installing a hook for the calling thread:
"Messages that are not associated with a window cannot be dispatched by the DispatchMessage function. Therefore, if the recipient thread is in a modal loop (as used by MessageBox or DialogBox), the messages will be lost. To intercept thread messages while in a modal loop, use a thread-specific hook."
KJK: I’ve submitted a correction to the documentation. There should be words to the effect of "As a general rule" at the start of that first sentence. The DispatchMessage documentation mentions WM_TIMERs as dispatchable provided the lParam is NULL. Note however that the documentation you quoted comes from PostThreadMessage, and you can’t post a WM_TIMER message; therefore, the sentence is correct in context although perhaps not strictly correct in an absolute sense.
3/2/2005 5:48 PM Raymond Chen
> There are sometimes you want to block input
> after a focus change, and sometimes you
> don’t. In the Alt+FXN case I don’t want
> input blocked – that ruins typeahead.
In the intended typeahead case you don’t want input blocked. Perhaps the condition should be that the new window is owned by the window that had focus? In that case the user might have expected the focus change and typed ahead.
If the new (or newly focused) window isn’t owned by the window that previously had focus, then let’s try to consider which cases the user might have intended keystrokes or clicks to go to them immediately. If the window was already partly or fully displayed, and the user clicked in a part that was already displayed in order to give it focus, that would be one example. I can’t think of other examples at the moment. In other cases I think it’s pretty obvious that the user didn’t intend input to go to that window, until the user has had time to notice what window is now there.
What about the case where you visit a web site and know that it’s going to display a popup window after 5 seconds, so you wait anxiously and the moment the popup appears you click the X / hit Alt+F4. How can the system distinguish this (an expected popup, not owned by the previous focus window) from an unexpected popup? I understand the value of underlying principle, but it in large part requires understand the user’s frame of mind, something computers aren’t very good at yet.
If you know exactly where that X is going to appear and be clickable, then you want to set your duration of disabled keyboard/mouse input to 0 instead of 1 second. I think there are a lot of people who will still find 1 second preferable, because the damage done by not having it is sometimes far more severe. Let us have the option.
As I already noted to you via email, I sent your proposal off to the team that would be responsible for it. But beware "too many options".
I’m not opposed to the idea. I’m just pointing out that there are places where the "one second delay" would be undesirable. Perhaps heuristics could be added to recognize those scenarios. I don’t know. (For example, maybe there’s a program that puts up ten message boxes in a row that you want to dismiss. You used to be able to just hold down the ESC key and they all go away. Now they disappear one per second. Yes, there’s the risk that I might have dismissed the wrong message box. Is the risk worth the benefit?)
But as I have already noted twice now, I forwarded your message to the people responsible and it is their decision at this point. My opinion carries no weight. In particular I did not include my opinion when I forwarded your suggestion to them.