Thursday, October 16, 2008

 

Delphi plugin by example


One of the top asked questions in Delphi newsgroups is how to place a form contained in a Dll into our application forms. This method allows to create pluggable applications, easier to maintain and customize.

To accomplish our task, first of all we'll create a Dll using File -> New -> Other -> Dll Wizard. Delete the comment aboud memory managment then Save the Dll as "dllform.dpr".

Now create a form inside the Dll with File -> New -> Form. Go to the Object Inspector and change the name of the new form to MyDllForm then add some controls like buttons, labels and everything you want, then remove the "var Form1: TForm1;" reference, you don't need it. Save the form as "myform".

Go back to the Dll source by going to Project -> View Source, and look at the uses clause, a reference to the recently created form unit must be there. Try to compile by hitting Ctrl + F9, if anything fails, re-check the previous paragraphs.

Adding exportable code

As you can't export a Form directly, you must create an exportable function who can export your form class.

Go to your form's source and add just above the "implementation" section, this code:
  // To get a reference of your form's class
TMyFormClass = class of TMyDllForm;
// To be able to export the form class.
function MyFormClass: TFormClass; stdcall; export;
Then create the body of MyFormClass function, go below the "implementation" section and write this:
function MyFormClass: TFormClass; stdcall;
begin
Result := TMyDllForm;
end;
Now, you must tell your library what functions to export. This is easy, just go to Project -> View Source and add this before the "end.":
exports
MyFormClass;
Before compiling be sure to activate "Build with Runtime Packages" in Project -> Options -> Packages. When you click the checkbox, a ton of packages separated by a comma appears just below, leave only the "vcl" package.

The main form

Create a new application by going to File -> New -> VCL Forms Application. This creates a new application with a main form called Form1. Go to the form and add a TButton and a TPanel.

Why a TPanel?, whell, the TPanel will contain the form we'll load from the Dll. You can use a TPageControl with a TTabSheet instead, or any other container.

Now we'll add a couple of private fields in the TForm1 class:

private
FLoadedForm: TForm;
FLibHandle: Cardinal;
Now we'll implement a method to dynamically load the Dll. Go to the Form1, then to the Object Inspector -> Events and double click on "OnCreate" and "OnDestroy" to create these two events, then write this code inside them:

procedure TForm5.FormCreate(Sender: TObject);
begin
FLibHandle := LoadLibrary('dllform.dll');
end;

procedure TForm5.FormDestroy(Sender: TObject);

begin
FLoadedForm.Free;
FreeLibrary(FLibHandle);
end;

After that, double click on the TButton to add the code needed to call the exported function in the Dll and create an instance of TMyForm:

procedure TForm1.Button1Click(Sender: TObject);
type
TMyFormClass = function: TFormClass; stdcall;

var
lMyFormClass: TMyFormClass;
begin
if not Assigned(FLoadedForm) then
begin
lMyFormClass := GetProcAddress(FLibHandle, 'MyFormClass');
if @lMyFormClass <> nil then
begin
// Create a TMyForm instance (not visible)
FLoadedForm := lMyFormClass.Create(nil);
// Place the Panel from TMyForm in Panel1
FLoadedForm.Controls[0].Parent := Panel1;
end;
end;
end;
Again, before compiling remember to enable "Build with Runtime Packages", as you did when compiling the Dll. If you don't do this, an "Cannot assign a TFont to a TFont" error will be raised when you click Button1.

You can download a sample project from here.
Comments:
Thanks for this. Be aware the compiled example Leonardo provides may not run after expanding the zip download. I got an error "rtl120bpl not found". Recompiling both the exe and dll using the compile with runtime option worked fine with Deplhi 2007.
 
Yes, SteveJG you are right. I used Delphi 2009 and forgot to include rtl120.bpl inside the zip.
 
hi (im amos from embarcadero forums)

thank you for this simple sample. trying your step by step worked ok. as i understood, your technique was to "move" the panel (of the dll form) to the panel on the hosting application and u dont "deal" with the dll form itself.

