Why use packages?
The answer is simple: because of the power of the package. Design-time packages simplify the release and installation of custom components; run-time packages inject fresh power into traditional programming. Once you compile reusable code into a runtime library, you can share it across multiple applications. All applications can access standard components through packages, and Delphi itself does this. Because the application does not have to copy a separate component library in the executable file, this greatly saves system resources and disk space. Additionally, packages reduce the time spent on compilation because you only need to compile application-specific code.
If packages can be used dynamically, then we can get more benefits. Packages provide a novel modular approach to developing applications. Sometimes you may want to make certain modules optional components of the application, such as an accounting system that comes with an optional HR module. In some cases, you only need to install the basic application, while in other cases you may need to install additional HR modules. This modular approach can be easily implemented through package technology. In the past, this could only be achieved by dynamically loading a DLL, but using Delphi's packaging technology, you can "package" each module type of the application into bundles. In particular, class objects created from packages are owned by the application and can therefore interact with objects in the application.
Runtime packages and applications
Many developers only think of Delphi packages as a place to put components, when in fact packages can (and should) be used in modular application design.
To demonstrate how to use packages to modularize your application, let's create an example:
1. Create a new Delphi program with two forms: Form1 and Form2;
2. Remove Form2 from the automatically created form list (PRoject | Options | Forms);
3. Place a button on Form1 and enter the following code in the button's OnClick event handler:
with TForm2.Create(application) do
begin
ShowModal;
Free;
End;
4. Remember to add Unit2 to the uses clause of Unit1;
5. Save and run the project.
We created a simple application that displays a form with a button that, when clicked, creates and displays another form.
But what should we do if we want to include Form2 in the above example into a reusable module and make it still work normally?
The answer is: Bao!
To create a package for Form2 requires the following work:
1. Open the Project Manager (View | Project Manager);
2. Right-click the Project Group and select "Add NewProject...";
3. Select “Package” in the “New” project list;
4. Now you should be able to see the package editor;
5. Select the “Contains” item and click the “Add” button;
6. Then click the "Browse..." button and select "Unit2.pas";
7. The package should now contain the "Unit2.pas" unit;
8. Finally save and compile the package.
Now we have completed the package. There should be a file named "package1.bpl" in your Project/BPL directory. (BPL is the abbreviation of Borland Package Library, and DCP is the abbreviation of Delphi CompiledPackage.)
This package is complete. Now we need to turn on the package options switch
and recompile the original application.
1. Double-click "Project1.exe" in the project manager to select the project;
2. Right-click and select "Options..." (you can also select Project | Options... from the menu);
3. Select the “Packages” option page;
4. Select the "Build with runtime packages" check box;
5. Edit the "Runtime packages" edit box: "Vcl50;Package1" and click the "OK" button;
6. Note: Do not remove Unit2 from the application;
7. Save and run the application.
The application will run as before, but the difference can be seen in the file size.
Project1.exe is now only 14K in size, compared to 293K previously. If you use the Resource Browser to view the contents of the EXE and BPL files, you will see that the Form2 DFM and code are now saved in the package.
Delphi completes static linking of packages during compilation. (This is why you cannot remove Unit2 from the EXE project.)
Think about what you can gain from this: you can create a data access module in the package, and when you change the data access rules (such as switching from BDE connections to ADO connections), slightly modify and republish the package. Alternatively, you could create a form in one package that displays the message "This option is not available in the current version" and then create a fully functional form in another package with the same name. Now we have the product in "Pro" and "Enterprise" versions without any effort.
Dynamic loading and unloading of packages
In most cases, a statically linked DLL or BPL will suffice. But what if we don't want to release BPL? "The dynamic link library Package1.bpl cannot be found in the specified directory" is the only message we can get before the application terminates. Or, in a modular application, can we use any number of plugins?
We need to dynamically connect to the BPL at runtime.
For DLLs, there is a simple method, which is to use the LoadLibrary function:
function LoadLibrary(lpLibFileName: Pchar): HMODULE;stdcall;
After loading the DLL, we can use the GetProcAddress function to call the DLL's exported functions and methods:
function GetProcAddress(hModule: HMODULE; lpProcName:LPCSTR): FARPROC; stdcall;
Finally, we use FreeLibrary to uninstall the DLL:
function FreeLibrary(hLibModule: HMODULE): BOOL;stdcall;
In the following example we dynamically load Microsoft's HtmlHelp library:
function TForm1.ApplicationEvents1Help(Command: Word; Data: Integer; var CallHelp: Boolean):Boolean;
type
TFNHtmlHelpA = function(hwndCaller: HWND; pszFile: PansiChar; uCommand: UINT;dwData: Dword): HWND; stdcall;
var
HelpModule: Hmodule;
HtmlHelp: TFNHtmlHelpA;
begin
Result := False;
HelpModule := LoadLibrary('HHCTRL.OCX');
if HelpModule <> 0 then
begin
@HtmlHelp := GetProcAddress(HelpModule, 'HtmlHelpA');
if @HtmlHelp <> nil then
Result := HtmlHelp(Application.Handle,Pchar(Application.HelpFile), Command,Data) <> 0;
FreeLibrary(HelpModule);
end;
CallHelp := False;
end;
Dynamically loading BPL
We can use the same simple method to deal with BPL, or should I say basically the same simple way.
We can dynamically load packages using the LoadPackage function:
function LoadPackage(const Name: string): HMODULE;
Then use the GetClass function to create a TPersistentClass type object:
function GetClass(const AclassName: string):TPersistentClass;
After everything is done, use UnLoadPackage(Module:HModule);
Let's make some small changes to the original code:
1. Select "Project1.exe" in the project manager;
2. Right-click it and select "Options...";
3. Select the “Packages” option page;
4. Remove "Package1" from the "Runtime packages" edit box and click the OK button;
5. In the Delphi toolbar, click the "Remove file from project" button;
6. Select "Unit2 | Form2" and click OK;
7. Now in the source code of "Unit1.pas", remove Unit2 from the uses clause;
8. Enter the OnClick time code of Button1;
9. Add two variables of type HModule and TPersistentClass:
var
PackageModule: HModule;
AClass: TPersistentClass;
10. Use the LoadPackage function to load the Pacakge1 package:
PackageModule := LoadPackage('Package1.bpl');
11. Check whether PackageModule is 0;
12. Use the GetClass function to create a persistent type:
AClass := GetClass('TForm2');
13. If this persistent type is not nil, we can return to the previous
Create and use objects of this type the same way:
with TComponentClass(AClass).Create(Application) as TcustomForm do
begin
ShowModal;
Free;
end;
14. Finally, use the UnloadPackage process to uninstall the package:
UnloadPackage(PackageModule);
15. Save the project.
Here is the complete list of OnClick event handlers:
procedure TForm1.Button1Click(Sender: Tobject);
var
PackageModule: HModule;
AClass: TPersistentClass;
begin
PackageModule := LoadPackage('Package1.bpl');
if PackageModule <> 0 then
begin
AClass := GetClass('TForm2');
if AClass <> nil then
with TComponentClass(AClass).Create(Application) as TcustomForm do
begin
ShowModal;
Free;
end;
UnloadPackage(PackageModule);
end;
end;
Unfortunately, that's not all.
The problem is that the GetClass function can only search for registered types. Form classes and component classes that are usually referenced in a form are automatically registered when the form is loaded. But in our case, the form cannot be loaded early. So where do we register the type? The answer is, in the bag. Each unit in the package is initialized when the package is loaded and cleaned up when the package is unloaded.
Now back to our example:
1. Double-click "Package1.bpl" in the project manager;
2. Click the + sign next to “Unit2” in the “Contains” section;
3. Double-click "Unit2.pas" to activate the unit source code editor;
4. Add the initialization section at the end of the file;
5. Use the RegisterClass procedure to register the form type:
RegisterClass(TForm2);
6. Add a finalization section;
7. Use the UnRegisterClass procedure to unregister the form type:
UnRegisterClass(TForm2);
8. Finally, save and compile the package.
Now we can safely run "Project1" and it will work just like before, but now you can load the packages however you want.
end
Remember, whether you are using packages statically or dynamically, turn on Project | Options | Packages | Build with runtime packages.
Before you uninstall a package, remember to destroy all class objects in the package and unregister all registered classes. The following process may help you:
procedure DoUnloadPackage(Module: HModule);
var
i: Integer;
M: TMemoryBasicInformation;
begin
for i := Application.ComponentCount - 1 downto 0 do
begin
VirtualQuery(GetClass(Application.Components[i].ClassName), M, Sizeof(M));
if (Module = 0) or (HMODULE(M.AllocationBase) = Module) then
Application.Components[i].Free;
end;
UnregisterModuleClasses(Module);
UnloadPackage(Module);
end;
Before loading the package, the application needs to know the names of all registered classes. One way to improve this situation is to create a registration mechanism that tells the application the names of all classes registered by the package.
Example
Multiple packages: Packages do not support circular references. That is, a unit cannot reference a unit that already references that unit (hehe). This makes it difficult for certain values in the calling form to be set by the called method.
The solution to this problem is to create some additional packages that are referenced by both the calling object and the objects in the package. Imagine how we make Application the owner of all forms? The variable Application is created in Forms.pas and included in the VCL50.bpl package. You may have noticed that your application not only needs to compile VCL50.pas, but also requires VCL50 in your package.
In our third example, we design an application to display customer information and, on demand, customer orders (dynamically).
So where can we start? like all database applications
The procedure is the same, we need to connect. We create a main data module containing a TDataBase connection. Then we encapsulate this data module in a package (cst_main).
Now in the application, we create a customer form and reference DataModuleMain (we statically link VCL50 and cst_main).
Then we create a new package (cst_ordr) that contains the customer order form and require cst_main. Now we can dynamically load cst_ordr in the application. Since the main data module already exists before the dynamic package is loaded, cst_ordr can directly use the application's main data module instance.
The picture above is a functional diagram of this application:
Replaceable Packages: Another use case for packages is creating replaceable packages. Implementing this functionality does not require the dynamic loading capabilities of the package. Suppose we want to release a time-limited trial version of the program, how to achieve this?
First we create a "Splash" form, usually a picture with the word "Trial" on it, and display it during application startup. Then we create an "About" form that provides some information about the application. Finally, we create a function that tests whether the software is out of date. We encapsulate these two forms and this function into a package and release it with the trial version of the software.
For the paid version, we also create a "Splash" form and an "About" form - with the same class names as the previous two forms - and a test function (which does nothing) and add them Encapsulated into a package with the same name.
What what? Is this useful, you ask? Well, we can release a trial version of the software to the public. If a customer purchases the app, we only need to send the non-trial package. This greatly simplifies the software release process, as only one installation and one registration package upgrade are required.
The package opens another door to modular design for the Delphi and C++ Builder development communities. With packages you no longer need to pass window handles around, no more callback functions, no more other DLL technology. This also shortens the development cycle of modular programming. All we have to do is let Delphi's packages work for us.