![]() |
|
Forms Inside PackagesIn 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. 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.
Loading Packages at Run TimeIn 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.
Using Interfaces in PackagesAccessing 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.
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
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.
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.
|
|
Copyright © 2004-2025 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide |
|