with your permission, i have several points i want to clear out:

1) in your sample u wrote the exports clause should be in the dll project source while it should be in myform unit.

2) i added a TColorDialog component to the dll form and tried to execute it: FLoadedForm.ColorDialog1.execute; but as expected it didnt even compile, saying ColorDialog1 is undeclared. so how can i access such components from the host application?

3) when u make different dlls each one has its own form and class (tformp1 tformp2 and so on). if i understood it correctly, with the line: TMyFormClass = class of TFormp1; (for example) the host application doesnt need to care what is the real form type and for the application it always deals with "TFormClass". right?

again, thank you very much for this.
 
hi (im amos from embarcadero forums)

thank you for this simple sample. trying your step by step worked ok. as i understood, your technique was to "move" the panel (of the dll form) to the panel on the hosting application and u dont "deal" with the dll form itself.

with your permission, i have several points i want to clear out:

1) in your sample u wrote the exports clause should be in the dll project source while it should be in myform unit.

2) i added a TColorDialog component to the dll form and tried to execute it: FLoadedForm.ColorDialog1.execute; but as expected it didnt even compile, saying ColorDialog1 is undeclared. how can i have access to such components?

3) when u make different dlls each one has its own form and class (tformp1 tformp2 and so on). if i understood it correctly, with the line: TMyFormClass = class of TFormp1; (for example) the host application doesnt need to care what is the real form type and for the application it always deals with "TFormClass". right?
 
Hi, Amos I'll try to answer your questions:

1) Yes, you are right. But it doesn't matter where you place the "exportable" functions, the important thing here is that your project be able to "know" where is your function.

In my example, the project includes a "uses" clause pointing to the unit where the exportable function lives, allowing the Exports section to find the functions.

2) Yes, in my example I'm using an abstract method of form invocation, you can place any form without knowing which class it is.

It is supposed your host application doesn't know anithing about your plugins, that said, how your app could know the ColorDialog1.Execute method?.

Anyway, if you want your plugins do something different than TForm, you can create a base form class with that functionality, then inherit all of your plugin forms from it. The Base form will be a skeleton accesible both from the Dll and the Host app, then you can cast FLoadedForm with that.

For example, to access a method like ExecColorDialog:

TBaseAmosForm(FLoadedForm).ExecColorDialog;

Don't forget to add the unit containing TBaseAmosForm in both the Host app and the Dll.

3) Well, I made a mistake here. Looking at my code, in the function MyFormClass: TFormClass, I'm returning TMyDllForm, and not TMyFormClass. So the line "TMyFormClas = class of TMyDllForm;" isn't neccesary at all, just delete it.
 
thank you for a quick reply

1) when i put exports in the dll project source it didnt compile saying it expects a statement and not exports

2) maybe i will give a better example of what i need: im using skins in my application. currently, each form contains the skin provider linked directly to the skin engine which is on the main form. so i need a way to access each form's skin provider, so once i load the form, i can link the skin engine to its skn provider, but since im using the panel and not the form itself, im "losing" the skin provider component which is on the form. or am i missing something?

3) removed and forgotten :)

4) additional question if i may: usually exe's weighs 300+K just by having a form because it integrates everything inside the exe (no runtime libs are needed). now i compiled WITH runtime packages and the form weighs only 20K. will it work on a computer with no delphi installation on it?
 
1) Are you sure?

I tested what you said with this simple example and it compiles without a problem:

library Project6;
uses
Classes;

function Example: Integer; stdcall; export;
begin
Result := 1;
end;

exports
Example;

end.

2) You must export another function from the Dll receiving the instance of the Skin provider as a parameter just before instantiating your plugin. In your Dll you can set as a global variable the skin object, then when the form is created (in the onCreate event), you must assign the skin.

4) Yes, because a lot of stuff is compiled inside your .exe if you compile without runtime packages. On the other hand, if you use Runtime Packages, you must distribute your exe plus every package listed below "Compile With Runtime Packages" checkbox. In my example, I forgot to include rtl120.bpl in the .zip
 
