Modality, part 8: A timed MessageBox, the better version

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 TOOLATEINFO. But how to pass this state to the timer callback? You can't pass any parameters to timer callbacks.

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 hwndOwner window because we don't know how to pick a timer ID that doesn't conflict with an existing one.

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 hwndOwner. The scratch window is a convenient window to use. We don't pass an interesting window procedure to CreateScratchWindow because there is no need; all we wanted was a window to own our timer.


Comments (16)
  1. Adrian says:

    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.)

  2. hmmmmmm says:

    It worked for me. I have the same configuration.

  3. Raymond Chen says:

    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

  4. Raymond Chen says:

    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…

  5. meh says:

    Spy++ can display the names of registered messages. Not sure how it does that.

  6. Igor Tandetnik says:

    Registered messages are implemented as atoms. See GlobalGetAtomName

  7. Adrian says:

    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?

  8. Brian says:

    Why not just use "__declspec( thread )"? or the less handy Tls* functions if you’re running in a dll?

  9. 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.

  10. bc says:

    Admin note: something’s wrong with the links. In my feed reader, they all point to my local harddrive.

    Who’s fault is it?

  11. Raymond Chen says:

    Probably because your reader can’t handle relative links.

  12. Luckie says:

    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.

  13. bc says:

    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

  14. Raymond Chen says:

    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.

  15. xge says:

    //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.

  16. Raymond Chen says:

    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.

Comments are closed.


*DISCLAIMER: I DO NOT OWN THIS CONTENT. If you are the owner and would like it removed, please contact me. The content herein is an archived reproduction of entries from Raymond Chen's "Old New Thing" Blog (most recent link is here). It may have slight formatting modifications for consistency and to improve readability.

WHY DID I DUPLICATE THIS CONTENT HERE? Let me first say this site has never had anything to sell and has never shown ads of any kind. I have nothing monetarily to gain by duplicating content here. Because I had made my own local copy of this content throughout the years, for ease of using tools like grep, I decided to put it online after I discovered some of the original content previously and publicly available, had disappeared approximately early to mid 2019. At the same time, I present the content in an easily accessible theme-agnostic way.

The information provided by Raymond's blog is, for all practical purposes, more authoritative on Windows Development than Microsoft's own MSDN documentation and should be considered supplemental reading to that documentation. The wealth of missing details provided by this blog that Microsoft could not or did not document about Windows over the years is vital enough, many would agree an online "backup" of these details is a necessary endeavor. Specifics include:

<-- Back to Old New Thing Archive Index