A Hierarchy of Forms (Views: 34)
Save Development Time with Form Inheritance
There I sat - in a hurry and already tired. I didn't want to build those four screens for listing four kinds of support data for this application. These screens would supply the pull-down lists for data-entry fields on various other tables. I wasn't looking forward to the work involved in building edit screens to go with each list either. Then I remembered form inheritance.
Form inheritance allows programmers to build the framework of a form that will be used repeatedly. It's an extension of Delphi's TForm, and it significantly speeds up the routine work of programming a complete application.
The hierarchy of forms I use has evolved over time, but the basic set has served me quite well for two years. These forms have saved hundreds of hours of development and debugging time, and have given my applications a consistent look and feel. They provide sharper-looking products, and reduce the time it takes my users to learn my applications.
The hierarchy starts with the TForm object supplied with the VCL. I added a couple of panels to it to make TfrmGenrForm (see Figure 1). I placed the control buttons for the descendant forms along the right side, so one panel, named pnlControls, is right-aligned on the base form. I gave it a width that accommodates the buttons I use on the descendant forms. (The samples available for download use a slightly modified TBitBtn, named TGenrBtn, which is 92 pixels wide, so I set pnlControl's width to 112; see end of article for download details.) The other panel is named pnlForm, and is aligned to the remainder of the form's client area.
Figure 1: The first-generation form, TfrmGenrForm.
Of course you can arrange the panels any way you like, and you can even add company logos or other special identifiers. By placing them in an ancestor object, they can remain the same throughout the application. The form object also has certain properties set, which relieves me of having to set them consistently each time I start a new form.
I also added the stand-alone procedure RunGenrForm. It takes a TfrmGenrForm as its sole argument, so it can run any of its descendants as well. It runs the form by calling its ShowModal method, and then frees it from memory:
procedure RunGenrForm(frm: TfrmGenrForm);
with frm do
If the descendant form needs no specific input from the calling routine, it can be called using this procedure. The code for making such a call is simple:
procedure TfrmMain.actShowListExecute(Sender: TObject);
where actShowListExecute is a TAction method within the calling form, and TfrmShowList is the descendant form being created.
Before adding a descendant of any of these ancestor forms to a project, it's a good idea to add its ancestor(s). Also, because they're never instantiated, you have to be sure Delphi doesn't set them up to be auto-created. In Delphi, select Project | Options, and move the form to the Available Forms list box.
The Generic List Display Screen
I like to build lists to display and maintain the support data that's used to supply the pull-down pick lists for data-entry fields elsewhere in the application. For many such lists, the user interface requirements are essentially the same: Users need ways to add, edit, and delete records, as well as ways to exit the screen. By building that common functionality into an ancestor form, the programmer can concentrate on the unique parts of the application.
I created a form, TfrmGenrList, which includes a DBGrid component for displaying data from a table (see Figure 2). When creating a descendant form, add a uses statement that includes your data module, specify the grid's DataSource, and set up the columns. Finally, add the specifics of adding, editing, and deleting records by overriding the DoAdd, DoEdit, and DoDrop methods. The remainder of the normal processing is done at the ancestor level.
Figure 2: The TfrmGenrList form. The pnlControls buttons and the pop-up menu items reference the TActionList items.
TfrmGenrList has a TActionList component with actions for adding a new record, and editing and deleting the current record. The form provides several ways to access these actions. Most obvious are the pop-up menu and the series of buttons on the pnlControls panel. The grid has an OnDblClick event handler that also invokes the edit action. The OnKeyDown event handler invokes the add action when users press [Insert], the edit action when users press [Enter], and the delete action when users press [Delete]:
procedure TfrmGenrList.grdDBKeyDown(Sender: TObject;
var Key: Word; Shift: TShiftState);
case Key of
Key := 0
The form also contains a method for enabling or disabling various actions. For example, if there are no records in the underlying database table, the SetButtons procedure disables the edit and drop actions. If the programmer has set the IsReadOnly property to True, the add and drop actions are made invisible, and the edit action's caption is changed to "View" (see Figure 3).
actAdd.Enabled := not IsReadOnly;
actAdd.Visible := actAdd.Enabled;
HasRecs := False;
with grdDB do
if Assigned(DataSource) then
with DataSource do
if Assigned(DataSet) then
with DataSet do
HasRecs := not BOF or not EOF;
actEdit.Enabled := HasRecs;
if IsReadOnly then
actEdit.Caption := 'View'
actEdit.Caption := 'Edit';
actDrop.Enabled := HasRecs and not IsReadOnly;
actDrop.Visible := actAdd.Visible
Figure 3: Code for enabling or disabling various actions.
Unless the table or query is already open, the form's FormActivate routine should activate it, and the FormClose method should close it. Inheriting from TfrmGenrList requires one additional task: implementing three methods for overriding the virtual abstract ones. The descendant form's DoAdd, DoEdit, and DoDrop routines actually insert, edit, and delete the records.
The Generic Edit Screen
Most of my edit screens use many of the same features as my list screens. The user may be modifying simple look-up data, complex database records, system options from an .ini file or the registry, or other information. In any case, I need an OK button to save the changes, and a Cancel button to discard them. It's also a helpful user interface feature to provide a visual indicator to show whether the data has been changed (see Figure 4).
Figure 4: The TfrmGenrEdit form. The TSpeedButtons in the pnlControls panel serve as a visual indicator of whether the user has changed the data.
Because there are several ways to exit the modal dialog box, I consolidated the save/cancel confirmation questions in the FormCloseQuery procedure (not shown). The OK button's ModalResult property sets the form's ModalResult to mrOK. The Cancel button and the other exit paths set it to mrCancel. Therefore, if ModalResult is mrOK, I give the user the option of saving or canceling. If ModalResult is mrCancel, the user is asked whether to discard the changes. If the user cancels the confirmation dialog box, I just set FormCloseQuery's CanClose parameter to False so the form stays open.
There are times on the edit form when I don't want the user to have to confirm whether to save or cancel changes. For example, my date picker dialog box is based on TfrmGenrEdit. However, date picking usually occurs as part of another function, and it would be confusing to ask the user to save the date change. Therefore, TfrmGenrEdit has a property named DoConfirm. The default setting is True; the date picker sets it to False.
To use TfrmGenrEdit, create a new form that inherits from it, and add data-entry controls as you would normally. For each such control, set its OnChange or OnClick event handler to DataChanging. If you have other processing to perform in one of these event handlers, just add the call to DataChanging to your routine. This method enables TfrmGenrEdit's save and discard speed buttons to show that something has changed, which, in turn, tells the form to confirm the exit process.
Finally, the form implements two protected virtual routines: PostChanges and CancelChanges. These routines are called by FormCloseQuery, and by the save and discard speed buttons. They should be overridden to store any changes the user has made, or to restore the original values. At the end of your implementation of these two methods, be sure to call inherited, so the save and discard speed buttons will be reset.
The Generic Print Screen
Most applications generate reports or perform other tasks that take place over a period of time. The TfrmGenrPrnt form provides the basic functionality to support such features (see Figure 5). It supplies a Close button to exit the screen, and a Print button to run the process. You can change the Print button's caption and glyph, if you're using the form for some other process, such as importing or exporting data. TfrmGenrPrnt also has a progress gauge and a Cancel button, both of which are hidden except when the process is running.
Figure 5: The TfrmGenrPrnt screen image. This form can be used as a basis for controlling any process that executes over a period of time.
To run a descendant form, the user selects options you have placed on the pnlForm panel of the descendant form, and then presses the Print button. The btnPrintClick event handler (see Figure 6) disables the Print and Close buttons, and makes the progress gauge and Cancel buttons visible. It then calls the DoPrint procedure. This routine is declared as a virtual abstract method, so the descendant has to declare and define an overriding DoPrint method.
procedure TfrmGenrPrnt.btnPrintClick(Sender: TObject);
btnClose.Enabled := False;
btnPrint.Enabled := False;
with gagProg do
if HideGagProg then
Visible := False
MaxValue := 100;
Progress := 0;
Visible := True
btnCancel.Visible := True;
FStopping := False;
FStopped := False;
Cursor := crHourglass;
Cursor := crDefault;
btnCancel.Visible := False;
gagProg.Visible := False;
btnClose.Enabled := True;
btnPrint.Enabled := True
Figure 6: The TfrmGenrPrnt.btnPrintClick procedure.
The descendant's DoPrint does the real work, typically in a loop. It should first determine the number of iterations for the loop, then set the progress gauge's MaxValue property accordingly. At the end of the iterations, the routine should increment the Progress property. (For those processes that don't lend themselves to the progress gauge paradigm, the ancestor publishes a HideGagProg Boolean variable. It defaults to False, but if the descendant sets it to True, the btnPrintClick handler doesn't display the gauge. The programmer should then provide some other way to show users that something is occurring.)
Part of DoPrint's loop control should also check the ancestor's Stopping property. It's initialized to False, but if the user clicks the Cancel button, the event handler sets Stopping to True. The next time the descendant checks the property, the ancestor displays a dialog box asking whether to cancel the process. If not, Stopping is reset to False, and the descendant's loop continues.
To iterate through a TTable, a descendant form's DoPrint method might resemble Figure 7.
// Confirm user's setting choices, open table(s), etc.
with tblXXXX do
gagProg.MaxValue := RecordCount;
while not EOF and not Stopping do
// Process the record.
with gagProg do
Progress := Progress + 1;
// Close the table(s), etc.
Figure 7: A form's DoPrint method iterating through a TTable.
Once the descendant's DoPrint routine finishes, the ancestor's btnPrintClick handler completes the process by hiding the progress gauge and Cancel button, and re-enabling the Close and Print buttons.
In the two years since I started using this hierarchy of inherited forms, I have saved literally hundreds of hours of programming time. Also, my customers have found it easy to learn the applications and to move from one application to another. This process has been well worth the time it took to learn. In a nutshell, it allows true rapid application development.
Component Download: http://www.baltsoft.com/files/dkb/attachment/Hierarchy_of_Forms.ziphttp://www.baltsoft.com/files/dkb/attachment/Hierarchy_of_Forms.zip
<< Back to main page