Switching modal forms in Delphi - delphi

I have a modal form (A) that shows another modal form (B). B displays a dataset and allows the user to interact with it. My problem is that one action requires that A becomes again the focused form so the user can input certain values without closing B. I have tried A.BringToFront and A.SetFocus and it indeed is shown at front, but the input focus remains in B and any click or the like in A results in the windows "ding" when you click where you should not. The code is some how like
A.ShowModal;
.
.
. inside an event of A:
B.ShowModal();
.
.
. inside an event of B:
someobject.someMethodThatRequiresAFocused;
My guess is that some obscure and strange API call could make A modal again ¿Any ideas?
Regards

When a modal form is shown, all currently visible forms including other modal forms are disabled. As such, it is not possible to switch between multiple modal forms. You need to re-think your UI design so that B does not go back to A for new input. At the very least, you could have B open a new modal form C that prompts the user for just the needed values and gives them to B, and then either B or C can update A with the new values afterwards.

There's no API that toggles modality between windows. In any case the API you're looking for your case is EnableWindow. That's how modality works, windows other than the one that the user should be working with are disabled so that he/she cannot interact with them. This is also the reason for the 'ding' sound, to provide feedback to the user.
So while letting the user work with a window that's been disabled in favor of another modal window is technically easy, handling states may not be straight forward. I present a bare minimum example below for what it would seem to take.
'FormB' first. Lets suppose you pass a reference of 'FormA' in the 'Owner' parameter while 'FormA' is constructing 'FormB'. The below is what the code that should make 'FormA' modal again could look like:
procedure TFormB.BtnMakeFormAModalAgainClick(Sender: TObject);
begin
Enabled := False; // so that 'A' will behave like it's modal
EnableWindow(TFormA(Owner).Handle, True); // so that 'A' could be interacted
TFormA(Owner).SetFocus;
end;
When this code runs, what happens is 'FormA' is enabled and brought to front, and 'FormB' is disabled - will produce a 'ding' when clicked on.
But we're not done yet. Because we have modified the meaning of modality - now we don't want 'FormA' to be closed when the user is done with it. Below is how could the code in 'FormA's unit could look like:
type
TFormA = class(TForm)
BtnShowModalB: TButton;
BtnOk: TButton;
procedure BtnShowModalBClick(Sender: TObject);
procedure BtnOkClick(Sender: TObject);
private
FModalB: TForm;
end;
implementation
uses
unitOfFormB;
{$R *.dfm}
procedure TFormA.BtnShowModalBClick(Sender: TObject);
begin
FModalB := TFormB.Create(Self); // so that FormB can find FormA from the Owner
FModalB.ShowModal;
FModalB.Free;
FModalB := nil; // Need this if we're going to decide if FormB is showing
// by testing against this reference
end;
procedure TFormA.BtnOkClick(Sender: TObject);
begin
if Assigned(FModalB) then begin // is FormB the actual modal form?
EnableWindow(Handle, False); // disable this form so it would 'ding'
FModalB.Enabled := True; // enable FormB, so user can interact with it
FModalB.SetFocus;
ModalResult := mrNone; // don't close, FormB is the first one to be closed
end else
ModalResult := mrOk;
end;
I'm nearly positive that this example is not complete, but here's the API that you're looking for.

Related

Expanding the functionality of existing example

