Centring message boxes plus timeout.

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

Centring message boxes plus timeout.

Postby deltarho[1859] » Mar 31, 2017 2:13

Got more than one monitor? Having your application running on the secondary monitor and message boxes popping up in the centre of the main monitor is a distraction. How about two or three applications running on one monitor and all their messages popping up in the centre of the desktop.

CenteredMessageBox will place an application's message boxes in the centre of it's parent. Just replace MessageBox with CenteredMessageBox and you are done - the parameter list is the same for both.

The code for centring was written by Bernhard Fomm at the PowerBASIC forums.

However, there are times when we don't need a message box to wait for a response; with MB_IconError or MB_IconInformation for example. So, I added some code so that the message box times out.

TimedMessageBox will do just that. The parameter list is the same as MessageBox with two extra parameters: Center - If true then centred on parent else on the desktop; lmSec - timed in milliseconds.

If you have something like this on clicking the Close button (WM_SYSCOMMAND > SC_CLOSE)

Code: Select all

MessageBox( hWnd, "Don't forget to, whatever", "MyApp", MB_IconInformation + MB_OK )

you could use

Code: Select all

TimedMessageBox( hWnd, "Don't forget to, whatever", "MyApp", MB_IconInformation + MB_OK, True, 5000 )

The message will appear in the centre of the dialog, will be removed after five seconds and then the application will close; unless we click on OK beforehand.

That is it!

Code: Select all

#Include Once "windows.bi"

Dim Shared hHookMsgBox As HHook
Dim Shared hTimedMsgBoxParent As HWND
Dim Shared hMsgBoxParent As HWND
Dim Shared MsgBoxTimedOut As Long

Function MessageBoxTimer() As Long
 
  MsgBoxTimedOut = True
  If hTimedMsgBoxParent <> Null Then
    EnableWindow( hTimedMsgBoxParent, TRUE )
  End If
 
  PostQuitMessage 0
 
  Return 0

End Function

Function CenteredMessageBox_CB ( lMsg As Long, wParam As HWND, lParam As ulong ) As Long
Dim As Rect rc1, rc2
Dim As Long x, y

  If lMsg = HCBT_ACTIVATE Then
 
    If hMsgBoxParent <> HWND_DESKTOP Then
      GetWindowRect hMsgBoxParent, Cast( LPRECT, @rc1)
      GetWindowRect wParam, Cast( LPRECT, @rc2)
      x = (rc1.Left + rc1.Right - rc2.Right + rc2.Left) / 2
      y = (rc1.Top + rc1.Bottom - rc2.Bottom + rc2.Top) / 2
      SetWindowPos wParam, HWND_TOPMOST, x, y, 0, 0, SWP_NOSIZE
    End If
   
    UnhookWindowsHookEx hHookMsgBox
   
    Return 0

  End If
 
End Function

Function CenteredMessageBox ( hWindow As HWND, _ ' Parent window handle
                              lpText As ZString, _ ' The message to be displayed
                              lpCaption As ZString, _ ' The message box caption
                              uType As Long _ ' Specifies the contents and behavior of the dialog box - MB_OK and so on
                            ) As Long
 
  hMsgBoxParent = hWindow
  hHookMsgBox = SetWindowsHookEx( WH_CBT, Cast( HOOKPROC, @CenteredMessageBox_CB), GetModuleHandle( 0 ), GetCurrentThreadId )
 
  Function = MessageBox( hWindow, lpText, lpCaption, uType )
 
End Function

Function TimedMessageBox( hWindow As HWND, _ ' As above
                          lpText As ZString, _ ' As above
                          lpCaption As ZString, _ ' As above
                          uType As ULong, _ ' As above
                          Center As Long, _ ' If true then centred on hWindow else desktop - redundant if hWindow = HWND_DESKTOP
                          lmSec As Long _ ' Timed in milliseconds
                        ) As Long
                       
