Messagebox timeout

Windows specific questions.
deltarho[1859]
Posts: 2337
Joined: Jan 02, 2017 0:34
Location: UK

Messagebox timeout

Postby deltarho[1859] » Mar 15, 2020 0:56

There are some Windows undocumented functions for timing out a messagebox.

Here is a method, ported from PowerBASIC, which does not use undocumented functions. I got the idea from a Microsoft technician about 10 years ago and have used it many times since without issue.

The Long value returned by TimedMessageBox() depends upon which button we click before a time out - the example just uses MB_OK + MB_TOPMOST.

In the following TimedMessageBox() is not called from a window, so I'm using the desktop as the parent.

Code: Select all

#include once "windows.bi"
#include once "win\winuser.bi"

Dim Shared As hWnd hTimedMsgBoxParent
Dim Shared As Boolean MsgBoxTimedOut

Function MessageBoxTimer() As Long

  MsgBoxTimedOut = True
  If hTimedMsgBoxParent <> 0 Then
    EnableWindow( hTimedMsgBoxParent, TRUE )
  End If
  PostQuitMessage 0
  Return 0

End Function

Function TimedMessageBox( ByVal hWnd As Hwnd, lpText As Zstring, lpCaption As ZString, _
                          ByVal uType As Dword, ByVal lmSec As ULong ) As Long
Dim As Long Timerbase, RetVal
Dim TimerProcPtr As Function() As Long = @MessageBoxTimer
Dim AS LPMSG Msg

  MsgBoxTimedOut = False
  hTimedMsgBoxParent = Null
 
  If Cast( Uinteger, hWnd ) And IsWindowEnabled( hWnd ) Then hTimedMsgBoxParent = hWnd

 TimerBase = SetTimer( 0, 0, lmSec, cast( TIMERPROC, TimerProcPtr ) )
  RetVal = MessageBox( hWnd, lpText, lpCaption, uType )

  KillTimer 0, TimerBase

  If MsgBoxTimedOut Then
    PeekMessage( Msg, hWnd, WM_QUIT, WM_QUIT, PM_REMOVE )
    RetVal = -1
  End If

  Function = RetVal

End Function

Function Main() As Long

 TimedMessageBox( HWND_DESKTOP, "Hi, folks - I'm only staying for three seconds.", "MsgBox timeout test", MB_OK + MB_TOPMOST, 3000 )
 Return 0
 
End Function

End Main()
Last edited by deltarho[1859] on Mar 15, 2020 4:37, edited 1 time in total.
deltarho[1859]
Posts: 2337
Joined: Jan 02, 2017 0:34
Location: UK

Re: Messagebox timeout

Postby deltarho[1859] » Mar 15, 2020 4:35

I had to make a correction.

PowerBASIC is a 32-bit compiler. Windows handles are 64-bit in 64-bit mode so I am now using

Code: Select all

Cast( Uinteger, hWnd )

so that it will compile in either 32-bit or 64-bit. Image
adeyblue
Posts: 6
Joined: Nov 07, 2019 20:08

Re: Messagebox timeout

Postby adeyblue » Mar 15, 2020 5:19

deltarho[1859] wrote:Here is a method which does not use undocumented functions. I got the idea from a Microsoft technician about 10 years ago and have used it many times since without issue.

It is technically broken though. Raymond Chen's original isn't, but this has all the parameters to the TimerProc deleted, unbalancing the stack in 32-bit mode since the compiler won't insert code to pop the 4 parameters from the stack (because the code says there aren't any). The absence of parameters is also why you need the cast.

It doesn't cause problems because Microsoft's code that calls it restores the stack pointer afterwards, which is nice of them, but that's not a thing to rely on in other code nor is it a good habit for people to get into.

Here's Chen's threadsafe better version, but 'better', as it uses one of the built-in window creation functions rather than having to do it all ourselves.

Code: Select all

#include once "windows.bi"
#include once "win\commctrl.bi"

Sub MessageBoxTimer(ByVal hwnd As Hwnd, ByVal uMsg As Dword, ByVal idEvent As UINT_PTR, ByVal dwTime As Dword)

  Dim passedPtr As Hwnd Ptr = cast(Hwnd Ptr, idEvent)
  Dim parent As Hwnd = *passedPtr
  If parent <> Null Then
    EnableWindow( parent, TRUE )
  End If
  *passedPtr = cast(Hwnd, &Hdeadf00d)
  PostQuitMessage 42

