I have a program with a large GUI. The GUI is split in several tabs.
The code for managing the GUI (for example one tab contains a full blown Windows Explorer clone) is pretty large.
Which will be the best approach for splitting such a large GUI in multiple files but without having the GUI split in multiple forms (at run time)?
Use embedded forms. This way you can maintain each one in a separate file, and at run-time they all appear to be part of the same GUI form.
Create a global variable to store the current form:
MyForm: TForm;
You don't want to auto-create the forms, so remove them from the project options -> Forms auto-create list. Instead, create them dynamically this way:
MyForm := TMyForm.Create(Application);
Then set the properties as needed, including the following:
I'm assuming you have a panel named something like EmbeddedMyForm_panel on the tabsheet where you want to embed the form. That's what I do, anyway. You could also probably use the TTabSheet directly.
with MyForm do begin
BorderIcons := [];
BorderStyle := bsNone;
parent := EmbeddedMyForm_panel;
Align := alClient;
Visible := true;
end;
I've worked on numerous projects that used this approach very successfully to embed separate forms on tons of tabs inside of one massive GUI.
ADDED: When I've asked why they didn't use frames instead, I was told that with a dozen or more embedded forms on the main form, loading it up with frames would take forever because the IDE would complain about not being able to find the ancestor form for virtually every frame on the form. You need to open all of the frame forms first in order to open the main form without getting any warnings from the IDE. Which is particularly annoying if you simply want to work on the main form itself (eg. edit the main menus) and don't need to deal with any of the frames at all.
Related
My Delphi XE7 FireMonkey project is growing controls and naturally I've moved to using frames. Where I've used frames in the VCL there have been situations where I've simply chosen to host one (complex) VCL form inside another instead, creating and displaying it in the form's OnShow and setting it client-aligned (the benefit of this is that you don't get issues with dangling inherited controls when you edit the frame).
With FireMonkey though, things have changed slightly and my attempt to get a child form client aligned inside another is stumbling. I came across this very useful SO link which shows how to host a FireMonkey form inside a VCL form so I built on this with my code as follows:
procedure THostForm.FormCreate(Sender: TObject);
begin
FForm := TChildForm.Create( Self );
FForm.BorderIcons := [];
FForm.BorderStyle := TFmxFormBorderStyle.None;
FForm.Visible := True;
FForm.Parent := Self;
ResizeForm;
end;
procedure THostForm.FormResize(Sender: TObject);
begin
inherited;
ResizeForm;
end;
procedure THostForm.ResizeForm;
begin
if Assigned(FForm) then
FForm.SetBounds( Round(ClientRect.Left), Round(ClientRect.Top), Round(ClientWidth), Round(ClientHeight));
end;
This produces a child form which changes size with the host form, but remains at the top left of the screen. I've tried various position options in the ResizeForm routine too. It seems to me that a form might not be able to be the parent of another because TForm is not IAligneableControl whereas TFrame is. So, I tried 'docking' my child form to a TRectangle client aligned in the host form and this behaves the same way.
Has anyone examined this?
* SOLUTION DETAIL AS SUGGESTED BY MARCO BELOW *
Marco's solution is very neat and reduces the 'hosting' to just two lines of code. You do need to ensure that your child (hosted) form has everything inside another client aligned control - Marco suggested using a TLayout, but I already had a TPanel that I am using for a background so I had no modifications to the child form at all. So, to host this child form TChildForm inside a THostForm simply do:
procedure THostForm.FormCreate(Sender: TObject);
begin
FForm := TChildForm.Create( Self );
FForm.Panel1.Parent := Self;
end;
Job done. Thanks Marco.
Mixing forms and controls in FireMonkey is not such a good idea as it is the VCL, because in the VCL controls and form are all TWinControl descendant with their own Windows handle, while in FireMoneky the form is associuated with an operating system object while the controls are not.
The address scenario, I've used a different solution. Created a form with a client-aligned, useless TLayout with all of the controls inside it. At runtime, create this form and parent the Layout to the new container (for example a tab in a multi tab control).
I've used this a few times, never found big issues with it, and a nice way to dynamically crate tab pages keeping the visual development model.
In DPR file:
Application.CreateForm(TMain, Main);
Application.CreateForm(TCommStatus, CommStatus);
But I get an error if I want to use CommStatus in Main, because it was not instanced yet. Then inside TMain I tried:
procedure TMainWindow.FormShow(Sender: TObject);
begin
Application.CreateForm(TCommStatus, CommStatus);
CommStatus.Expand(Self);
end;
I was trying to have my LOG window to be positioned and sized according to my MainWindow position and width. But as my LOG window is created after Main Window, I can't really call it in OnCreate(), even because there is no correct positioning data in OnCreate().
Don't use Application.CreateForm to create the CommStatus form at all. Create it yourself in your MainWindow.OnCreate:
proccedure TMainForm.FormCreate(Sender: TObject);
begin
CommStatus := TCommStatus.Create(Self);
CommStatus.Expand(Self);
end;
Don't forget to remove CommStatus from the auto-create forms list (in Project->Options->Forms).
I need to know if CommStatus is Assigned before execute CreateForm.
You answered your own question - use Assigned(), eg:
uses
..., CommStatusFormUnit;
if not Assigned(CommStatus) then
Application.CreateForm(TCommStatus, CommStatus);
Or:
uses
..., CommStatusFormUnit;
if not Assigned(CommStatus) then
CommStatus := TCommStatus.Create(Application);
Global variables, like the CommStatus variable in the CommStatusFormUnit unit, are zero-initialized at program startup, thus Assinged() will return False until the form is actually created, as long as you assign the new Form instance to the global variable (as the examples above do).
But CommStatus identifier does not exists until it. So I can't use Assigned(CommStatus).
Yes, it does exist, and yes, you can use Assigned(CommStatus). If you are having errors with it, then you are not using it correctly.
Expand(Self) should use Main position information to put CommStatus beside Main in the same left position, but it doesn't.
Then you are not handling the positioning logic correctly.
Another option is to create CommStatus first in your DPR. But without using Application.CreateFom (which would then incorrectly make CommStatus you main form).
This way you'll know it has definitely been created, and not have to worry.
I.e.
CommStatus := TCommStatus.Create(Application);
Application.CreateForm(TMain, Main);
However, that fact that you're trying to Expand CommStatus in the OnShow of your main form seems a little dubious to me. You've created a hidden dependency between the two forms, which is exactly one of the reasons globals are frowned upon.
One option to clean that up would be as follows:
CommStatus := TCommStatus.Create(Application);
Application.CreateForm(TMain, Main);
CommStatus.Expand(Main);
This way the relationship between the two is explicit and less error-prone.
I stumbled on this page Why shouldn’t I call Application.CreateForm.
Now I have some code like this:
SplashForm := TSplashForm.Create(Application);
SplashForm.Show;
SplashForm.Update; // force update
Application.Initialize;
Application.CreateForm(TClientData, ClientData);
SplashForm.Update; // force update
Application.CreateForm(TClientMainForm, ClientMainForm);
Application.ShowHint := True;
Application.Run;
ClientMainForm.ServerConnected := false;
FreeAndNil(ClientMainForm);
FreeAndNil(ClientData);
First a splashform is created, then a datamodule and last the main form. The page says that Application.CreateForm should not be called twice. Should the code above be changed?
There is nothing wrong with using Application.CreateForm multiple times. But this introduces global variables for each form which can be a code smell.
Unfortunately the IDE creates one for each form. Although you can remove them if you like.
A better way is to create a form when you need it and release it when you are ready with it. So you only use Application.CreateForm for the main form.
A main datamodule can be created by the main form. But it can be global too, just a matter of taste.
So to answer the question, you can avoid Application.CreateForm by creating and releasing the forms locally.
The article mentions the side effect of Application.CreateForm (the first completed form is the main form).
So there can be unexpected side effects if the main form creates other forms using Application.CreateForm.
So just to avoid any nastyness, you should limit yoursef to a single call. Which is done using only one global form.
If TClientData is a Data Module and TClientMainForm is a form, then no (except perhaps the two FreeAndNil calls at the end - not really needed). But take care. Because as it says Rob Kennedy in his post, the Application.CreateForm does other things behind (it sets the MainForm variable), so I would advise to set up your project file according to the following rules:
Create all the forms which you want to create at startup using a single call to Application.CreateForm - usually this is done by the IDE.
Remove from the project file the forms which you want to create dynamically (on-demand) in your program. (In Project | Options | Forms...) - move them from 'Auto-Create Forms' to 'Available Forms'
Create your forms in your code using TmyForm.Create(Owner) (etc.) and not with Application.CreateForm(...). As an aside, if you are sure that you will free the form, then it is better (in order to speed the things up) to call TmyForm.Create(nil) - iow without any owner.
If you want to do some kind of initialization at startup you can have a procedure / method in the project file tied to a form / data module already created and run it before application run.
For example:
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TdmoMain, dmoMain); //<--this is a data module
Application.CreateForm(TfrmMain, frmMain); //<--this will became the main form
Application.CreateForm(TfrmAbout, frmAbout);
//... other forms created here...
frmMain.InitEngine; //<--initialization code. You can put somewhere else, according with your app architecture
Application.Run;
end.
In this way you will have the project file clean and you will know exactly which is which.
HTH
When I wrote that article, I was thinking primarily of code outside the DPR file. People see the form-creation code generated by the IDE in the DPR file and think that's the best way to create forms generally, so they use that elsewhere in their programs. They sometimes use it in the main form's OnCreate event handler to create other forms their program needs, and then they hit problems because the program's main form isn't what they think it is.
In the code you provided, it's easy to call CreateForm just once. Use it for the main form, and for nothing else. The data module isn't a main form, so you don't need CreateForm's magic there.
SplashForm := TSplashForm.Create(Application);
SplashForm.Show;
SplashForm.Update; // force update
Application.Initialize;
// Change to this.
ClientData := TClientData.Create(Application);
SplashForm.Update; // force update
Application.CreateForm(TClientMainForm, ClientMainForm);
Application.ShowHint := True;
Application.Run;
ClientMainForm.ServerConnected := false;
// Remove these.
FreeAndNil(ClientMainForm);
FreeAndNil(ClientData);
You really shouldn't free the objects you've created here because you don't own them. They're owned by the global Application object, so let it take care of freeing them: Remove the two calls to FreeAndNil.
The article you refer to is incorrect. There are a number of valid reasons why you would want multiple calls to Application.CreateForm
1) Datamodules: You probably want these available all of the time. Best way to do this is Application.CreateForm. I know of applications with several themed Datamodules e.g. Customer, Invoice, Address to handle different areas of the database & encapsulate the functionality neatly. All of these are created in the .dpr
2) Big, slow loading stuff (which is a bad idea in & of itself but these things happen and are often inherited by support programmers...). Move the load time into the application startup exactly like your example code along with the splash screen updating. The users expect applications to take a while to get going thanks to the stirling efforts of our collegues working on Microsoft Office to lower the bar of expectations for the rest of us :)
So, in summary, don't worry your code is fine - but you can lose the "FreeAndNil" stuff. However small quick hitting Dialog type stuff is best invoked by:
with TMyform.Create(nil) do
try
//Setup
case ShowModal of
// Whatever return values you care about (if any)
end;
finally
Free;
end;
Short, sweet, to the point & minimises memory usage...
Our application used to make use of a common base form that all forms were meant to inherit from. I'd like to get rid of it for a number of reasons, ranging from the need to police that everyone uses it to several annoyances relating to Delphi's VFI implementation. It turns out that the bulk of the features it offered can be done in other, more reliable ways.
The one that I am not so sure about, is automatically positioning all forms in the center of their callers. So if I open Dialog A from my main form, it should be placed over the center of the main form. And if I then open Dialog B from Dialog A, it should be placed over the center of Dialog A and so on.
We used to take care of all this by setting the base form's Position property to poOwnerFormCenter and it worked great. But how do I do this app-wide?
I thought of using Screen.OnActiveFormChange, but I think this happens each time the form receives focus. I also thought of using Application.OnModalBegin but there doesn't seem to be an obvious way to find the form at the point this is called.
Has anyone tried this?
Well, obviously form inheritance is provided to solve exactly the problem you're trying to solve. Any solution is probably going to wind up mimicking form inheritance in some way.
Could you do something as simple as globally searching your code for "= class(TForm)" and replacing the TForm class with either your existing base form or a new, simplified base form class with only the functionality you need?
Failing that, you could try to modify the original TForm class itself to have the positioning behavior you want. Obviously, modifying the supplied classes is a little on the dangerous side.
If you are not going to go with a common base form, then I would suggest placing a non-visual component on each form. That component can inject the behaviors you want into the base form. If you want to have various different behaviors on different forms then give your component a role property that defines what role that form should have, and it can then inject different characteristics based on that role.
BTW, you can also have non-visual form inheritance, which is my preferred method of creating a common base class for all forms. It also has the advantage of adding properties to the form, and then based on those properties you can change the role or behavior of the form.
Without knowing more about your application, my advice would be to add the positioning code to each form individually - the advantages of not having a base class is that it makes it easier to have certain forms that do things slightly differently, and it keeps all the logic of a form together in one place.
I normally use the FormShow event for this, using the SetBounds() procedure.
With other non-form controls you can do the same thing by overriding the CMShowing message.
I took your idea of OnModalBegin and ran with it. The following is a "Hack", but it seems to work. To test simply drag around the form and click the button.
procedure TMainForm.Button1Click(Sender: TObject);
var
mForm: TForm;
begin
mForm := TForm.create(self);
mform.width := 300;
mform.height := 300;
mForm.ShowModal;
mForm.Free;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
application.OnModalBegin := modalbegin;
end;
procedure TMainForm.FormShow(Sender: TObject);
begin
if Screen.FormCount>1 then begin
screen.forms[Screen.FormCount-1].left := round((screen.forms[Screen.FormCount-2].left + screen.forms[Screen.FormCount-2].width/2) - screen.forms[Screen.FormCount-1].width/2);
screen.forms[Screen.FormCount-1].top := round((screen.forms[Screen.FormCount-2].top + screen.forms[Screen.FormCount-2].height/2) - screen.forms[Screen.FormCount-1].height/2);
application.processmessages;
screen.forms[Screen.FormCount-1].Caption := inttostr(screen.forms[Screen.FormCount-1].top)+','+inttostr(screen.forms[Screen.FormCount-1].left);
end;
end;
procedure TMainForm.ModalBegin(Sender: TObject);
begin
if Screen.FormCount>=0 then
screen.forms[Screen.FormCount-1].OnShow := FormShow;
end;
I have two forms,one is main and other is inherited form main.Lets say I have a function on the main form:
procedure FormMain.CreateButton;
begin
with TsButton.Create(Self) do begin
Width := 31;
Height := 31;
Left := 31;
Top := 31;
Visible := true;
Parent := Self;
end;
end;
Usually everything on the main form should be on the inherited form,but this is what I do:
I call CreateButton from mainForm ,but the button is only on the main form.
Is it possible to inherit that button too?
There's a difference between design-time and runtime. The form designer creates a definition for your form, which it instantiates at runtime. If you inherit one form from another, then it takes the base template and adds to it. But form-designer forms are only templates, like class definitions.
Now, at runtime, you instantiate a base form and a derived form, and it creates them from the templates stored in the resource section of your app. If you add something to the instance of the base form, you're modifying an individual instance, not the definition, so of course it's not going to show up on another instance. If you want to add a button dynamically to a form, you have to create it on that instance (in this case, the derived form) individually.
If you mean "inherited" the way it's normally meant, then the answer is no. (By normal, I mean you created your main form in the IDE, and then in the IDE created a descendant of that main form.)
In that case, controls created at runtime are not part of the inheritance tree, and the descendant knows nothing about it. You'll have to add the same code manually to the descendant as well.
What exactly are you trying to accomplish? If you know ahead of time that the button will be needed on both the base and the descendant forms (which you obviously do, since you're writing code to create the button), why not just actually drop the button on the ancestor?
If this were to inherit you would have no way of doing anything different on the two forms. Thus you do not want it to inherit your runtime changes!