Dim As Long RetVal, TimerBase
Dim TimerProcPtr As TIMERPROC
Dim Msg As LPMSG

  MsgBoxTimedOut = False
  hTimedMsgBoxParent = Null
 
  If hWindow Andalso IsWindowEnabled(hWindow) Then hTimedMsgBoxParent = hWindow
 
  If lmSec <= 0 Then lmSec = 5000 ' just in case <smile>
  TimerProcPtr = Cast( TIMERPROC, @MessageBoxTimer)
  TimerBase = SetTimer( 0, 0, lmSec, TimerProcPtr )
  If Center Andalso hWindow <> HWND_DESKTOP Then
    RetVal = CenteredMessageBox( hWindow, lpText, lpCaption, uType )
  Else
    RetVal = MessageBox( hWindow, lpText, lpCaption, uType )
  End If

  KillTimer 0, TimerBase
 
  If MsgBoxTimedOut Then
    PeekMessage( Msg, Null, WM_QUIT, WM_QUIT, PM_REMOVE )
    RetVal = -1
  End If
 
  Function = RetVal
   
End Function
deltarho[1859]
Posts: 532
Joined: Jan 02, 2017 0:34
Location: UK

Re: Centring message boxes plus timeout.

Postby deltarho[1859] » Mar 31, 2017 12:02

I saved the above code as "CTM.bas"

Here is an example of a timed message box centred in the console window.

Code: Select all

#Include Once "CTM.bas"

Dim As HWND hConsole

hConsole = GetConsoleWindow

Print "Some heavy duty output"

TimedMessageBox( hConsole, "I have been going for a while - put the kettle on.", "I am thirsty", MB_OK, True, 5000 )

Sleep

which gave me this with the message box displaying for five seconds.

Image

As an experiment I had the Print done ten times before and after TimedMessageBox and got what I expected: Ten prints, the message box displaying for five seconds and then a further ten prints.
deltarho[1859]
Posts: 532
Joined: Jan 02, 2017 0:34
Location: UK

Re: Centring message boxes plus timeout.

Postby deltarho[1859] » Mar 31, 2017 16:30

I forgot to mention that in the event of a time out TimedMessageBox returns -1 as opposed to a positive value indicating which button, if any, that we clicked on.

Run the following and fold your arms - you will get a second message box saying "I timed out earlier."

Run it again, only this time click the first message box's OK button before it times out - you will now get a second message box saying "You beat me to it earlier."

The first message box will time out in five seconds and the second message will time out in three seconds.

Not that important but it may be useful to an application to know whether we responded or not.

Code: Select all

#Include Once "CTM.bas"

Dim As HWND hConsole
Dim as Long i, MsgReturn

hConsole = GetConsoleWindow

For i = 1 to 10
  Print "Some heavy duty output"
Next

MsgReturn = TimedMessageBox( hConsole, "I have been going for a while - put the kettle on.", "I am thirsty", MB_OK, True, 5000 )

For i = 1 to 10
  Print "More heavy duty output"
Next

If MsgReturn = -1 Then
  TimedMessageBox( hConsole, "I timed out earlier.", "Time Out", MB_OK, True, 3000 )
Else
  TimedMessageBox( hConsole, "You beat me to it earlier.", "Time Out", MB_OK, True, 3000 )
End If

Sleep
MrSwiss
Posts: 1928
Joined: Jun 02, 2013 9:27
Location: Switzerland

Re: Centring message boxes plus timeout.

Postby MrSwiss » Mar 31, 2017 22:13

@deltarho[1859],

I don't understand *Center As Long* below:

Code: Select all

Function TimedMessageBox( hWindow As HWND, _ ' As above
                          lpText As ZString, _ ' As above
                          lpCaption As ZString, _ ' As above
                          uType As ULong, _ ' As above
                          Center As Long, _ ' If true then centred on hWindow else desktop - redundant if hWindow = HWND_DESKTOP
                          lmSec As Long _ ' Timed in milliseconds
                        ) As Long