End Sub

Function TimedMessageBox( ByVal hWnd As Hwnd, lpText As Zstring, lpCaption As ZString, _
                          ByVal uType As Dword, ByVal lmSec As Dword ) As Long
  Dim As Long RetVal
  Dim As UINT_PTR TimerBase
  Dim As MSG Msg
  Dim hTimedMsgBoxParent As Hwnd = Null
  ' This can be any function that'll create a window where you can control
  ' the parent in order to set it as HWND_MESSAGE. We just need a handle to hang the timer off
  Dim hStatusWindow as Hwnd = CreateStatusWindow(WS_CHILD, "", HWND_MESSAGE, 1)
 
  If hWnd <> Null And IsWindowEnabled( hWnd ) Then hTimedMsgBoxParent = hWnd

  ' If you use a Non-Null window handle, you can use the timer id to pass things to the callback
  ' This is the only reason for the statuswindow, since people can pass Null (ie HWND_DESKTOP) to us
  ' I got the idea from a Microsoft technician last year
  ' https://devblogs.microsoft.com/oldnewthing/20191009-00/?p=102974
  TimerBase = SetTimer( hStatusWindow, cast(UINT_PTR, @hTimedMsgBoxParent), lmSec, @MessageBoxTimer)

  RetVal = MessageBox( hWnd, lpText, lpCaption, uType )

  KillTimer hStatusWindow, TimerBase
  DestroyWindow(hStatusWindow)

  If hTimedMsgBoxParent = &Hdeadf00d Then
    PeekMessage( @Msg, Null, WM_QUIT, WM_QUIT, PM_REMOVE )
    RetVal = -1
  End If

  Function = RetVal

End Function

Function Main() As Long

 TimedMessageBox( HWND_DESKTOP, "Hi, folks - I'm only staying for three seconds.", "MsgBox timeout test", MB_OK + MB_TOPMOST, 3000 )
 Return 0
 
End Function

End Main()
deltarho[1859]
Posts: 2337
Joined: Jan 02, 2017 0:34
Location: UK

Re: Messagebox timeout

Postby deltarho[1859] » Mar 15, 2020 6:52

@adeyblue

Blimey, Raymond Chen's code predates mine by about five years and no one at PowerBASIC spotted it.

It seems that I was lucky to get away with quite a bit of stuff with Windows being uncharacteristically forgiving.

At the moment I am trying to figure out why you gave the parent a dummy handle in the TIMERPROC callback.
Added: Ah, it is to do with idEvent. I need to read again "A window can’t have two timers with the same ID" link you mentioned in your code.

I see that the exit code used in PostQuitMessage is the answer to the ultimate question of life, the universe, and everything. I often use 666 when I am stuck for a number because the devil is in the detail.

I need to check your code with a fine tooth comb but, on the face it, it looks like mine is for the rubbish bin.

Thank you. Image
deltarho[1859]
Posts: 2337
Joined: Jan 02, 2017 0:34
Location: UK

Re: Messagebox timeout

Postby deltarho[1859] » Mar 15, 2020 8:56

@adeyblue

In this context I'm not sure that idEvent would be an issue. Anyway, notwithstanding that, I have replaced your &Hdeadf00d with &HFabC0de. Image
deltarho[1859]
Posts: 2337
Joined: Jan 02, 2017 0:34
Location: UK

Re: Messagebox timeout

Postby deltarho[1859] » Mar 15, 2020 9:19

It is worth mentioning that all return values of messagebox are positive so if you want to do something specific for a time out then check if the return value of TimedMessageBox() is -1.
Josep Roca
Posts: 483
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: Messagebox timeout

Postby Josep Roca » Mar 15, 2020 11:32

Although MessageBoxTimeout is undocumented, it has been available and unaltered since Windows XP. The following wrapper function loads it dynamically and, if unavailable, it defaults to a regular MessageBox.

Code: Select all

