Delphi Programming Guide
Delphi Programmer 

Menu  Table of contents

Part I - Foundations
  Chapter 1 – Delphi 7 and Its IDE
  Chapter 2 – The Delphi Programming Language
  Chapter 3 – The Run-Time Library
  Chapter 4 – Core Library classes
  Chapter 5 – Visual Controls
  Chapter 6 – Building the User Interface
  Chapter 7 – Working with Forms
Part II - Delphi Object-Oriented Architectures
  Chapter 8 – The Architecture of Delphi Applications
  Chapter 9 – Writing Delphi Components
  Chapter 10 – Libraries and Packages
  Chapter 11 – Modeling and OOP Programming (with ModelMaker)
  Chapter 12 – From COM to COM+
Part III - Delphi Database-Oriented Architectures
  Chapter 13 – Delphi's Database Architecture
  Chapter 14 – Client/Server with dbExpress
  Chapter 15 – Working with ADO
  Chapter 16 – Multitier DataSnap Applications
  Chapter 17 – Writing Database Components
  Chapter 18 – Reporting with Rave
Part IV - Delphi, the Internet, and a .NET Preview
  Chapter 19 – Internet Programming: Sockets and Indy
  Chapter 20 – Web Programming with WebBroker and WebSnap
  Chapter 21 – Web Programming with IntraWeb
  Chapter 22 – Using XML Technologies
  Chapter 23 – Web Services and SOAP
  Chapter 24 – The Microsoft .NET Architecture from the Delphi Perspective
  Chapter 25 – Delphi for .NET Preview: The Language and the RTL
       
  Appendix A – Extra Delphi Tools by the Author
  Appendix B – Extra Delphi Tools from Other Sources
  Appendix C – Free Companion Books on Delphi
       
  Index    
  List of Figures    
  List of tables    
  List of Listings    
  List of Sidebars  

 
Previous Section Next Section

Forms Inside Packages

In Chapter 9, I discussed the use of component packages in Delphi applications. Now I'm discussing the use of packages and DLLs for partitioning an application, so I'll begin talking about the development of packages holding forms. I've mentioned earlier in this chapter that you can use forms inside DLLs, but doing so causes quite a few problems. If you are building both the library and the executable file in Delphi, using packages results in a much better and cleaner solution.

At first sight, you might believe that Delphi packages are solely a way to distribute components to be installed in the environment. However, you can also use packages as a way to structure your code but, unlike DLLs, retain the full power of Delphi's OOP. Consider this: A package is a collection of compiled units, and your program uses several units. The units the program refers to will be compiled inside the executable file, unless you ask Delphi to place them inside a package. As discussed earlier, this is one of the main reasons for using packages.

To set up an application so that its code is split among one or more packages and a main executable file, you only need to compile some of the units in a package and then set up the options of the main program to dynamically link this package. For example, I made a copy of the "usual" color selection form and renamed its unit PackScrollF; then I created a new package and added the unit to it, as you can see in Figure 10.5.

Click To expand
Figure 10.5:  The structure of the package hosting a form in Delphi's Package Editor

Before compiling this package, you should change its default output directories to refer to the current folder, not the standard /Projects/Bpl subfolder of Delphi. To do this, go to the Directories/Conditional page of the package Project Options, and set the current directory (a single dot, for short) for the Output directory (for the BPL) and DCP output directory. Then compile the package and do not install it in Delphi—there's no need to.

At this point, you can create a normal application and write the standard code you'll use in a program to show a secondary form, as in the following listing:

uses
  PackScrollF;
   
procedure TForm1.BtnChangeClick(Sender: TObject);
var
  FormScroll: TFormScroll;
begin
  FormScroll := TFormScroll.Create (Application);
  try
    // initialize the data
    FormScroll.SelectedColor := Color;
    // show the form
    if FormScroll.ShowModal = mrOK then
      Color := FormScroll.SelectedColor;
  finally
    FormScroll.Free;
  end;
end;
   
procedure TForm1.BtnSelectClick(Sender: TObject);
var
  FormScroll: TFormScroll;
begin
  FormScroll := TFormScroll.Create (Application);
  // initialize the data and UI
  FormScroll.SelectedColor := Color;
  FormScroll.BitBtn1.Caption := 'Apply';
  FormScroll.BitBtn1.OnClick := FormScroll.ApplyClick;
  FormScroll.BitBtn2.Kind := bkClose;
  // show the form
  FormScroll.Show;
end;

One of the advantages of this approach is that you can refer to a form compiled into a package with the same code you'd use for a form compiled in the program. If you compile this program, the unit of the form will be bound to the program. To keep the form's unit in the package, you'll have to use run-time packages for the application and manually add the PackWithForm package to the list of run-time packages (this is not suggested by the Delphi IDE, because you have not installed the package in the development environment).