This comment has been removed by the author.
 
Amos, I deleted by mistake your last post. Can you contact me by email?

Leonardo.
 
hi, leonardo:
I have a question, what about building a delphi form DLL and can be called by VC++ applicaton or VB app, I tried, but failed.

Could you tell me your email address, my email address is striverwang@gmail.com.
Thank you in advance~
 
You can create a Dll with a function like "OpenDllForm" but be careful and take care of the parameter types you pass to the function. Objects/Classes from C++ are not compatible with Delphi, so you can't pass these type of parameters, you are only allowed to use simple types like Integer, Char*, and others.

I don't have VB nor VC++, but I'll try to create an example using a Lazarus .exe calling a Delphi dll.
 
Hi leonardo,
I have this project i'm working on and i need a way of doing what you have on your example, and I tryed it but i have a question.

1 - why not just doing this?
FLoadedForm := lMyFormClass.Create(nil);
FLoadedForm.Show;

you are exporting the form itself and using the panel only. it works. I'm I missing something?

2 - Now, this is very handy for plugins like you sayd, but plugins usually communicate with the main app. i tryed it several different ways, but i always get access violation. how can it be done?


if i can have this working nice, the next task is to use them as docked windows inside the main app. I've been having lots of problems combining dll forms and tab dock windows.
 
Carlos,

1) Yes, you can show the form as you said, my example was to show how one can "embed" the contents of a form inside a tabsheet, or panel.

2) To communicate with the main app, you just have to create exported functions from the Dll, then use the functions directly.
A Hint: The MyFormClass is an exported function!, just create another one and use it.

You can read this article to learn more:

http://archive.devx.com/dbzone/articles/hg0102/hg0102-1.asp
 
I've just tried this out (using Delphi 2007). Pretty straight forward, however I am having a problem and I can't see what is causing it.

The form in the DLL is just a TLabel and a TButton. The TButton is meant to toggle the font color of the TLabel.

Everything builds fine and when I press the Plugin invocation button on the main form, the Plugin form is loaded into the TPanel. Except the TButton is missing. The TLable is fine, but there is no button.

Am I missing something obvious?
 
Hi Keith, can you paste the code you use to call the Dll and set the parent of the plugin form?.
 
Here it is in all its glory. Hope you can read it OK.



// ========================================================== X1Plugin.dpr
library X1Plugin;

uses
SysUtils,
Classes,
X1Plugin_main in 'X1Plugin_main.pas' {X1Plugin_form};

{$R *.res}

exports
PluggableForm;
begin
end.

// ========================================================== X1Plugin_main.pas
unit X1Plugin_main;
interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

type
TX1Plugin_form = class(TForm)
label1: TLabel;
Button1: TButton;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
Fchanged : boolean;
public
{ Public declarations }
end;

TPluggableClass = class of TX1Plugin_form;

var
X1Pluged_form: TX1Plugin_form;

function PluggableForm : TFormClass; stdcall; export;

implementation

{$R *.dfm}

procedure TX1Plugin_form.Button1Click(Sender: TObject);
begin
case Fchanged of
false : label1.Font.Color := clRED;
true : label1.Font.Color := clBLACK;
end;
Fchanged := not Fchanged;
end;

procedure TX1Plugin_form.FormCreate(Sender: TObject);
begin
Fchanged := false;
label1.Font.color := clBlack;
end;

function PluggableForm : TFormClass;
begin
result := TX1Plugin_form;
end;
end.

// ========================================================== X1Plugin_main.dfm
object X1Plugin_form: TX1Plugin_form
Left = 0
Top = 0
Caption = 'X1Plugin_form'
ClientHeight = 131
ClientWidth = 487
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
PixelsPerInch = 96
TextHeight = 13
object label1: TLabel
Left = 24
Top = 48
Width = 455
Height = 23
Caption = 'Your'#39'e no good, You'#39're no good, Baby You'#39're no good'
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -19
Font.Name = 'Tahoma'
Font.Style = []
ParentFont = False
end
object Button1: TButton
Left = 200
Top = 85
Width = 75
Height = 25
Caption = 'Toggle'
TabOrder = 0
OnClick = Button1Click
end
end