' ========================================================================================
' Displays a modal dialog box that contains a system icon, a set of buttons, and a brief
' application-specific message, such as status or error information.
' The message box times out and auto closes if the user does not respond to it first.
' The message box returns an integer value that indicates which button the user clicked.
' This is an undocuimented function available in user32.dll since Windows XP.
' This wrapper function calls MessageBoxTimeoutW dynamically. If the API function is not
' available, it calls the MessageBoxW function, which don't times out.
' ========================================================================================
PRIVATE FUNCTION AfxMessageBoxTimeout (BYVAL hwnd AS HWND, BYVAL pwszText AS LPWSTR, BYVAL pwszCaption AS LPWSTR, _
   BYVAL uType AS UINT, BYVAL wLanguageId AS WORD, BYVAL dwMilliseconds AS DWORD) AS LONG
   DIM AS ANY PTR pLib = DyLibLoad("user32.dll")
   IF pLib = NULL THEN RETURN 0
   ' // Call the undocumented MessageBoxTimeoutW function
   DIM pMessageBoxTimeout AS FUNCTION (BYVAL hwnd AS HWND, BYVAL pwszText AS LPWSTR, BYVAL pwszCaption AS LPWSTR, _
       BYVAL uType AS UINT, BYVAL wLanguageId AS WORD, BYVAL dwMilliseconds AS DWORD) AS LONG
   pMessageBoxTimeout = DyLibSymbol(pLib, "MessageBoxTimeoutW")
   IF pMessageBoxTimeout THEN
      FUNCTION = pMessageBoxTimeout(hwnd, pwszText, pwszCaption, uType, wLanguageId, dwMilliseconds)
   ELSE
      ' // Use the normal message box
      FUNCTION = MessageBoxW(hwnd, pwszText, pwszCaption, uType)
   END IF
   DyLibFree(pLib)
END FUNCTION


Usage example:

Code: Select all

DIM dwFlags AS UINT = MB_OK OR MB_SETFOREGROUND OR MB_SYSTEMMODAL OR MB_ICONINFORMATION
DIM iRet AS LONG = AfxMessageBoxTimeout(NULL, "Test a timeout of 2 seconds.", "MessageBoxTimeout Test", dwFlags, 0, 2000)
Last edited by Josep Roca on Mar 15, 2020 12:24, edited 1 time in total.
deltarho[1859]
Posts: 2337
Joined: Jan 02, 2017 0:34
Location: UK

Re: Messagebox timeout

Postby deltarho[1859] » Mar 15, 2020 12:09

José wrote:Although MessageBoxTimeout is undocumented, it has been available and unaltered since Windows XP.

Point taken. I took a similar stance with NtSetTimerResolution and NtQueryTimerResolution because they are used by timeBeginPeriod.

Found this:-

"It is unlikely that Microsoft will depricate this function for quite some time because all of the standard MessageBox API calls actually call MessageBoxTimeOutA or MessageBoxTimeoutW and pass $FFFFFFFF as the timeout period meaning the dialog will wait a very long time, approx 49 days!"

Re wLanguageID

"The language for the text displayed in the message box button(s). Specifying a value of zero (0) indicates to display the button text in the default system language."

This:-

Code: Select all

#include once "windows.bi"
 
<José's PRIVATE FUNCTION AfxMessageBoxTimeout>
 
AfxMessageBoxTimeout( HWND_DESKTOP, "Hi, folks - I'm only staying for three seconds.", "MsgBox timeout test", _
MB_OK + MB_TOPMOST, 0, 3000 )

works in both 32-bit and 64-bit mode.

The use of PRIVATE comes into its own if it is in an inc file - if it isn't used it won't get loaded.

Thanks, José

Added: José came back with 'Usage example' while I was composing this. I always compose offline which is why my posts are perfect. Image
Josep Roca
Posts: 483
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: Messagebox timeout

Postby Josep Roca » Mar 15, 2020 12:26

I have modified my post because the wrapper was using a wrong logic. It was calling the default MessageBox if the call to DyLibLoad failed, when it must do it if the call to DyLibSymbol fails.
Josep Roca
Posts: 483
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: Messagebox timeout

Postby Josep Roca » Mar 15, 2020 12:41

"It is unlikely that Microsoft will depricate this function for quite some time because all of the standard MessageBox API calls actually call MessageBoxTimeOutA or MessageBoxTimeoutW and pass $FFFFFFFF as the timeout period meaning the dialog will wait a very long time, approx 49 days!"