Once you've performed this step, compile the program; it will behave as usual. But now the form is in a DLL package, and you can modify the form in the package, recompile it, and run the application to see the effects. Notice, though, that for most changes affecting the interface portion of the package's units (for example, adding a component or a method to the form), you should also recompile the executable program calling the package.

Note 

You can find the package and the program testing it in the PackForm folder of the source code related to the current chapter. The code for the next example is in the same folder. The package and projects are all referenced by the project group (BPG) file within the folder.

Loading Packages at Run Time

In the previous example, I indicated that the PackWithForm package is a run-time package to be used by the application. This means the package is required to run the application and is loaded when the program starts, just as with the typical use of DLLs. You can avoid both aspects by loading the package dynamically, as you've done with DLLs. The resulting program will be more flexible, start more quickly, and use less memory.

An important element to keep in mind is that you'll need to call the LoadPackage and UnloadPackage Delphi functions rather than the LoadLibrary/SafeLoadLibrary and FreeLibrary Windows API functions. The functions provided by Delphi load the packages, but they also call their proper initialization and finalization code.

Besides this important element—which is easy to accomplish once you know about it—the program will require some extra code, because you cannot refer from the main program to the unit hosting the form. You cannot use the form class directly, nor access its properties or components—at least, not with the standard Delphi code. Both issues, however, can be solved using class references, class registration, and RTTI (run-time type information).

Let me begin with the first approach. In the form unit, in the package, I've added this initialization code:

initialization
  RegisterClass (TFormScroll);

As the package is loaded, the main program can use Delphi's GetClass function to get the class reference of the registered class and then call the Create constructor for this class reference.

To solve the second problem, I've made the SelectedColor property of the form in the package a published property, so that it is accessible via RTTI. Then I've replaced the code accessing this property (FormScroll.Color) with the following:

SetPropValue (FormScroll, 'SelectedColor', Color);

Summing up all these changes, here is the code used by the main program (the DynaPackForm application) to show the modal form from the dynamically loaded package:

procedure TForm1.BtnChangeClick(Sender: TObject);
var
  FormScroll: TForm;
  FormClass: TFormClass;
  HandlePack: HModule;
begin
  // try to load the package
  HandlePack := LoadPackage ('PackWithForm.bpl');
  if HandlePack > 0 then
  begin
    FormClass := TFormClass(GetClass ('TFormScroll'));
    if Assigned (FormClass) then
    begin
      FormScroll := FormClass.Create (Application);
      try
        // initialize the data
        SetPropValue (FormScroll, 'SelectedColor', Color);
        // show the form
        if FormScroll.ShowModal = mrOK then
          Color := GetPropValue (FormScroll, 'SelectedColor');
      finally
        FormScroll.Free;
      end;
    end
    else
      ShowMessage ('Form class not found');
    UnloadPackage (HandlePack);
  end
  else
    ShowMessage ('Package not found');
end;

Notice that the program unloads the package as soon as it is done with it. This step is not compulsory. I could have moved the UnloadPackage call in the OnDestroy handler of the form, and avoided reloading the package after the first time.

Now you can try running this program without the package available. You'll see that it starts properly, only to complain that it cannot find the package as you click the Change button. In this program, you don't need to use run-time packages to keep the unit outside your executable file, because you are not referring to the unit in your code. Also, the PackWithForm package doesn't need to be listed in the run-time packages. However, you must use run-time packages for it to work at all, or else your program will include VCL global variables (such as the Application object) and the dynamically loaded package will include another version, because it will refer to the VCL packages anyway.

Warning 

When a program that loads a package dynamically is closed, you may experience access violations. Frequently, they occur because an object whose class is defined in the package is kept in memory even after the package is unloaded. When the program shuts down, it may try to free that object by calling the Destroy method of a non-existing VMT, and thus cause the error. Having said this, I know by experience that these types of errors are very difficult to track and fix. I suggest that you make sure to destroy all the objects before unloading the package.

Using Interfaces in Packages

Accessing forms' classes by means of methods and properties is much simpler than using RTTI all over the place. To build a larger application, I definitely try to use interfaces and to have multiple forms, each implementing a few standard interfaces defined by the program. An example cannot really do justice to this type of architecture, which becomes relevant for a large program, but I've tried to build a program to show how this idea can be applied in practice.

Note 

If you don't know much about interfaces, I suggest you refer to the related portion of Chapter 2, "The Delphi Programming Language," before reading this section.

To build the IntfPack project, I've used three packages plus a demo application. Two of the three packages (IntfFormPack and IntfFormPack2) define alternative forms used to select a color. The third package (IntfPack) hosts a shared unit, used by both other packages. This unit includes the definition of the interface. I couldn't add it to both other packages because you cannot load two packages that have the same name with a unit (even by run-time loading).

The IntfPack package's only file is the IntfColSel unit, displayed in Listing 10.1. This unit defines the common interface (you'll probably have a number of them in real-world applications) plus a list of registered classes; it mimics Delphi's RegisterClass approach, but makes available the complete list so that you can easily scan it.