I'd use Center As Boolean (only 1 Byte, instead of 4 Byte), then the initialization with TRUE/FALSE makes
more sense. (TRUE/FALSE was introduced for use with Boolean variables)
In the days before Boolean variables introduction (FBC 1.04.0) the use of 0/1 (Long) was common (as a sort
of replacement).
deltarho[1859]
Posts: 532
Joined: Jan 02, 2017 0:34
Location: UK

Re: Centring message boxes plus timeout.

Postby deltarho[1859] » Apr 01, 2017 18:17

Look, I do the ideas. For the boring stuff like datatypes they are normally handled by my assistant, but she is on holiday. <laugh>

It took a while to convert from PowerBASIC to FreeBASIC but looking at Center again and I find that it is not required.

As most of you probably know the desktop is actually a window and it has it's own handle, HWND_DESKTOP. So, if we wanted a timed message box centred on the desktop all we need do is use HWND_DESKTOP as the first parameter of TimedMessageBox.

So, remove Center from the parameter list. Into the function we find 'If Center Andalso hWindow <> HWND_DESKTOP Then'. Change that to 'If hWindow <> HWND_DESKTOP Then'.

Instead of HWND_DESKTOP we could use Null or 0. I advise against that - it is bad practice and puts us on a slippery slope.

While I am at it I am going to suggest changing 'lmSec As Long' to 'lmSec As UShort'. UShort has a range from 0 to 65535. If we inadvertently use a negative value then it will wrap back into the range. If we use a value in excess of 65535 then it too will wrap back into the range. 65,535 milli-seconds is just over 65 seconds and far greater than we need for a short lived message box. If we do this then we can remove 'If lmSec <= 0 Then lmSec = 5000 ' just in case <smile>' which is just a few statements into TimedMessageBox.

My assistant has just 'phoned me. She said that she has met someone, will be getting married and will stay were she is. I said "Congratulations. Who is going to sort out my datatypes now?". She responded with "As if I care!". You just cannot get the staff can you?
dodicat
Posts: 4340
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Centring message boxes plus timeout.

Postby dodicat » Apr 01, 2017 22:27

There is a messageboxtimeout in user32.dll
http://edn.embarcadero.com/article/32736

example:

Code: Select all



declare function msgbox lib "user32.dll" alias "MessageBoxTimeoutA"(as any ptr,as zstring,as zstring,as long,as long,as long)  as long
msgbox(0,"Wait 2 seconds","Hello",0,0,2000)
sleep
 

I think from Vista onwards, but not sure about this.
deltarho[1859]
Posts: 532
Joined: Jan 02, 2017 0:34
Location: UK

Re: Centring message boxes plus timeout.

Postby deltarho[1859] » Apr 02, 2017 12:18

I played with MessageBoxTimeout a few years ago but not in the context of centring message boxes as well.

I replaced

Code: Select all

Function CenteredMessageBox( hWindow As HWND, _ ' Parent window handle
                              lpText As ZString, _ ' The message to be displayed
                              lpCaption As ZString, _ ' The message box caption
                              uType As Long _ ' Specifies the contents and behavior of the dialog box - MB_OK and so on
                           ) As Long
 
  hMsgBoxParent = hWindow
  hHookMsgBox = SetWindowsHookEx( WH_CBT, Cast( HOOKPROC, @CenteredMessageBox_CB), GetModuleHandle( 0 ), GetCurrentThreadId )
 
  Function = MessageBox( hWindow, lpText, lpCaption, uType )
 
End Function

with

Code: Select all

Function CenteredMessageBox( hWindow As HWND, lpText As ZString, lpCaption As ZString, uType As Long, Dummy as Long, lmSec As Long ) As Long
 
  hMsgBoxParent = hWindow
  hHookMsgBox = SetWindowsHookEx( WH_CBT, Cast( HOOKPROC, @CenteredMessageBox_CB), GetModuleHandle( 0 ), GetCurrentThreadId )
 
  Function = MsgBox( hWindow, lpText, lpCaption, uType, 0, lmSec )
 
End Function

and it worked fine.