More precisely:

Code: Select all

MessageBox
   |_ MessageBoxEx
      |_ MessageBoxIndirect
         |_ MessageBoxTimeoutIndirect

MessageBoxTimeout
   |_ MessageBoxTimeoutIndirect



MessageBoxA calls:

Code: Select all

return MessageBoxExA(hWnd, lpText, lpCaption, uType, LANG_NEUTRAL);


MessageBoxExA calls:

Code: Select all

{
  MSGBOXPARAMSA msgbox;

  msgbox.cbSize = sizeof(msgbox);
  msgbox.hwndOwner = hWnd;
  msgbox.hInstance = 0;
  msgbox.lpszText = lpText;
  msgbox.lpszCaption = lpCaption;
  msgbox.dwStyle = uType;
  msgbox.lpszIcon = NULL;
  msgbox.dwContextHelpId = 0;
  msgbox.lpfnMsgBoxCallback = NULL;
  msgbox.dwLanguageId = wLanguageId;

  return MessageBoxIndirectA(&msgbox);
}


MessageBoxIndirectA calls:

Code: Select all

{
  MSGBOXPARAMSW msgboxW;
  UNICODE_STRING textW, captionW, iconW;
  int ret;

  if (!IS_INTRESOURCE(lpMsgBoxParams->lpszText))
  {
      RtlCreateUnicodeStringFromAsciiz(&textW, (PCSZ)lpMsgBoxParams->lpszText);
      /*
       * UNICODE_STRING objects are always allocated with an extra byte so you
       * can null-term if you want
       */
      textW.Buffer[textW.Length / sizeof(WCHAR)] = L'\0';
  }
  else
      textW.Buffer = (LPWSTR)lpMsgBoxParams->lpszText;

  if (!IS_INTRESOURCE(lpMsgBoxParams->lpszCaption))
  {
      RtlCreateUnicodeStringFromAsciiz(&captionW, (PCSZ)lpMsgBoxParams->lpszCaption);
      /*
       * UNICODE_STRING objects are always allocated with an extra byte so you
       * can null-term if you want
       */
      captionW.Buffer[captionW.Length / sizeof(WCHAR)] = L'\0';
  }
  else
      captionW.Buffer = (LPWSTR)lpMsgBoxParams->lpszCaption;

  if (lpMsgBoxParams->dwStyle & MB_USERICON)
  {
      if (!IS_INTRESOURCE(lpMsgBoxParams->lpszIcon))
      {
          RtlCreateUnicodeStringFromAsciiz(&iconW, (PCSZ)lpMsgBoxParams->lpszIcon);
          /*
           * UNICODE_STRING objects are always allocated with an extra byte so you
           * can null-term if you want
           */
          iconW.Buffer[iconW.Length / sizeof(WCHAR)] = L'\0';
      }
      else
          iconW.Buffer = (LPWSTR)lpMsgBoxParams->lpszIcon;
  }
  else
      iconW.Buffer = NULL;

  msgboxW.cbSize = sizeof(msgboxW);
  msgboxW.hwndOwner = lpMsgBoxParams->hwndOwner;
  msgboxW.hInstance = lpMsgBoxParams->hInstance;
  msgboxW.lpszText = textW.Buffer;
  msgboxW.lpszCaption = captionW.Buffer;
  msgboxW.dwStyle = lpMsgBoxParams->dwStyle;
  msgboxW.lpszIcon = iconW.Buffer;
  msgboxW.dwContextHelpId = lpMsgBoxParams->dwContextHelpId;
  msgboxW.lpfnMsgBoxCallback = lpMsgBoxParams->lpfnMsgBoxCallback;
  msgboxW.dwLanguageId = lpMsgBoxParams->dwLanguageId;

  ret = MessageBoxTimeoutIndirectW(&msgboxW, (UINT)-1);

  if (!IS_INTRESOURCE(lpMsgBoxParams->lpszText))
      RtlFreeUnicodeString(&textW);

  if (!IS_INTRESOURCE(lpMsgBoxParams->lpszCaption))
      RtlFreeUnicodeString(&captionW);

  if ((lpMsgBoxParams->dwStyle & MB_USERICON) && !IS_INTRESOURCE(iconW.Buffer))
      RtlFreeUnicodeString(&iconW);

  return ret;
}
Josep Roca
Posts: 483
Joined: Sep 27, 2016 18:20
Location: Valencia, Spain