// ============================================================================
// ========================================================== X1Socket.dpr
program X1Socket;

uses
Forms,
X1Socket_Main in 'X1Socket_Main.pas' {Form1};

{$R *.res}

begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.

// ========================================================== X1Socket_main.pas
unit X1Socket_Main;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ExtCtrls, StdCtrls;

type
TForm1 = class(TForm)
Button1: TButton;
Panel1: TPanel;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
FLoadedForm: TForm;
FLibHandle: Cardinal;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
type TPluggableClass = function: TFormClass; stdcall;
var
PluginForm : TPluggableClass;
begin
if not assigned(FloadedForm) then begin
PluginForm := GetProcAddress(FlibHandle, 'PluggableForm');
if (PluginForm <> nil) then begin
FLoadedForm := PluginForm.Create(nil);
FLoadedForm.COntrols[0].Parent := Panel1;
end;
end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
FLibHandle := LoadLibrary('X1Plugin.Dll');
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
FLoadedForm.Free;
FreeLibrary(FLibHandle);
end;

end.

// ========================================================== X1Socket_main.dfm
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 301
ClientWidth = 562
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
TextHeight = 13
object Button1: TButton
Left = 484
Top = 272
Width = 75
Height = 25
Caption = 'Plug In'
TabOrder = 0
OnClick = Button1Click
end
object Panel1: TPanel
Left = 8
Top = 12
Width = 551
Height = 254
Caption = 'Panel1'
TabOrder = 1
end
end
 
Keith, the problem is this:

FLoadedForm.COntrols[0].Parent := Panel1;

You are placing the first control of the pluginform in Panel1, what about the other controls?

Solution 1:

for I := 0 to FloadedForm.ControlCount - 1 do
FLoadedForm.Controls[I].Parent := Panel1;

Solution 2:

In the pluginform, create a panel and place all the controls inside it, with this, you can use FLoadedForm.Controls[0], because there's only one control (which contains many sub controls.) to place in the host form.
 
>> ...
>> Solution 2:
>> ...

d'oh!

I thought I'd done that. I couldn't see the forest for the trees.

Thanks, works fine now.
 
Yet another solution to embed an entire form to a TPanel is using the CreateParented method:

Note, that some functionality won't work, for example alClient alignment, Tab order, etc. But you don't have to compile with runtime libraries.

procedure TContainerForm.BtnLoadClick(Sender: TObject);
begin
HLib:=LoadLibrary(SQLiteDllAppPath);
GetDllFormClass:=GetProcAddress(HLib, 'GetDllFormClass');
DllFormClass:=GetDllFormClass;
// DllForm:=DllFormClass.Create(nil);
// Application.CreateForm(DllFormClass,DllForm);
DllForm:=DllFormClass.CreateParented(Panel1.Handle);
DllForm.Visible:=false;
DllForm.BorderStyle:=bsNone;
// DllForm.Align:=alClient;
DllForm.BoundsRect:=Panel1.ClientRect;
DllForm.Caption:='';
DllForm.BorderIcons:=[];
DllForm.Visible:=true;
end;
 
Yes, indeed, before choosing the "Runtime Packages" approach I lost a lot of time trying to use standard dlls with the CreateParented method. The problem with this, as you noted, is that it loses some functionality, and I needed to create Plugins that feels exactly like if them where created in a monolithic application.
 
Hi, that is very useful article, thanks for your effort first. Now the question :)

I want to do same thing with VCL controls. not with Form. Can we do ?

for example i have 2 different dll files. and this 2 files own a different VCL control but this 2 control comes from TCustomPanel

I have tried your method for it. working is perfectly in runtime but I cannot see the VCL control on the screen why ? :(

I was set parent property of the controls already. the controls parent is a Tpanel on the main form.

Thank you in advance
 
The demo its no longer available
 
Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?