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.
0 comments