Re: Messagebox timeout

Postby Josep Roca » Mar 15, 2020 12:47

As it is the norm in Windows, MessageBoxTimeoutIndirectA delegates the work to MessageBoxTimeoutIndirectW:

Code: Select all

{
  MSGBOXPARAMSW msgboxW;
  UNICODE_STRING textW, captionW;
  int ret;

  if (!IS_INTRESOURCE(lpText))
      RtlCreateUnicodeStringFromAsciiz(&textW, (PCSZ)lpText);
  else
      textW.Buffer = (LPWSTR)lpText;

  if (!IS_INTRESOURCE(lpCaption))
      RtlCreateUnicodeStringFromAsciiz(&captionW, (PCSZ)lpCaption);
  else
      captionW.Buffer = (LPWSTR)lpCaption;

  msgboxW.cbSize = sizeof(msgboxW);
  msgboxW.hwndOwner = hWnd;
  msgboxW.hInstance = 0;
  msgboxW.lpszText = textW.Buffer;
  msgboxW.lpszCaption = captionW.Buffer;
  msgboxW.dwStyle = uType;
  msgboxW.lpszIcon = NULL;
  msgboxW.dwContextHelpId = 0;
  msgboxW.lpfnMsgBoxCallback = NULL;
  msgboxW.dwLanguageId = wLanguageId;

  ret = MessageBoxTimeoutIndirectW(&msgboxW, (UINT)dwTimeout);

  if (!IS_INTRESOURCE(textW.Buffer))
      RtlFreeUnicodeString(&textW);

  if (!IS_INTRESOURCE(captionW.Buffer))
      RtlFreeUnicodeString(&captionW);

  return ret;
}


and MessageBoxTimeoutIndirectW:

Code: Select all