This is just for learning sake : Using accepted answer in this example Delphi application with login / logout - how to implement?,how can you show a specific form (if you have many forms) ?
I have placed a query on that LoginForm (which displays various forms that can be selected and shown).
How should I change the existing code so I can accomplish this maintaining the same functionality meaning that whatever form I select and click 'open' it will become the mainform and be shown?
The main form is the first form created using Application.CreateForm. So use an if or case statement to conditionally switch which form is created that way.
In pseudo-code:
if Form1 is main form then
Application.CreateForm(TForm1, Form1)
else if Form2 is main form then
Application.CreateForm(TForm2, Form2)
....
There are quite likely slicker ways to do this, but this is the basic principle.
So, you might well make a function to return the class of the main form, using whatever mechanism you have to choose it:
function GetMainFormClass: TFormClass;
begin
Result :=...; // your logic here
end;
Then in the .dpr file you write
Application.CreateForm(GetMainFormClass, MainForm);
You seem, to be really struggling with this. I'll try to spell it out in a little more detail. The implementation of GetMainFormClass might look like this:
function GetMainFormClass: TFormClass;
var
FormClassName: string;
begin
FormClassName := ...; // somehow get the name of the form class, e.g. 'TForm1'
if FormClassName = 'TForm1' then
Result := TForm1
else if FormClassName = 'TForm2' then
Result := TForm2
else
raise EUnrecognisedFormClassName.Create(...);
end;
Personally, I don't understand why you are presenting a grid control to the user. To me that seems like a very weak user interface with which to make a selection. I would present a radio group. Then you could write code like this:
type
TMainFormType = (mftForm1, mftForm2, mftForm3);
const
MainFormClass = array [TMainFormType] of TFormClass = (
TForm1,
TForm2,
TForm3
);
Then you can have a function that returns TMainFormType, by way of showing the user a form and presenting a radio group with three buttons. You can convert the radio group's ItemIndex into the appropriate TMainFormType value.
function GetMainFormType: TMainFormType;
begin
// ... your UI shows now, presenting the radio group
Result := TMainFormType(RadioGroup.ItemIndex);
end;
And then in the .dpr file you write
Application.CreateForm(MainFormClass[GetMainFormType], MainForm);
There are lots of different ways to do this, but you do need to understand the basics of branching code. Either if statements, or lookup tables. If you don't understand how these work then, with all due respect, you need to brush up those skills before you proceed.

DELPHI Edit.OnExit by TAB, show window result on focus bug

I'm having trouble with the following scenario:
2 Edit's
Type something in Edit1 and press TAB, focus goes to Edit2
Edit1.OnExit -> show a Form with a message "Processing..." (makes a lengthy validation)
After the form closes, the focus on Edit2 seems to be "crashed"...
- the hole TEXT in Edit2 isn't selected
- the carret isn't flashing
Example:
Create a new form
Put 2 edits
Set this as OnExit event in Edit1:
procedure TForm1.Edit1Exit(Sender: TObject);
begin
with TForm.CreateNew(self) do
try
Width := 100;
Height := 50;
Position := poMainFormCenter;
show;
sleep(200);
finally
Free;
end;
end;
Run the application
Set focus in the Edit1 and press TAB
I'm using:
Delphi 7 Enterprise
Windows 7 x64
This is a known problem. Windows has problems when you change focus before it's completed the last focus change (eg., focus starts changing from Edit1 to Edit2, but Edit1.OnExit does something to change focus to another control or form.
This happens, for instance, when apps try to do validations in an OnExit event and then try to return focus to the original control when the validation fails.
The easiest solution is to post a message to your form handle in the OnExit instead, and handle the focus change need there. It will fire once the target control gets the input focus, and Windows doesn't get confused.
const
UM_EDIT1_EXITED = WM_USER + 1;
type
TForm1=class(TForm)
...
private
procedure UMEdit1Exited(var Msg: TMessage); message UM_EDIT1_EXITED;
end;
implementation
procedure TForm1.Edit1Exit(Sender: TObject);
begin
PostMessage(Handle, UM_EDIT1_EXITED, 0, 0);
end;
procedure TForm1.UMEdit1Exited(var Msg: TMessage);
begin
// Show your other form here
end;
From an old Borland NG post by Dr. Peter Below of TeamB:
here is my general sermon on the "show dialog from OnExit" problem:
If an OnExit handler is triggered (which happens in response to the
Windows
message WM_KILLFOCUS) Windows is in the midst of a focus change. If you do
something in the handler that causes another focus change (like popping up
a message box or doing a SetFocus call) Windows gets terribly confused.
The
missing cursor is a symptom of that.
If you have to display a message to your user from an OnExit handler, do
it
this way:
Define a constant for a user message somewhere in the INterface
section
of your unit, above the type declaration for your form
'Const
UM_VALIDATE = WM_USER + 200;'
Give your Form a handler for this message, best placed in the private
section of the class declaration:
Procedure UMValidate( Var Msg: TMessage ); message UM_VALIDATE;
Post a UM_VALIDATE message to the form from the OnExit handler if
the contents of the field are not ok. You can pass additional
information in the wparam and lparam parameters of the message, e.g.
an error number and the Sender object. In fact you could do the whole
validation in the UMValidate handler!
I'm not sure precisely what's going on here, but it looks like the order of processing of messages is a bit messed up. Instead of killing your other form with Free, use Release and the focus will behave as you desire.
Another option is to use ShowModal instead of Show. Normally you show a processing dialog modally because you don't want the user making modifications to the main form whilst you are processing. If you do that then you can carry on using Free.

Delphi GUI Testing and Modal Forms

