Users do not read dialogs. They. Do. Not. Neither do we developers (admit it). Dialogs are scary things that pop to the user asking to make some kind of selection / decision. Users mostly simply click the little [x] button and hope no further questions would be raised. Regression: do you know what happens in your code if you display a [Yes|No] dialog and the user closes it by clicking on the [x] button?
Among various types of dialogs (and the information inside) you present to the user, be it a warning, error or information, one type actually requires no user input. Such dialogs are those displaying only the OK (or any single) button. It is questionable should you even have a dialog stating something to the user where the only possible user action is to click on the only button (+ the [x]) displayed by the dialog. Anyhow, I’m certain you have (at least I do) lots of those (…,mtInformation, [mbOk], …) scattered around your code.
I’m talking about stuff like MessageDlg(‘Your action has succeeded’, mtInformation, [mbOK], -1);
Do we even really need this kind of message being delivered to the user? I guess we could decide to display a message only if something went wrong. However, I have used (and am certain you have) and am still presenting such questionably informative messages to the user – more than I should be probably.
What if we want to help the user making a decision when such a dialog is displayed – as after all there’s no decision to be done – only the OK button to be clicked on. Time is precious, and if we would to automatically close the dialog even if the user has not yet clicked the OK button – no harm would be done. What’s more I even got asked by some users: could you make those info dialogs disappear after a few seconds?
Timing Out a Message Dialog
Depending on the type of dialog you present to the user there are various ways how you can achieve for the dialog to get auto-closed after some time. You could place a progress bar inside a standard dialog box and close the box after a few seconds. This approach has some advantages but also has one big disadvantage. It uses Delphi’s internal CreateMessageDialog – and this one would look “ugly” if you (/users) have themes enabled (Windows XP and later). Such approach would not result in nice native dialogs being displayed.
If your application is running on newer Windows, MessageDlg internally creates (calling CreateMessageDialog) and displays a standard Delphi form (though set to look and work as a dialog). If your application is running on at least Windows Vista and runtime themes are enabled and used by Windows, MessageDlg will internally create the task dialog.
Another way to auto close a dialog box would be to use the undocumented Windows API: MessageBoxTimeOut. Being undocumented, and even having that all who tried said it is working as expected, I’d rather not go undocumented if another approach is possible.
Finally, here’s one solution that works and involves using a Windows timer to post a message to the dialog window to close itself after some specified amount of time.
In short, the code would look like:
function MessageDlgTimed(const Msg: string; DlgType: TMsgDlgType; Buttons: TMsgDlgButtons; HelpCtx: Longint): Integer; const closePeriod = 2 * 1000; var timerCloseId: UINT_PTR; procedure CloseMessageDlgCallback(AWnd: HWND; AMsg: UINT; AIDEvent: UINT_PTR; ATicks: DWORD); stdcall; var activeWnd: HWND; begin KillTimer(AWnd, AIDEvent); activeWnd := GetActiveWindow; if IsWindow(activeWnd) AND IsWindowEnabled(activeWnd) then PostMessage(activeWnd, WM_CLOSE, 0, 0); end; (*CloseMessageDlgCallback *) begin timerCloseId := SetTimer(0, 0, closePeriod, @CloseMessageDlgCallback); result := MessageDlg(Msg, DlgType, Buttons, HelpCtx); if timerCloseId <> 0 then KillTimer(0, timerCloseId); end;
The SetTimer API is used to call a function CloseMessageDlgCallback after closePeriod seconds (in this case: 2 * 1000 = 2 seconds).
The MessageDlg is used to present the dialog to the user.
If the user does not close the dialog during the 2 seconds period, the CloseMessageDlgCallback would sent the WM_CLOSE message to the dialog – and the dialog would “auto-close”.
The idea for the above comes from this article: A timed MessageBox, the cheap version.
Timing Out a Message Dialog – Providing Some Visual Feedback
While the above code works as expected, the user has no idea that the dialog being presented would at some point auto-close. Ok, you could add something like “this message will self-destruct in 2 seconds” – but this will scare the user even more (if the user is even reading what the dialog has to say).
Here’s a more complex version that will provide a kind of “closing” feedback to the user in the dialog title / caption.
The idea is for the title of the dialog to change from: “Information ……..”, to “Information …..” and finally to “Information” – that is removing one dot from the title every quarter of a second. Not too attractive as a feedback, but does the trick – I let you come up with a more eye-candy solution 🙂
function MessageDlgTimed(const Msg: string; DlgType: TMsgDlgType; Buttons: TMsgDlgButtons; HelpCtx: Longint): Integer; const closePeriod = 2 * 1000; tickPeriod = 250; var timerCloseId, timerTickId: UINT_PTR; r : integer; peekMsg : TMsg; procedure CloseMessageDlgCallback(AWnd: HWND; AMsg: UINT; AIDEvent: UINT_PTR; ATicks: DWORD); stdcall; var activeWnd: HWND; begin KillTimer(AWnd, AIDEvent); activeWnd := GetActiveWindow; if IsWindow(activeWnd) AND IsWindowEnabled(activeWnd) then PostMessage(activeWnd, WM_CLOSE, 0, 0); end; (*CloseMessageDlgCallback*) procedure PingMessageDlgCallback(AWnd: HWND; AMsg: UINT; AIDEvent: UINT_PTR; ATicks: DWORD); stdcall; var activeWnd: HWND; wCaption : string; wCaptionLength : integer; begin activeWnd := GetActiveWindow; if IsWindow(activeWnd) AND IsWindowEnabled(activeWnd) AND IsWindowVisible(activeWnd) then begin wCaptionLength := GetWindowTextLength(activeWnd); SetLength(wCaption, wCaptionLength); GetWindowText(activeWnd, PChar(wCaption), 1 + wCaptionLength); SetWindowText(activeWnd, Copy(wCaption, 1, -1 + Length(wCaption))); end else KillTimer(AWnd, AIDEvent); end; (*PingMessageDlgCallback*) begin if (DlgType = mtInformation) AND ([mbOK] = Buttons) then begin timerCloseId := SetTimer(0, 0, closePeriod, @CloseMessageDlgCallback); if timerCloseId <> 0 then begin timerTickId := SetTimer(0, 0, tickPeriod, @PingMessageDlgCallback); if timerTickId <> 0 then r := HookResourceString(@SMsgDlgInformation, PChar(SMsgDlgInformation + ' ' + StringOfChar('.', closePeriod div tickPeriod))); end; result := MessageDlg(Msg, DlgType, Buttons, HelpCtx); if timerTickId <> 0 then begin KillTimer(0, timerTickId); UnHookResourceString(@SMsgDlgInformation, r); end; if timerCloseId <> 0 then KillTimer(0, timerCloseId); end else result := MessageDlg(Msg, DlgType, Buttons, HelpCtx); end;
Here, I’m ensuring only to use timed approach if the dialog is of mtInformation type and only displays the Ok button.
Now we have 2 timers. One to close the dialog after 2 seconds, and another one to alter the title of the dialog every 250 milliseconds – as a kind of a visual feedback to the user.
Note: I’m also using a custom HookResourceString function to alter the default caption of the dialog once it gets displayed. You can use the same approach to alter the text that appears on buttons. Like, go from “Ok” to “Ok (will auto close in 2 seconds)”.
Here’s the code:
function HookResourceString(ResStringRec: pResStringRec; NewStr: pChar) : integer ; var OldProtect: DWORD; begin VirtualProtect(ResStringRec, SizeOf(ResStringRec^), PAGE_EXECUTE_READWRITE, @OldProtect) ; result := ResStringRec^.Identifier; ResStringRec^.Identifier := Integer(NewStr) ; VirtualProtect(ResStringRec, SizeOf(ResStringRec^), OldProtect, @OldProtect) ; end; procedure UnHookResourceString(ResStringRec: pResStringRec; oldData: integer); var OldProtect: DWORD; begin VirtualProtect(ResStringRec, SizeOf(ResStringRec^), PAGE_EXECUTE_READWRITE, @OldProtect) ; ResStringRec^.Identifier := oldData ; VirtualProtect(ResStringRec, SizeOf(ResStringRec^), OldProtect, @OldProtect) ; end;
Feedback? Any better solution?
I’d rather popup some kind of toast notification, similar to the Outlook new mail notification.
What does your UnHookResourceString procedure look like?
… and what does your version of HookResourceString look like, since you made it into a function?
@Mike, I’ve included the code.
This code does not compile under Delhi 2007.
Is that correct?
Hi Jack, am not sure have not tried. What error do you have?
Undeclared identifier ‘SMsgDlgInformation’
You need to add “uses Vcl.Consts”.