Listing 10.1: The IntfColSel Unit of the IntfPack Package
Start example
unit IntfColSel;
   
interface
   
uses
  Graphics, Contnrs;
   
type
  IColorSelect = interface
  ['{3F961395-71F6-4822-BD02-3B475FF516D4}']
    function Display (Modal: Boolean = True): Boolean;
    procedure SetSelColor (Col: TColor);
    function GetSelColor: TColor;
    property SelColor: TColor
      read GetSelColor write SetSelColor;
  end;
   
procedure RegisterColorSelect (AClass: TClass);
   
var
  ClassesColorSelect: TClassList;
   
implementation
   
procedure RegisterColorSelect (AClass: TClass);
begin
  if ClassesColorSelect.IndexOf (AClass) < 0 then
    ClassesColorSelect.Add (AClass);
end;
   
initialization
  ClassesColorSelect := TClassList.Create;
   
finalization
  ClassesColorSelect.Free;
   
end.
End example

Once you have this interface available, you can define forms that implement it, as in the following example taken from IntfFormPack:

type
  TFormSimpleColor = class(TForm, IColorSelect)
    ...
  private
    procedure SetSelColor (Col: TColor);
    function GetSelColor: TColor;
  public
    function Display (Modal: Boolean = True): Boolean;

The two access methods read and write the value of the color from some components of the form (a ColorGrid control in this case), whereas the Display method internally calls either Show or ShowModal, depending on the parameter:

function TFormSimpleColor.Display(Modal: Boolean): Boolean;
begin
  Result := True; // default
  if Modal then
    Result := (ShowModal = mrOK)
  else
  begin
    BitBtn1.Caption := 'Apply';
    BitBtn1.OnClick := ApplyClick;
    BitBtn2.Kind := bkClose;
    Show;
  end;
end;

As you can see from this code, when the form is modeless the OK button is turned into an Apply button. Finally, the unit has the registration code in the initialization section, so that it is executed when the package is dynamically loaded:

RegisterColorSelect (TFormSimpleColor);

The second package, IntfFormPack2, has a similar architecture but a different form. You can look it up in the source code (I've not discussed the second form here as its code doesn't add much to the structure of the example).

With this architecture in place, you can build a rather elegant and flexible main program, which is based on a single form. When the form is created, it defines a list of packages (HandlesPackages) and loads them all. I've hard-coded the package in the code of the example, but of course you can search for the packages of the current folder or use a configuration file to make the application structure more flexible. After loading the packages, the program shows the registered classes in a list box. This is the code of the LoadDynaPackage and FormCreate methods:

procedure TFormUseIntf.FormCreate(Sender: TObject);
var
  I: Integer;
begin
  // loads all runtime packages
  HandlesPackages := TList.Create;
  LoadDynaPackage ('IntfFormPack.bpl');
  LoadDynaPackage ('IntfFormPack2.bpl');
   
  // add class names and select the first
  for I := 0 to ClassesColorSelect.Count - 1 do
    lbClasses.Items.Add (ClassesColorSelect [I].ClassName);
  lbClasses.ItemIndex := 0;
end;
   
procedure TFormUseIntf.LoadDynaPackage(PackageName: string);
var
  Handle: HModule;
begin
  // try to load the package
  Handle := LoadPackage (PackageName);
  if Handle > 0 then
    // add to the list for later removal
    HandlesPackages.Add (Pointer(Handle))
  else
    ShowMessage ('Package ' + PackageName + ' not found');
end;

The main reason for keeping the list of package handles is to be able to unload them all when the program ends. You don't need these handles to access the forms defined in those packages; the run-time code used to create and show a form uses the corresponding component classes. This is a snippet of code used to display a modeless form (an option controlled by a check box):

var
  AComponent: TComponent;
  ColorSelect: IColorSelect;
begin
  AComponent := TComponentClass
    (ClassesColorSelect[LbClasses.ItemIndex]).Create (Application);
  ColorSelect := AComponent as IColorSelect;
  ColorSelect.SelColor := Color;
  ColorSelect.Display (False);

The program uses the Supports function to check that the form really does support the interface before using it, and also accounts for the modal version of the form; but its essence is properly depicted in the preceding four statements.

By the way, notice that the code doesn't require a form. A nice exercise would be to add to the architecture a package with a component encapsulating the color selection dialog box or inheriting from it.

Warning 

The main program refers to the unit hosting the interface definition but should not link this file in. Rather, it should use the run-time package containing this unit, as the dynamically loaded packages do. Otherwise the main program will use a different copy of the same code, including a different list of global classes. It is this list of global classes that should not be duplicated in memory.


 
Previous Section Next Section


 


 

Delphi Sources


Copyright © 2004-2024 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide
ร๐๓๏๏เ ยส๎ํ๒เ๊๒ๅ   Facebook   ั๑๛๋๊เ ํเ Twitter