In this interesting blog post on delphiXtreme I read about DUnit's built-in GUI testing capabilities (basically an alternative test case class TGUITestCase defined in unit GUITesting that has several utility functions for invoking actions in the GUI). I was quite happy with it until I noticed that it didn't work with modal forms. For example the following sequence won't work if the first button shows a modal configuration form:
Click ('OpenConfigButton');
Click ('OkButton');
The second Click is only executed when the modal form is closed, which I have to do manually.
I don't know much about how modal forms work in the background but there must be some way to circumvent this behaviour. Naively, I want to somehow execute the ShowModal "in a thread" so that the "main thread" stay responsive. Now I know that running ShowModal in a thread will probably mess up everything. Are there any alternatives? any way to circumvent the blocking nature of a ShowModal? Has anybody some experiences with GUI testing in Delphi?
I know about external tools (from QA or others) and we use those tools, but this question is about GUI testing within the IDE.
Thanks!
You can't test modal forms by calling ShowModal; because as you have quite rightly discovered, that results in your test case code 'pausing' while the modal form awaits user interaction.
The reason for this is that ShowModal switches you into a "secondary message loop" that does not exit until the form closes.
However, modal forms can still be tested.
Show the usually Modal form using the normal Show method.
This allows your test case code to continue, and simulate user actions.
These actions and effects can be tested as normal.
You will want an additional test quite particular to Modal forms:
A modal form is usually closed by setting the modal result.
The fact that you used Show means the form won't be closed by setting the modal result.
Which is fine, because if you now simulate clicking the "Ok" button...
You can simply check that the ModalResult is correct.
WARNING
You can use this technique to test a specific modal form by explicitly showing it non-modally. However, any code under test that shows a modal form (e.g. Error Dialog) will pause your test case.
Even your sample code: Click ('OpenConfigButton'); results in ShowModal being called, and cannot be tested in that manner.
To resolve this, you need your "show commands" to be injectible into your application. If you're unfamliar with dependency injection, I recommend Misko Hevery's Clean Code Talks videos available on You Tube. Then while testing, you inject a suitable version of your "show commands" that won't show a modal form.
For example, your modal form may show an error dialog if validation fails when the Ok button is clicked.
So:
1) Define an interface (or abstract base class) to display an error messages.
IErrorMessage = interface
procedure ShowError(AMsg: String);
end;
2) The form you're testing can hold an injected reference to the interface (FErrorMessage: IErrorMessage), and use it to show an error whenever validation fails.
procedure TForm1.OnOkClick;
begin
if (Edit1.Text = '') then
FErrorMessage.ShowError('Please fill in your name');
else
ModalResult := mrOk; //which would close the form if shown modally
end;
3) The default version of IErrorMessage used / injected for production code will simply display the message as usual.
4) Test code will inject a mock version of IErrorMessage to prevent your tests from being paused.
5) Your tests can now execute cases that would ordinarily display an error message.
procedure TTestClass.TestValidationOfBlankEdit;
begin
Form1.Show; //non-modally
//Do not set a value for Edit1.Text;
Click('OkButton');
CheckEquals(0, Form1.ModalResult); //Note the form should NOT close if validation fails
end;
6) You can take the mock IErrorMessage a step further to actually verify the message text.
TMockErrorMessage = class(TInterfaceObject, IErrorMessage)
private
FLastErrorMsg: String;
protected
procedure ShowError(AMsg: String); //Implementaion trivial
public
property LastErrorMsg: String read FLastErrorMsg;
end;
TTestClass = class(TGUITesting)
private
//NOTE!
//On the test class you keep a reference to the object type - NOT the interface type
//This is so you can access the LastErrorMsg property
FMockErrorMessage: TMockErrorMessage;
...
end;
procedure TTestClass.SetUp;
begin
FMockErrorMessage := TMockErrorMessage.Create;
//You need to ensure that reference counting doesn't result in the
//object being destroyed before you're done using it from the
//object reference you're holding.
//There are a few techniques: My preference is to explicitly _AddRef
//immediately after construction, and _Release when I would
//otherwise have destroyed the object.
end;
7) Now the earlier test becomes:
procedure TTestClass.TestValidationOfBlankEdit;
begin
Form1.Show; //non-modally
//Do not set a value for Edit1.Text;
Click('OkButton');
CheckEquals(0, Form1.ModalResult); //Note the form should NOT close if validation fails
CheckEqulsString('Please fill in your name', FMockErrorMessage.LastErrorMsg);
end;
There is actually a way to test modal windows in Delphi. When a modal window is shown your application still processes windows messages so you could post a message to some helper window just before showing the modal window. Then your message would be handled from the modal loop allowing you to execute code while the modal window is still visible.
Recently I've been working on a simple library to handle this very problem. You can download the code from here: https://github.com/tomazy/DelphiUtils (see: FutureWindows.pas).
Sample usage:
uses
Forms,
FutureWindows;
procedure TFutureWindowsTestCase.TestSample;
begin
TFutureWindows.Expect(TForm.ClassName)
.ExecProc(
procedure (const AWindow: IWindow)
var
myForm: TForm;
begin
myForm := AWindow.AsControl as TForm;
CheckEquals('', myForm.Caption);
myForm.Caption := 'test caption';
myForm.Close();
end
);
with TForm.Create(Application) do
try
Caption := '';
ShowModal();
CheckEquals('test caption', Caption);
finally
Free;
end;
end;