{
     int ret = 0;
     UINT i;
     LPWSTR defCaption = NULL;
     MSGBOXDATA mbd;
     MSGBTNINFO Buttons;
     LPCWSTR ButtonText[MSGBOXEX_MAXBTNS];
 
     // TODO: Check whether the caller is an NT 3.x app and if so, check
     // instead for the MB_SERVICE_NOTIFICATION_NT3X flag and adjust it.
     if (lpMsgBoxParams->dwStyle & MB_SERVICE_NOTIFICATION)
     {
         NTSTATUS Status;
         UNICODE_STRING CaptionU, TextU;
         ULONG Response = ResponseNotHandled; /* HARDERROR_RESPONSE */
         ULONG_PTR MsgBoxParams[4] =
         {
             (ULONG_PTR)&TextU,
             (ULONG_PTR)&CaptionU,
             /*
              * Retrieve the message box flags. Note that we filter out
              * MB_SERVICE_NOTIFICATION to not enter an infinite recursive
              * loop when we will call MessageBox() later on.
              */
             lpMsgBoxParams->dwStyle & ~MB_SERVICE_NOTIFICATION,
             dwTimeout
         };
 
         /* hwndOwner must be NULL */
         if (lpMsgBoxParams->hwndOwner != NULL)
         {
             ERR("MessageBoxTimeoutIndirectW(MB_SERVICE_NOTIFICATION): hwndOwner is not NULL!\n");
             return 0;
         }
 
         //
         // FIXME: TODO: Implement the special case for Terminal Services.
         //
 
         RtlInitUnicodeString(&CaptionU, lpMsgBoxParams->lpszCaption);
         RtlInitUnicodeString(&TextU, lpMsgBoxParams->lpszText);
 
         Status = NtRaiseHardError(STATUS_SERVICE_NOTIFICATION | HARDERROR_OVERRIDE_ERRORMODE,
                                   ARRAYSIZE(MsgBoxParams),
                                   (1 | 2),
                                   MsgBoxParams,
                                   OptionOk, /* NOTE: This parameter is ignored */
                                   &Response);
         if (!NT_SUCCESS(Status))
         {
             ERR("MessageBoxTimeoutIndirectW(MB_SERVICE_NOTIFICATION): NtRaiseHardError failed, Status = 0x%08lx\n", Status);
             return 0;
         }
 
         /* Map the returned response to the buttons */
         switch (Response)
         {
             /* Not handled */
             case ResponseReturnToCaller:
             case ResponseNotHandled:
                 break;
 
             case ResponseAbort:
                 return IDABORT;
             case ResponseCancel:
                 return IDCANCEL;
             case ResponseIgnore:
                 return IDIGNORE;
             case ResponseNo:
                 return IDNO;
             case ResponseOk:
                 return IDOK;
             case ResponseRetry:
                 return IDRETRY;
             case ResponseYes:
                 return IDYES;
             case ResponseTryAgain:
                 return IDTRYAGAIN;
             case ResponseContinue:
                 return IDCONTINUE;
 
             /* Not handled */
             default:
                 break;
         }
         return 0;
     }
 
     ZeroMemory(&mbd, sizeof(mbd));
     memcpy(&mbd.mbp, lpMsgBoxParams, sizeof(mbd.mbp));
     mbd.wLanguageId = (WORD)lpMsgBoxParams->dwLanguageId;
     mbd.dwTimeout   = dwTimeout;
 
     if (!mbd.mbp.lpszCaption)
     {
         /* No caption, use the default one */
         LoadAllocStringW(User32Instance,
                          IDS_ERROR,
                          &defCaption,
                          L"Error");
         mbd.mbp.lpszCaption = (defCaption ? defCaption : L"Error");
     }
 
     /* Create the selected buttons; unknown types will fall back to MB_OK */
     i = (lpMsgBoxParams->dwStyle & MB_TYPEMASK);
     if (i >= ARRAYSIZE(MsgBtnInfo))
         i = MB_OK;
 
     /* Get the buttons IDs */
     Buttons = MsgBtnInfo[i];
 
     /* Add the Help button */
     if (lpMsgBoxParams->dwStyle & MB_HELP)
     {
         Buttons.btnIdx[Buttons.btnCnt] = IDHELP;
         Buttons.btnIds[Buttons.btnCnt] = IDS_HELP;
         Buttons.btnCnt++;
     }
 
     ASSERT(Buttons.btnCnt <= MSGBOXEX_MAXBTNS);
 
     /* Retrieve the pointers to the button labels and find the Cancel button */
     mbd.uCancelId = (i == MB_OK ? IDOK : 0);
     for (i = 0; i < Buttons.btnCnt; ++i)
     {
         // FIXME: Use the strings in the correct language.
         // MB_GetString gives the string in default system language.
         ButtonText[i] = MB_GetString(Buttons.btnIds[i] - IDS_OK); /* or: Buttons.btnIdx[i] - IDOK */
 #if 0
         LoadAllocStringW(User32Instance,
                          Buttons.btnIds[i],
                          &ButtonText[i],
                          L"");
 #endif
         if (Buttons.btnIdx[i] == IDCANCEL)
             mbd.uCancelId = IDCANCEL;
     }
 
     mbd.pidButton = Buttons.btnIdx;
     mbd.ppszButtonText = ButtonText;
     mbd.dwButtons = Buttons.btnCnt;
 
     mbd.uDefButton = ((lpMsgBoxParams->dwStyle & MB_DEFMASK) >> 8);
     /* Make the first button the default button if none other is */
     if (mbd.uDefButton >= mbd.dwButtons)
         mbd.uDefButton = 0;
 
     /* Call the helper function */
     ret = SoftModalMessageBox(&mbd);
 
 #if 0
     for (i = 0; i < mbd.dwButtons; i++)
     {
         if (ButtonText[i] && *ButtonText[i])
             RtlFreeHeap(RtlGetProcessHeap(), 0, ButtonText[i]);
     }
 #endif
 
     if (defCaption)
         RtlFreeHeap(RtlGetProcessHeap(), 0, defCaption);
 
     return ret;
 }


Please note that the above code comes from Wine / ReactOS, not from Microsoft.
deltarho[1859]
Posts: 2337
Joined: Jan 02, 2017 0:34
Location: UK

Re: Messagebox timeout

Postby deltarho[1859] » Mar 15, 2020 13:05

@José

I have just belted through your last two posts and now need a lie down in a darkened room. Not a problem because I often have a nap in the afternoon. Image

Return to “Windows”

Who is online

Users browsing this forum: No registered users and 6 guests