Date: | March 4, 2005 / year-entry #56 |
Tags: | code;modality |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20050304-00/?p=36273 |
Comments: | 16 |
Summary: | A few days ago, we saw a simple version of a timed message box which had a limitation that it could be used from only one thread at a time. Today we'll work to remove that limitation. As you may recall, the reason why it could be used from only one thread at a time... |
A few days ago, we saw a simple version of a timed message box which had a limitation that it could be used from only one thread at a time. Today we'll work to remove that limitation. As you may recall, the reason why it could be used from only one thread at a time was that we kept the "Did the message box time out?" flag in a global. To fix it, we will move the flag to a per-instance location, namely a helper window. Start with the scratch program, add the code for the scratch window class, change the name of the scratch window class so it doesn't conflict with the class name of the scratch program (thanks to reader Adrian for pointing this out), then add the following: #define IDT_TOOLATE 1 typedef struct TOOLATEINFO { BOOL fTimedOut; HWND hwndReenable; } TOOLATEINFO; void CALLBACK MsgBoxTooLateProc(HWND hwnd, UINT uiMsg, UINT_PTR idEvent, DWORD dwTime) { TOOLATEINFO *ptli = reinterpret_cast<TOOLATEINFO*>( GetWindowLongPtr(hwnd, GWLP_USERDATA)); if (ptli) { ptli->fTimedOut = TRUE; if (ptli->hwndReenable) { EnableWindow(ptli->hwndReenable, TRUE); } PostQuitMessage(42); } } int TimedMessageBox(HWND hwndOwner, LPCTSTR ptszText, LPCTSTR ptszCaption, UINT uType, DWORD dwTimeout) { TOOLATEINFO tli; tli.fTimedOut = FALSE; BOOL fWasEnabled = hwndOwner && IsWindowEnabled(hwndOwner); tli.hwndReenable = fWasEnabled ? hwndOwner : NULL; HWND hwndScratch = CreateScratchWindow(hwndOwner, DefWindowProc); if (hwndScratch) { SetWindowLongPtr(hwndScratch, GWLP_USERDATA, reinterpret_cast<LPARAM>(&tli)); SetTimer(hwndScratch, IDT_TOOLATE, dwTimeout, MsgBoxTooLateProc); } int iResult = MessageBox(hwndOwner, ptszText, ptszCaption, uType); if (hwndScratch) { KillTimer(hwndScratch, IDT_TOOLATE); if (tli.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; } DestroyWindow(hwndScratch); } return iResult; } void OnChar(HWND hwnd, TCHAR ch, int cRepeat) { switch (ch) { case ' ': TimedMessageBox(hwnd, TEXT("text"), TEXT("caption"), MB_OK, 2000); break; } } // add to WndProc HANDLE_MSG(hwnd, WM_CHAR, OnChar); // add to InitApp RegisterScratchWindowClass(); This is basically the same as the previous cheap version, just with slightly different bookkeeping.
The state of the timed message box is kept in the structure
Aha, but timer callbacks do get a window handle.
But as we discovered a few days ago, we can't just hang the callback
off the
The solution: Hang it on a window of our own window creation.
That way, we get a whole new space of timer IDs to play in,
separate from the timer IDs that belong to |
Comments (16)
Comments are closed. |
Cute trick, in theory.
With your previous programming posts, I’ve only studied the code. For the first time, however, I decided to piece together the bits verbatim and actually try it out.
But it doesn’t work properly.
The timer expires, the quit message is posted, the MessageBox call returns, and PeekMessage attempts to remove the WM_QUIT message. But PeekMessage apparently fails to remove the WM_QUIT. The call returns FALSE and the MSG structure is not filled out. The next message encountered by the main message loop is WM_QUIT (with your 42 in the wParam, indicating that it is the timer copy and not some other PostQuitMessage like the one in OnDestroy). Of course, the main loop exits at that point.
Is WM_QUIT so special that PeekMessage won’t/can’t remove it?
(Windows XP SP2, Visual Studio .NET 2003, default solution and project settings except that I disabled 64-bit portability warnings, added /NODEFAULTLIB to linker, and added msvcrtd.lib and comctl32.lib manually.)
It worked for me. I have the same configuration.
Thanks to reader Adrian who pointed out that you need to rename the scratch window class to "Scratch2" to avoid conflicting with the class name "scratch" of the main program.http://weblogs.asp.net/oldnewthing/archive/2005/03/02/383562.aspx#385251
Good catch, Adrian. (Though who is posting you these strange messages might be worth investigating.) I’ll have to think about how this can be fixed…
Spy++ can display the names of registered messages. Not sure how it does that.
Registered messages are implemented as atoms. See GlobalGetAtomName
I (sort of) figured out why it doesn’t work on my machine, but I’m not sure what the correct solution is.
When MessageBox returns, the WM_QUIT message is the *third* message in the queue, not the first. Thus PeekMessage doesn’t find it. The first two messages have NULL hwnds and message identifiers over 0xC000 (registered messages). (Anybody know a trick for finding out who registered these messages?)
By putting PeekMessage (with PM_REMOVE and no filter) in a loop, I can eventually find the WM_QUIT and get rid of it. But then those other messages are lost. I suppose I could repost them, but is that really the right thing to do?
All of this suggests to me that using WM_QUIT to exit a modal loop is a dangerous thing. A seemingly trivial bug like this means your app quits without saving your work or doing other essential cleanup. Wouldn’t it be safer to find a way to post a WM_CLOSE or a WM_COMMAND/IDCANCEL to the MessageBox?
Why not just use "__declspec( thread )"? or the less handy Tls* functions if you’re running in a dll?
Because you might not own the code you’re getting called by.
ThreadLocalStorage has the same issue that SetTimer has; you can’t guarantee that your TLS slot isn’t already being used by the app you’re talking to.
Admin note: something’s wrong with the links. In my feed reader, they all point to my local harddrive.
Who’s fault is it?
Probably because your reader can’t handle relative links.
Jeffrey Richter shows another "Timed Messagebox" in his book "Programming Applications for Microsoft Windows". He uses the API CreateTimerQueueTimer: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dllproc/base/createtimerqueuetimer.asp
Personally I think, that is quite smart. Also it requires Windows XP or Windows 2000 Professional.
re: relative links in readers
I notified Feeddemon’s author:
I do see the problem, but I’m not convinced that it’s the fault of the RSS reader. The RSS spec doesn’t address the problem of relative links, and Dave Winer has stated (http://inessential.com/?comments=1&postid=2238) that relative links aren’t allowed in RSS.
If you have a relative link in an RSS feed, what is it relative to? Is it relative to the channel link, or the URL of the feed itself? More importantly, if a single item containing a relative link is used outside of the actual feed (for example, in the results returned by a Feedster RSS search), how do you determine what the link is relative to?
To illustrate the problem, you state that the links in Raymond’s feed should be relative to http://weblogs.asp.net/ yet the channel link in this feed is http://weblogs.asp.net/oldnewthing/. FeedDemon – like many other RSS readers – uses the channel link to complete relative URLs, which doesn’t work in this situation.
So…to make a long story short, the only reliable way to resolve this is for the feed to not use relative links.
Nick Bradbury
Bradbury Software Support
Unfortunately I don’t control the feed. The feed is auto-generated from the web page, and the web page is my authoring target. The fact that the page is hosted on multiple domains precludes the use of an absolute URL.
//Is WM_QUIT so special that PeekMessage won’t/can’t remove it?
I have the same problem with WinXP+ServicePack1 + VC6 + SP5. Even if I put PeekMessage in a while loop and keep trying to remove the WM_QUIT message, it never finds it and the program hangs.
Yes, WM_QUIT is a special message. I’ll take up its specialness in a future entry. It looks like your only solution is to pump the message queue until the WM_QUIT pops out.