Form is hidden behind other forms when ShowModal is called

My application is based on modal forms. Main form opens one form with ShowModal, this form opens another with ShowModal, so we have stacked modal forms. There is sometimes a problem that when we call ShowModal in new form, it hides behind previous forms, instead of showing on top. After pressing alt+tab, form comes back to the top, but this is not good solution. Did You meet this problem and how did you handle it?
EDIT:
I use Delphi 7.
You didn't mention which version of Delphi...
Newer Delphi versions have added two new properties to TCustomForm: PopupMode and PopupParent. Setting PopupParent of your modal dialog to the form that's creating that dialog makes sure that the child form stays on top of it's parent. It usually fixes the problem you're describing.
I think this pair of properties were added in Delphi 2006, but it may have been 2005. They're definitely there in Delphi 2007 and up.
EDIT: After seeing you're using Delphi 7, the only suggestion I have is that, in the code that displays your modal form, you disable the form creating it, and re-enable on return. That should prevent the creating window from receiving input, which may help keep the Z-order correct.
Something like this may work (untested, as I'm no longer using D7):
procedure TForm1.ShowForm2;
begin
Self.Enabled := False;
try
with TForm2.Create(nil) do
begin
try
if ShowModal = mrOk then
// Returned OK. Do something;
finally
Free;
end;
end;
finally
Self.Enabled := True;
end;
end;
If Form2 creates a modal window (as you've mentioned), just repeat the process - disable Form2, create Form3 and show it modally, and re-enable Form2 when it returns. Make sure to use try..finally as I've shown, so that if something goes wrong in the modal form the creating form is always re-enabled.
Sorry for adding a separate answer, but I have done a bit more research, and some of it indicates that my previous answer (DisableProcessWindowsGhosting) doesn't help. Since I can't always reproduce this issue, I cannot say for sure.
I found a solution that appears to appropriate. I referenced the code in Delphi 2007 for the CreateParams method and it matches pretty close (without having all of the other code that handles PopupMode).
I created the unit below which subclasses TForm.
unit uModalForms;
interface
uses Forms, Controls, Windows;
type
TModalForm = class(TForm)
protected
procedure CreateParams(var params: TCreateParams); override;
end;
implementation
procedure TModalForm.CreateParams(var params: TCreateParams);
begin
inherited;
params.WndParent := Screen.ActiveForm.Handle;
if (params.WndParent <> 0) and (IsIconic(params.WndParent)
or not IsWindowVisible(params.WndParent)
or not IsWindowEnabled(params.WndParent)) then
params.WndParent := 0;
if params.WndParent = 0 then
params.WndParent := Application.Handle;
end;
What I do then is include this unit in with a form unit, and then change the form's class (in the .pas code file) from class(TForm) to class(TModalForm)
It works for me, appears to be close to CodeGear's solution.
From this link it appears that the problem is with the "Ghosting window" that was introduced in 2000/XP. You can disable the ghosting feature by calling the following code at startup.
procedure DisableProcessWindowsGhosting;
var
DisableProcessWindowsGhostingProc: procedure;
begin
DisableProcessWindowsGhostingProc := GetProcAddress(
GetModuleHandle('user32.dll'),
'DisableProcessWindowsGhosting');
if Assigned(DisableProcessWindowsGhostingProc) then
DisableProcessWindowsGhostingProc;
end;
The only issue that I can see is that it will cause problems with the feature that allows for the user to minimize, move, or close the main window of an application that is not responding. But in this way you do not have to cover each call with the Self.Enabled := False code.
Just set the Visible property of the form, that you want to open modal, to False. Then you can open it with .ShowModal(); and it will work.
I have found that using the "Always On Top" flag on more than one form causes problems with the Z order. And you may also find the need for the BringWindowToTop function.
When launching a message box using the built-in WinAPI (MessageBox), I have found that passing the calling window's handle is necessary in order to make sure that the the prompt appears on top all the time.
try it
OnShowForm:
PostMessage(Self.Handle, WM_USER_SET_FOCUS_AT_START, 0, 0);

Fake modal dialog using Show?

My application have several modules, each in one tab on the mainform.
When using a dialog it is convenient to call ShowModal because you know when the dialog is finished. But for the user it is not good as it lock the whole program until the dialog closes.
I want to have a locally modal dialog. So one module can open a dialog and it locks current module only. The user can still switch to another module and continue to work. If the user return to the first module the dialog is there waiting for close before the user can continue to work in that module.
I have to make some kind of framework for this that all dialogs in the application can use.
I have a baseclass for all dialogs TAttracsForm and I think here is the place to add my Show() method.
This should lock access to all wincontrols only in the current module.
It should simulate a call to ShowModal(). How can I achieve this ?
Regards
You will have to do the following:
Have an identity for each module
Have a flag that is active or inactive for each module
Have a flag that stores the modality of the attached dialog. If it is modal and the module is active, then call the show method in the appropriate eventhandler. Remember to update these values in the onshow and onclose events of each dialog.
You may have to fine tune this suggestion till you achieve the exact functionality that you require.
Do you still want to implement this with "you know when the dialog is finished" metaphor?
So like
DoSomethingBeforeDialog();
Form:=TFakeFormDialog.Create(Nil);
try
Form.FakeShowModal();
finally
Form.Free;
end;
DoSomethingAfterDialog();
if the answer is yes, you would try to implement this with threads, like Google Chrome do this with tab pages. But without threads only you can catch message processing with a code like this
function TFakeModalDlg.FakeShowModal(FormParent: TWinControl): boolean;
begin
Parent:=FormParent;
SetBounds((FormParent.Width - Width) div 2, (FormParent.Height - Height) div 2,
Width, Height);
Show;
while NoButtonIsPressed() do
begin
Application.HandleMessage;
end;
Hide;
end;
And you even have code below...
Form:=TFakeModalDlg.Create(Nil);
try
(Sender as TButton).Caption:='Going modal...';
Form.FakeShowModal(TabSheet1);
(Sender as TButton).Caption:='Returned from modal';
finally
Form.Free;
end;
called multiply times from your tabs, but the problem is the these "dialogs" should be closed in "stack order" i.e. reverse to the order they were showed. I think it's impossible to force users to close forms in developers preference order :)
I have actually almost implemented local modal dialogs now.
It is built around that when a TForms Enabled property is set To False the whole Form is locked from input. And my modules is just a descendant from TForm.
My ViewManager class that decide what modules is current add/close modules etc got 2 new methods. LockCurrentView and UnLOckCurrentView.
function TViewManager.LockCurrentView: TChildTemplate;
begin
Result := CurrentView;
Result.Enabled := False;
Result.VMDeactivate; // DeActivate menus and toolbas for this module
end;
procedure TViewManager.UnLockCurrentView(aCallerForm: TChildTemplate);
begin
aCallerForm.VMActivate; // Activate menus and toolbas for this module
aCallerForm.Enabled := True;
end;
TAttracsForm is the baseclass of all dialogs. I Override FormClose and add a new method ShowLocalModal to call instead of ShowModal. I also have to add a TNotifyEvent OnAfterDestruction to be called when the dialog is closed.
procedure TAttracsForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
if Assigned(fCallerForm) then
begin
ClientMainForm.ViewManager.UnLockCurrentView(fCallerForm as TChildTemplate);
if Assigned(OnAfterDestruction) then
OnAfterDestruction(Self);
Action := caFree;
end;
end;
{ Call to make a dialog modal per module.
Limitation is that the creator of the module must be a TChildtemplate.
Several modal dialogs cannot be stacked with this method.}
procedure TAttracsForm.ShowLocalModal(aNotifyAfterClose: TNotifyEvent);
begin
fCallerForm := ClientMainForm.ViewManager.LockCurrentView; // Lock current module and return it
PopupParent := fCallerForm;
OnAfterDestruction := aNotifyAfterClose;
Show;
end;
Some test with simple dialogs looks promising. So the module just have to call ShowLocalModal(myMethod) which have a TNotifyEvent as parameter. This method is called when the dialog is closed.

Resources