I have read that the time out with MessageBoxTimeout tops out at 49 days. This tells me it is using lmSec as Ulong which will top out at 49.7 days, as does GetTickCount. Strictly speaking it should be called MessageBoxExTimeout because the fifth parameter uses wLanguageId as does MessageBoxEx whereas MessageBox does not.

However, there is a problem and that is MessageBoxTimeout is undocumented, introduced in Windows XP, and I am loathed to dispense with my two timer related functions, which can be looked at and checked by anyone, in favour of an undocumented API. Having said that I am no-one's keeper so if anyone wants to change my code then be my guest.

That 49.7 days gave me an idea.

If we revert back to 'lmSec As Ulong' and '#Define INFINITY 4294967295' we can forego using CenteredMessageBox and use TimedMessageBox for everything.

The following uses hParent as a handle to either Parent as a GUI window or a Console window.

This gives a centred message box on the parent with a time out.

Code: Select all

TimedMessageBox( hParent, "Centred on Parent", "With time out", MB_OK, 3000 )

This gives a centred message box on the parent without a time out.

Code: Select all

TimedMessageBox( hParent, "Centred on Parent", "Without time out", MB_OK, INFINITY )

This gives a centred message box on the desktop with a time out.

Code: Select all

TimedMessageBox( HWND_DESKTOP, "Centred on Desktop", "With time out", MB_OK, 3000 )

This gives a centred message box on the desktop without a time out.

Code: Select all

TimedMessageBox( HWND_DESKTOP, "Centred on Desktop", "Without time out", MB_OK, INFINITY )

The last one is redundant because, this side of 49.7 days, it behaves the same as MessageBox.

I often use the first type to remind me of something when I close an application - such as clearing the clipboard of sensitive data. I use the second type when an application definitely wants me to read it. I use the third type in an email checker I wrote. My Inbox is checked periodically and a message pops up briefly in the centre of the screen to let me know that I have mail.
dodicat
Posts: 4340
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: Centring message boxes plus timeout.

Postby dodicat » Apr 02, 2017 16:13

The centring is your baby, I have only declared MessageBoxTimeout.
It is undocumented in msdn online help, but it is discussed widely on the great net, and used often it seems!
Cheers!
deltarho[1859]
Posts: 532
Joined: Jan 02, 2017 0:34
Location: UK

Re: Centring message boxes plus timeout.

Postby deltarho[1859] » Apr 02, 2017 18:10

The centring is your baby

It was until I posted it when it became everybody's baby. That is how I see programming forums. If anyone uses some of my code in work published here then I want to be credited for it. If they screw up then "it has nothing to do with me, guv". <smile>

Just found this at Code Project.
You may be thinking what reassurances exist that someday Microsoft may remove this function. In all honesty, there are none, however, it's interesting to note that internally, all the documented MessageBox* functions call the MessageBoxTimeout API and simply pass 0xFFFFFFFF as the timeout period (a very long time), so the probability of it being removed is minimal.

That is definitely food for thought. It reminds me of RtlGenRandom, which was also introduced in Widows XP, where Microsoft writes "It may be altered or unavailable in subsequent versions.". It is used in at least one of their flagship products so woe betide them if it was made unavailable.

Anyway, I'll stay with my code as that is not going anywhere and it couldn't be anymore documented being source code.

When I thought of using INFINITY I did a search for it in the bi files and could not find it. Sign of old age I am afraid as I should have done a search for INFINITE, as may be used in WaitForSingleObject. INFINITE is 'const INFINITE = &hffffffff' in winbase.bi which is, in turn, referenced in windows.bi so my '#Define INFINITY 4294967295' can be removed and INFINITY replaced by INFINITE. With regard WaitForSingleObject Microsoft writes "If dwMilliseconds is INFINITE, the function will return only when the object is signaled." That should end with "or 49.7 days have elapsed whichever occurs first". However, that would totally ruin the illusion so I think we can let Microsoft off on that one.

Of course, we can always define INFINITY as 2^64 - 1 and that 49.7 days becomes 584,542,046 years. Shall I do that? No? Just a thought. <HaHa>

Return to “Windows”

Who is online

Users browsing this forum: No registered users and 1 guest