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.
Related
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;
I have the following issue: we're building a rather large application (win32, Delphi 6 Enterprise). In several part of the application, modal windows are used, usually containing the detail of the selection of the main window.
We included a modification of the handling of the WM_SYSCOMMAND messages so that, if the window is modal, then a SW_SHOWMINNOACTIVE message will be sent to the application's main window. This causes the whole application to be minimized instead of just the modal form.
There is, however, an issue happening in a specific case: if the calling window is set to full screen, then upon restoration, the modal window will appear UNDER the (disabled) maximized main window (this seems to happen on Windows 7)
My problem is two fold:
First, I don't seem to get any syscommand message when the application is restored any more so I cannot introduce code to restore the Z-Order because I don't know where to put it.
Second, it seems to me that, if the whole application is minimized, clicking on the app's button in the task bar should restore it in the same state, not with a modal window under it. Is there a way to fix that ?
Edit: we did some additional testing and it seems we can actually detect the problem in the WM_ACTIVATE handler for the main form. We can also identify the modal window at that stage. I cannot, however, find a way to restore it to the top of the Z-Order.
Edit2: here is the code that minimizes the application when the modal form is minimized:
procedure TfmGITForm.WMSysCommand(var Message: TWMSysCommand);
begin
if (fsModal in FormState) or
not Application.MainForm.Visible then
begin
case Message.CmdType of
SC_MINIMIZE:
begin
ShowWindow(Application.Handle, SW_SHOWMINNOACTIVE);
end;
SC_RESTORE:
begin
ShowWindow(Application.Handle, SW_SHOWNORMAL);
inherited;
end;
else
inherited;
end; // case
end
else
inherited;
end;
All our forms descend from that one.
Override the dialog's CreateParams function and set Params.WndParent to the full-screen window (or Owner.Handle if you're owning things properly). The default is Application.Handle, which will cause these kinds of problems. The PopupParent properties introduced in the later Delphi releases does the exact same thing.
This has to do with the Window ghosting by Windows which was introduced in (I think) XP. I have the same issues in a D5 app on these operating systems. Peter Below offered the following work around at the time and it still serves me well:
procedure DisableProcessWindowsGhosting;
type
TDisableProcessWindowsGhostingProc = procedure; stdcall;
const
sUser32 = 'User32.dll';
var
ModH: HMODULE;
_DisableProcessWindowsGhosting: TDisableProcessWindowsGhostingProc;
begin
ModH := GetModuleHandle(sUser32);
if ModH <> 0 then begin
#_DisableProcessWindowsGhosting := nil;
#_DisableProcessWindowsGhosting := GetProcAddress(ModH,
'DisableProcessWindowsGhosting');
if Assigned(_DisableProcessWindowsGhosting) then begin
_DisableProcessWindowsGhosting;
end;
end;
end;
I call it at the beginning of the app's main form's OnCreate handler.
I need develop an application (Delphi) that never receive the focus, I use the DLL to display the video on second monitor (I found in Torrys Delphi - Dr.SAGURA Media Player v.1.0) that receive the focus every time play the video, how avoid this ?
I try :
procedure TForm.WMActivate(var Msg: TWMActivate);
begin
WA_ACTIVE :
begin
Msg.Result := 0;// cancel focus
end;
end;
Unsuccessful !
Thanks
Jean Alysson
In your project DPR set Application.MainFormOnTaskBar := False.
Override the form's OnShow and OnActivate events and add this to both of them: ShowWindow(Application.Handle, SW_HIDE);
Override your form's CreateParams procedure and add WS_EX_NOACTIVATE to Params.ExStyle.
The first two items hide the application from the taskbar and the Alt+Tab list and the last keeps it from gaining focus when it's shown and when clicking on it.
What's the intention behind not be focusable? You want to avoid the user from closing/minimize/maximize the window?
Maybe your need can be acomplished understanding the real problem.
Maybe having a sevice instead of a regular application can make the trick.
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);
In my Delphi form's OnShow method, I determine that a dialog must be opened automatically once the form is opened - and I should be able to do this by simulating a click on a menuitem.
However, calling menuitem.Click brings up the dialog before the main form has opened - which is not what I want.
I expect that should do what I want, but I cannot find what parameters to pass for "wparam" to send the click to my menuitem.
PostMessage(handle, WM_COMMAND, wparam, 0)
The MSDN WM_COMMAND docs talk about IDM_* identifiers, but how does that appear in Delphi?
(I know this is a very old question but despite being resolved in some way the real question has really gone unanswered.)
--
The command item identifier of a 'TMenuItem' is in the Command property. According to WM_COMMAND's documentation the high word of 'wParam' would be '0' and the low word would be the menu identifier;
PostMessage(Handle, WM_COMMAND, MakeWParam(MyMenuItem.Command, 0), 0);
or simply;
PostMessage(Handle, WM_COMMAND, MyMenuItem.Command, 0);
With a popup menu item there would be a slight difference: the VCL handles popup menus' messages with a different utility window. The global PopupList variable has the handle to it in its Window property;
PostMessage(PopupList.Window, WM_COMMAND, MyPopupMenuItem.Command, 0);
Perhaps you can try to open the dialog in the OnActivate event ?
I am not really sure if the OnActivate gets fired again other than when the form is shown but if it does you can use :
procedure TForm1.FormActivate(Sender: TObject);
begin
Form2.ShowModal;
Self.OnActivate := nil;
end;
Wouldn't you have to do this with a one-time timer, if you want the form to appear as per a normal Show/ShowModal, get drawn (etc) fully, and then immediately do something else?
tmrKickOff : a TTimer, 100 ms interval, disabled at design time,
fires off a 'tmrKickOffTimer' event.
in form create,
tmrKickOff.Enabled:=false; //just in case something happened in IDE
in form show, at end of all other stuff;
tmrKickOff.Enabled:=true;
in tmrKickOffTimer
begin
tmrKickOffTimer.Enabled:=false;
menuItemClick(nil);
end;
with apologies for style, form and any error-trapping :-)
Alternatively, handle the Application.OnIdle event with something along the lines of ...
if not DialogDone then
begin
MyDialogForm.ShowModal; // or menuItem.Click ....
DialogDone := true;
end;
OnIdle won't fire (for the first time) until the Form is shown and the message queue is empty.
I don't think you can send a message directly to your menu item, but you can just post it to the main window and show your dialog from there. I do this and it works great so that the dialog box (in my case, a login prompt) appears on top of the main window to avoid confusion.
-Mark
procedure WMPostStartup(var Message: TMessage); message WM_POSTSTARTUP;
procedure TMainForm.WMPostStartup(var Message: TMessage);
begin
Self.Refresh;
// Now show the dialog box.
end;
One method I have used, which is very simular to MarkF's solution, is to create a new user defined message type and send a message using that type to yourself when you determine that you need to perform this other process after your main form displays:
const
wm_SpecialProc = wm_User + 1;
procedure TForm1.WMSpecialProc(var Message:tMessage); message wm_SpecialProc;
begin
Form2.ShowModal;
end;
procedure TForm1.OnShow(Sender:tObject);
begin
if true then
PostMessage(Application.MainForm.Handle,wm_SpecialProc,0,0);
end;
The nice thing about this method is that you are in control of the message generation, so can populate ANY lparam or wparam you want to later use by your handler. I sent the message directly through the application.mainform but you could also just say handle for the current form.