Project JEDI Knowledgebase Article (original) (raw)
At some point in almost any application, you need to communicate with the user - to prompt for input or to provide specific instructions in a modal fashion. Our common tools for these tasks are ShowMessage or MessageDlg for quick, impromptu messages and MessageDlg to present mutually exclusive response options and return a value. For single line string input, you might use the InputQuery function. Several other methods are available to do the same tasks, but these three exemplify how standard dialogs work.
Dialogs of this nature are normally centered on the display screen so that they might grab the user's attention. Most of the time this works, although sometimes the effect would be improved if the dialog were centered on the application or even positioned over a particular control. There are no solid rules for positioning since there are simply too many situations where rules would be overruled by the specific context. What you can do is be prepared with knowledge of the alternatives to the standard methods like ShowMessage and InputQuery.
InputQuery and InputBox
Capturing information in ways that simulate InputQuery and InputBox could consume this entire article but that is not my plan. Instead we will cover the basics. In my opinion, InputBox and InputQuery should be avoided in business applications since they do not provide any events to assist with validation. For instance, if the user does not enter anything in the text box you must show an error message and redisplay the dialog.
If you are set on using InputBox or InputQuery you can enhance it, or build a better method to retrieve information. I suggest reviewing the source code for InputBox. I have included modified version of InputQuery in this month's demo to assist with this. The modified InputQuery permits it to be centered over the application's active form, or centered on the screen if there is no active form. Use TForm as the foundation to get the same functionality as Delphi's standard functions and place your validation directly into the form.
ShowMessage and MessageDlg
ShowMessage is useful for presenting quick feedback when it does not matter where the message is positioned on the screen and when the need to acknowledge before proceeding does not intrude unnecessarily on the work flow. MessageDlg builds on ShowMessage by allowing you to control the appearance of your message, to select which buttons are used and to provide context-defined help. With MessageDlg you can also ask questions and get back controlled responses from the user:
procedure TForm1.cmdExitClick(Sender: TObject); begin if MessageDlg('Ready to quit?',mtConfirmation,[mbNo,mbYes],0) = mrYes then Close; end;
The first parameter is the message presented to the user, the second is a message type constant to determine the dialog's caption and the icon which will appear. The third is a set of message button constants which tells Delphi which buttons you want to be shown. The final parameter is for the Help ID of the topic which should be invoked if the user presses F1. If you do not want to provide help, use zero here.
Handling the Help
How does the user know he can press F1 for help? Some users won't remember it is available, even if you tell them. A better approach is to employ a Help button:
procedure TForm1.cmdExitClick(Sender: TObject); begin if MessageDlg('Ready to quit?', mtConfirmation,[mbNo,mbYes,mbHelp],103) = mrYes then Close; end;
Now when the user needs help he simply presses the help button. OK, I jumped ahead a little there. There is more to be said about buttons!
Employing the Buttons
You need to enclose the buttons in square brackets, even if there is only one, because this parameter is of type set. If you look at help for MessageDlg you will see the following buttons are available:
Button Constant Caption mbYes Yes mbNo No mbOK OK mbCancel Cancel mbHelp Help mbAbort Abort mbRetry Retry mbIgnore Ignore mbAll All |
---|
You can freely to combine any buttons from this list.
Tip: The Dialogs unit has three constants representing sets of commonly used combinations of buttons:
const mbYesNoCancel = [mbYes, mbNo, mbCancel]; mbOKCancel = [mbOK, mbCancel]; mbAbortRetryIgnore = [mbAbort, mbRetry, mbIgnore];
When using these constants, don't enclose them in square brackets. If you do, Delphi will give you an error message, which says something that will not help you figure out the error.
Example using one of the predefined button sets
procedure TForm1.cmdExitClick(Sender: TObject); begin if MessageDlg('Ready to quit?',mtConfirmation, mbYesNoCancel,0) = mrYes then Close; end
If you are presenting more than two buttons, a simple if statement is not the cleanest method to condition the response handler. I suggest using a case construct instead. The following example displays a multi-choice message dialog and responds appropriately with another, simple modal one:
var msg: String; begin ... ... case MessageDlgCtr('Printing failed', mtInformation, mbAbortRetryIgnore, mrRetry,True) of mrRetry: msg := 'Retry was picked'; mrAbort: msg := 'Abort was picked'; mrIgnore: msg := 'Ignore was picked'; end; MessageDlgCtr(msg,mtInformation,[mbOk],mrOk,True); ... ... end;
Although the message dialog type has changed, you can see that this is a cleaner way to check which button was pressed.
Button Order
In case you didn't notice, the first example had the button order as [mbNo,mbYes], but the actual ordering of the buttons was Yes followed by No. This is because MessageDlg is created using CreateMessageDialog. If you look at the source code you will see:
for B := Low(TMsgDlgBtn) to High(TMsgDlgBtn) do
In the definition of TMsgDlgBtn:
TMsgDlgBtn = (mbYes, mbNo, mbOK, mbCancel, mbAbort, mbRetry, mbIgnore, mbAll, mbNoToAll, mbYesToAll, mbHelp);
you see that the button order is from left to right in TMsgDlgBtn, not the order you called them.
With a bit of hacking we can change the order of the buttons! Below is a very simple piece of demo code that will do just that:
procedure TForm1.Button1Click(Sender: TObject); var F: TForm; OkBtn: TControl; CancelBtn: TControl; L: Integer; begin F := CreateMessageDialog('button swap demo', mtInformation, [mbOk,mbCancel]); try OkBtn := F.FindComponent('Ok') as TControl; CancelBtn := F.FindComponent('Cancel') as TControl; { swap left position, you might want to swap taborder too } L := CancelBtn.Left; CancelBtn.Left := OkBtn.Left; OkBtn.Left := L; { as you see, you can change anything you want before showing the form, even insert new controls, hide/delete existing etc.} F.ShowModal; finally F.Free; end; end;
The secret is to locate the buttons (in this case "Cancel" and "Ok") using FindComponent. Once found, simply manipulate the left property of each button to suit.
NOTE Many programmers wish they could define their own captions for message boxes such as ShowMessage or MessageDlg. The logic to do this is not difficult, although the coding is not so simple. When I need a dialog with better control over button captions, I use Greg Lief's G.L.A.D. message box. For more information on G.L.A.D. send email to glad AT greglief DOT com) glad@greglief.com. The price of this library is (last time I checked) 69.00USD,whichbreaksdowntoaround69.00 USD, which breaks down to around 69.00USD,whichbreaksdowntoaround1.30 per component. Of course you could build your own dialog, but do not go running to the keyboard so fast! In this month's demo you will find a component which provides not only custom buttons, but some other nifty features. Complete source code is included.
Excerpt from Demo Project
I coded this portion of the demo so you need not install the component simply to try it out. All you need to do is make sure that the source code is in the library search path. Normally you would install the component and use it like any other dialog component, e.g. TOpenDialog.
procedure TForm1.cmdDemoingUserDlgComponentClick(Sender: TObject); const kgNope = 0; kgYep = 1; var f:TUserDlg; begin f := TUserDlg.Create(Self); try { Must clear the list, otherwise you will get an "OK" Button } f.Buttons.Clear; f.Buttons.Add('Nope'); f.Buttons.Add('Yep'); f.Message.Add('Do you use Delphi 4'); f.Message.Add('Please respond'); f.Title := 'Question' ; case f.Show of kgNope: MessageDlg('0 returned',mtInformation,[mbOk],0); kgYep: MessageDlg('1',mtInformation,[mbOk],0); end; finally f.Free; end; end;
Positioning
One nice feature would be a method to center a message box on a form. With the standard message box routines you get your message displayed in the center of the screen. To find out why, work through the code that creates MessageDlg, to a function called MessageDlgPosHelp. With this function CreateMessageDialog creates our dialog and sets the position property to poScreenCenter. Knowing this, it is not hard to figure out how to alter the position so that it centers on the calling form rather than the screen. I created a wrapper surrounding CreateMessageDialog, similarly to the way Delphi does MessageDlg.
function MessageDlgCtr(const aCaption: string; const Msg: string; DlgType: TMsgDlgType; Buttons: TMsgDlgButtons; HelpCtx: Longint; bCenterOnForm: boolean): Integer; begin if Screen.ActiveForm = nil then bCenterOnForm := false; with CreateMessageDialog(Msg, DlgType, Buttons) do try Caption := aCaption; HelpContext := HelpCtx; if bCenterOnForm then try Left := Screen.ActiveForm.Left + (Screen.ActiveForm.Width div 2) - (Width div 2); Top := Screen.ActiveForm.Top + (Screen.ActiveForm.Height div 2) -(Height div 2); except { place logic here if you desire to raise an exception on failure } end; Result := ShowModal; finally Free; end; end;
Note that the first line in the functions block checks to see if there is an active form. What you passed (True or False) gets passed along if there is an active form; otherwise, the value of bCenterOnForm parameter is forced to False. You might ask, how would there be a application without a form? Many programmers create complete applications without forms using just the project file and other units.
Now we have a dialog that can be centered on a calling form or positioned as any other standard dialog function. The next step is to add a method to allow changing the default button, that is, which button has initial focus. The logic for this goes through each component on the dialog form, checks to see if it matches the DefButton parameter and makes it the default button if it does.
function MessageDlgCtr(const Msg: string; DlgType: TMsgDlgType; Buttons: TMsgDlgButtons; DefButton: Integer; bCenterOnForm: boolean): Integer; var i: Integer; btn: TButton; begin if Screen.ActiveForm = nil then bCenterOnForm := false; with CreateMessageDialog(Msg, DlgType, Buttons) do try for i := 0 to ComponentCount -1 do if Components[i] Is TButton then begin btn := TButton(Components[i]); btn.default:= btn.ModalResult = DefButton; if btn.default then ActiveControl := Btn; end; if bCenterOnForm then begin Left := Screen.ActiveForm.Left + (Screen.ActiveForm.Width div 2) -(Width div 2); Top := Screen.ActiveForm.Top + (Screen.ActiveForm.Height div 2) - (Height div 2); end; Result := ShowModal; finally Free; end; end;
One more trivial addition, a method to separate breaking lines:
function StrCrLf(const cMsg:String):String ; var nLen, i:Integer; begin nLen := Length(cMsg); i := 1; while i <= nLen do begin if cMsg[i] in [';','~'] then Result := Result + #13 else Result := Result + cMsg[i]; Inc(i); end; end;
Example of Using the Function
This example will display two lines in the message and center the dialog on the form which called it.
MessageDlgCtr('This is some message~'More text', mtInformation, [mbOk],mrOk, True) ;
Now we have a base function which can be made simpler to use by wrapping it into functions designed for specific tasks.
Note for pre-Delphi 4 users
These wrappers use the default parameters feature introduced with Delphi 4. Each wrapper's last parameter sets a default value. If you are not using D4, then remove the text following the last formal parameter starting with the = sign, up to but not including the closing ")". The dialog.pas unit included with this month's demo has versions of each wrapper for both Delphi 4 and lower versions which do not support default parameters.
function Question(cQuestion: String; DefButton: Integer = mrYes): boolean; begin case DefMessageDlg('Question', cQuestion, mtInformation, [mbYes,mbNo], DefButton, 0) of IDYES : Result := True; IDNO : Result := False; else Result := False; end; end;
function RetryOperation(cMessage: string ; DefButton: Integer = mrReTry; DefCaption: String = 'Alert'): boolean; begin case DefMessageDlg(DefCaption, cMessage, mtInformation, [mbNo,mbRetry], DefButton, 0) of IDRETRY : Result := True; IDNO : Result := False; else Result := False; end; end;
So, to ask a question we need only supply the question and look for a response:
if Question('Format hard disk?') then // format the disk
Using Overloading and Default Parameters to Enhance It
Although the question function does a nice job of wrapping up some tedious coding, it could be enhanced a little. You might want to control the dialogs caption and perhaps have the choice of centering it on either the screen or the calling form. To add these enhancements I will be utilizing two features of Delphi 4, default parameters (already mentioned) and overloading.
Overloading functions is not used very much. The main purpose Delphi architects used it for was to allow different numeric types to be passed into routines. A good example is IntToStr, which accepts either a plain integer or Int64 (a signed 64bit integer).
function IntToStr(Value: Integer): string; overload;
function IntToStr(Value: Int64): string; overload;
The following is from my demo dialog unit's interface section. All declarations under VER120 section are also in the ELSE clause for earlier versions of Delphi.
MessageDlgCtr function uses overloading to gain control over the dialog's caption and to keep it as the first parameter, which I tend to like. The purist would most likely slap me silly for even thinking about what I did, but each to his own. In any event, the same was done to the function Question. I just love to have a default caption, but also have the ability to override it if needed.
{$IFDEF VER120} function MessageDlgCtr(const Msg : String; DlgType : TMsgDlgType; Buttons : TMsgDlgButtons; DefButton : Integer; bCenterOnForm: boolean): Integer; overload;
function MessageDlgCtr(constaCaption : String; Msg : String; DlgType : TMsgDlgType; Buttons : TMsgDlgButtons; DefButton : Integer; bCenterOnForm: boolean): Integer; overload;
function Question(cQuestion: String; DefButton: Integer = mrYes): boolean; overload;
function Question(aCaption : String; cQuestion : String; DefButton : Integer = mrYes; bCenterOnForm: boolean = True): boolean; overload;
{$ELSE} function MessageDlgCtr(const Msg: string; DlgType: TMsgDlgType; Buttons: TMsgDlgButtons; DefButton: Integer; bCenterOnForm: boolean): Integer;
function Question(const Msg): boolean; {$ENDIF}
When you type in MessageDlgCtr in Delphi 4, Code Completion shows you two sets of parameters since we are working with an overloaded routine. As you type, Delphi figures out which version of the function you intend to use and the other declaration goes away.
Conclusion
I've about covered the basics and I hope these additional tools will be useful as you develop your applications.
Download this month's demo project and component
Download this article (Word 7 format)
Kevin S. Gallagher is a full time systems analyst at Oregon Department Of Revenue where he uses Delphi, Visual Basic and Clipper to create Property Tax Systems.
---