Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

About one solution of calling Delphi forms placed into DLL from non-Delphi applications

1,544 views
Skip to first unread message

Alexander Yeryomin

unread,
May 18, 2001, 3:33:58 AM5/18/01
to
Hi All!

Many developers have problems with using forms that created in Delphi
and placed into DLL. Of course, if an application using DLL is made in
Delphi as well as DLL there is no problem. And what is more, it is
possible and ever better to use packages instead ⌠native■ DLL, because
packages are only special DLLs used by Delphi.

However, how can we use Delphi▓s DLLs from foreign applications, i.e.
applications that built with the help of other development systems such
Microsoft VS or Excelsior XDS M2? You can simply create some form from
DLL within your ⌠main■ program but it doesn▓t work correctly, for
example, switching between form▓s controls don▓t work by TAB;
accelerators don▓t work, and so on.

I bumped myself into this problem recently. Of course, I really know how
to manage DLL (in general, shared library) under a great number of
various OS, and I wondered when I couldn▓t easy convert my Delphi
application into ⌠usable■ DLL.

I have found some solution, which I want to describe here. The fact is
that not using DLL by itself raises the problems but because Delphi VCL
creates forms and processes messages in special manner.

Again, what I want to get? I want to create and open forms from my
⌠main■ program under following conditions:
1) Relative buttons for each form should not appear in the Windows Tasks
Bar. Only one button for ⌠main■ program may appear in the Tasks Bar.
2) Forms should be independent each other
3) Messages processing loop for ⌠main■ program
(GetMessage->TranslateMessage ->DispathMessage) should not receive any
messages from forms.
4) Any form may be requested to open at any time by several concurrent
requesters.
5) When form is opened it should stay active form for whole application,
i.e. it should get a focus, stays topmost, and receives events from the
user.

I had other some conditions but they are unimportant for us now.

From these conditions, it is necessary to set some owner for each form.
Notice an owner is not a parent because if Form.Parent is specified the
form will be shown within parent▓s window area. But if we get a window
of ⌠main■ program as owner for all Delphi forms, this ⌠main■ window
never brings up over its owned forms because owned windows (forms) are
always displayed on top of its OWNER window!

It is needed to create fake window that will be invisible for the user,
it hasn▓t the button in the Tasks Bar, and it will be owner for all
Delphi forms instead of the window of ⌠main■ program. This fake window
is only intended to be an anchor for forms.

Also we need to execute a separate thread for all forms to protect
⌠main■ application from messages for Delphi forms. The thread will have
its own messages processing loop. All forms should be created within
this thread because all messages for window created in some thread will
be received by this thread. Functions GetMessage(), PeekMessage(),
WaitMessage() are thread-dependent, i.e. they get messages only for the
thread within which they are called.

The point is when and where to create a form and its owned visual
components (buttons, editors, ...). Requests to create and to open form
are sent from ⌠main■ program. It means that they are sent from another
thread (⌠main■ thread). How is it possible to notify another thread
about it? The good way is sending user-defined messages with parameters
to another thread. Though SendMessage is more convenient because it
waits for result of the call, we have to use PostMessage. It is very
important to use PostMessage instead of SendMessage because it
guarantees that messages will be processed by WinProc() exactly within
context of recipient-thread. We can create a special event that allows
us to wait results of PostMessage. Indeed, if we create a form then open
it, we must make sure that the form has time to create itself before
opening. Don▓t forget, requests to create and to open form are sent from
⌠main■ thread!


The fragment of code from unit VCL\controls.pas allows us to see that
Windows object (having handle) for some visual control is created only
when the control is about to show not it has being created (see a line
marked by me).

procedure TWinControl.UpdateShowing;
var ShowControl: Boolean;
I: Integer;
begin
ShowControl := (FVisible or (csDesigning in ComponentState)
and not (csNoDesignVisible in ControlStyle))
and not (csReadingState in ControlState);
if ShowControl then
begin
if FHandle = 0 then CreateHandle; <------- !!!!!
if FWinControls <> nil then
. . . . .
---8<------8<------8<------8<------8<---
end;

The VCL control may be created but not of all its fields may be
initialized at the same time. It is so called ⌠lazy■ or postponed
initialization. Field Fhandle will be initialized once when the control
stays visible at the first time. It is correctly for any successor of
TWinControl. We are sure that all visual fields are initialized after
call of Show() or ShowModal() only.

Therefore, we need not only to create forms in our auxiliary thread but
also to show forms in this useful thread.

(By the way, you must create and execute all VCL forms in the same
thread. Oh, you say me about method Synchronize()? Forget it! For
example, any popup menus will never bring up. Why? I will tell you about
it later.)


const
// NOTE: All Message Numbers below 0x0400 are RESERVED.
// Private Window Messages Start from WM_USER.
WM_USER = $400;

// Used to notify the Launcher to init the Application
WM_INIT_APP = WM_USER+05474;
// Parameters:
// lParam: Boolean, use own hWnd to initialize the Application
// wParam: not used

// Used to notify the Launcher to create a form
WM_CREATE_FORM = WM_USER+05475;
// Parameters:
// lParam: ^TForm, pointer to a form variable that should be created
// wParam: TFormClass, a form class

// Used to notify the Launcher to show a form
WM_SHOW_FORM = WM_USER+05476;
// Parameters:
// wParam: TForm, a form variable
// lParam: Boolean, show or hide a form;


var
WaitingEvent: TEvent; // the event is used to wait a readiness of...

// Waits a readiness
procedure Wait ();
begin
WaitingEvent.WaitFor (INFINITE);
WaitingEvent.ResetEvent ();
end;

// Signals about a readiness
procedure Ready ();
begin
WaitingEvent.SetEvent ();
end;

var
// hWnd of the launcher window
hLauncherWnd: HWND;


// WindowProc for the launcher window
function LauncherWndProc (hWnd: HWND; message: UINT;
wParam: WPARAM; lParam: LPARAM
): LRESULT; StdCall;
type
PForm = ^TForm;

begin
case message of
// Initilaze the Application
WM_INIT_APP:
begin
Application.Initialize;
// if Application.Handle has hWnd the application hasn't its own

// button in Windows Tasks Bar. You can leave it nil instead
hWnd.
if Boolean (lParam) then
Application.Handle := hWnd
else
Application.Handle := 0;
Result := 0;
end;
// Creates a form by form class
WM_CREATE_FORM:
begin
TForm(PForm(lParam)^) := TFormClass(wParam).Create
(Application);
ShowWindow (hWnd, SW_HIDE);
Result := 0;
Ready (); // a form is successfully created
end;
// Makes a form visible or not depending on its parameter
WM_SHOW_FORM:
begin
TForm(wParam).Visible := Boolean(lParam);
ShowWindow (hWnd, SW_HIDE);
Result := 0;
Ready (); // a form is successfully swown
end;
else
Result := DefWindowProc (hWnd, message, wParam, lParam);
END;
END;


We need to pass parameters to this window procedure. Functions doing
this will be called only within Delphi part of DLL; they will be not
exported from DLL because it is necessary to call these functions with
some actual parameters. Because of that we don▓t use directive StdCall.

However, pay your attention to the fact that these functions will work
within ⌠main■ thread! They must be called from ⌠main■ thread indirectly
for creating or opening forms. Of course, creating and opening will be
performed within the auxiliary thread but request to create or show form
will be initialized from ⌠main■ thread!

// Creates a form by form class
function CreateForm (const FormClass: TFormClass): TForm;
var
Form: TForm;
begin
PostMessage (hLauncherWnd, WM_CREATE_FORM, DWORD(FormClass),
DWORD(@Form));
Wait ();
Result := Form;
end;


// Makes a form visible or not depending on its parameter
procedure FormVisible (const Form: TForm; Visible: Boolean);
begin
PostMessage (hLauncherWnd, WM_SHOW_FORM, DWORD(Form), DWORD(Visible));

if Visible then
begin
Wait ();
// check if the form is minimazed and restore it
if Form.WindowState = wsMinimized then
Form.WindowState := wsNormal;
Form.Show();
end;
end;


It is interesting that in FormVisible() we▓re not waiting until a form
is hiding. In contrast of that, in the case when a form should be opened
we▓re waiting while the form is showing in another thread, then if it is
needed restore the form (by default, Delphi doesn▓t it by itself if a
form is minimized), then call method Show() again. For what do I call it
again? Because the form has focus in the auxiliary thread, and we need
to do the form active in the ⌠main■ thread. Repeated Form.Show() makes
the form active in the ⌠main■ thread.

At last, we have to create the invisible ⌠anchor■ window and start it in
the separate thread.

const
Init_Ok = 0; // the initialization is successful
Init_Error_RegisterClassEx = -1; // the initialization is wrong,
RegisterClassEx fails
Init_Error_CreateWindow = -2; // the initialization is wrong,
CreateWindow fails

type
TDelphiThread = class (TThread)
public
procedure Execute (); override;
end;

procedure TDelphiThread.Execute ();
CONST
LauncherWindowClassName = 'Excelsior_LauncherWindow';
var
wc: WNDCLASSEX;
begin
FreeOnTerminate := true;

if not GetClassInfoEx (GetModuleHandle(NIL), LauncherWindowClassName,
wc) then
begin
wc.style := 0;
wc.lpfnWndProc := @LauncherWndProc;
wc.cbClsExtra := 0;
wc.cbWndExtra := 0;
wc.hInstance := GetModuleHandle(NIL);
wc.hIcon := 0;
wc.hCursor := 0;
wc.hbrBackground := 0;
wc.lpszMenuName := nil;
wc.lpszClassName := LauncherWindowClassName;
wc.cbSize := SIZEOF (WNDCLASSEX);
wc.hIconSm := 0;

IF RegisterClassEx (wc) = 0 THEN
begin
ReturnValue := Init_Error_RegisterClassEx;
Ready (); // error
exit;
end;
end;

hLauncherWnd := CreateWindow ( LauncherWindowClassName //
address of registered class name
, '' // no name // address
of window name
, 0 // window
style
, 4096 // out of screen bounds //
horizontal position of window
, 4096 // out of screen bounds // vertical
position of window
, 1 // window
width
, 1 // window
height
, 0 // hasn't parent // handle of
parent or owner window
, 0 // hasn't menu // handle of
menu or child-window identifier
, GetModuleHandle(NIL) // handle of
application instance
, nil // hasn't data // address
of window-creation data
);

IF hLauncherWnd = 0 THEN
begin
ReturnValue := Init_Error_CreateWindow;
Ready (); // error
exit;
end;

// hide the window
ShowWindow (hLauncherWnd, SW_HIDE);
// init the Application
PostMessage (hLauncherWnd, WM_INIT_APP, 0, Integer(true));
ReturnValue := Init_Ok;

Ready (); // success

// messages-processing loop for Delphi forms
repeat
Application.HandleMessage();
until Application.Terminated;
end;


After the ⌠anchor■ window has been created we immediately post
WM_INIT_APP message to it to initialize the Application object.
Parameter of WM_INIT_APP message specifies that forms have separate
relative button in the Tasks Bar or not.


We call method Form.Create() directly in WndProc

TForm(PForm(lParam)^) := TFormClass(wParam).Create (Application);

instead indirectly call it through the Application

Application.CreateForm (TFormClass(wParam), TForm(PForm(lParam)^);

the Application HASN▓T main form because of that we cannot use
Application.Run() (this method checks presence of the main form before
starting messages processing loop). We simply write loop with the help
of Application.HandleMessage(). It is identical to Run() because all
messages are processed by Application.ProcessMessage() at any cases.


The last action is initialization of the thread.

var
IsInitialized: Boolean;

// Initializes the launcher.
// This procedure must be called first before using the launcher.
function Init (): Integer; StdCall;
var
DelphiThread: TDelphiThread;
begin
if IsInitialized then
Result := Init_Ok
else
begin
IsMultiThread := true;
WaitingEvent := TEvent.Create (NIL, TRUE, FALSE,
'__Excelsior_Waiting_Event__');
DelphiThread := TDelphiThread.Create ({CreateSuspended=}false);
Wait ();
Result := DelphiThread.ReturnValue;
IsInitialized := Result = Init_Ok;
end;
end;

begin // module initialization
IsInitialized := false;
end.


This function is marked by StdCall directive. It allows (and what we
must do) to export this function from DLL then call it from ⌠main■
program.

I hope I described all things enough clear. Maybe you will need to add
procedure Exit() as pair to Init(). The Exit() destroys all created
forms when the ⌠main■ application is about to close.

As I said above, functions CreateForm() и FormVisible() have not StdCall
directive. Instead of the functions you have to export special wrappers
that create forms you need with actual type. Wrappers must be marked
with StdCall directive, they are exported from DLL. For example:


unit ASatelliteGeometryParameters;
// wrapper for SatelliteGeometryParameters unit

interface

uses
Forms,
// here actual form is designed TfrmSatelliteGeometryParameters
SatelliteGeometryParameters;


// this fuction must be exported from DLL
function SGP_Init (): Pointer; StdCall;

// this fuction must be also exported from DLL
procedure SGP_Visible (AVisible: Boolean); StdCall;

// some other functions
// * * *

implementation

uses
Launcher; // this unit incapsulates all thread things

function SGP_Init (): Pointer; StdCall;
begin
if frmSatelliteGeometryParameters = nil then
frmSatelliteGeometryParameters := TfrmSatelliteGeometryParameters
(CreateForm (TfrmSatelliteGeometryParameters));
Result := frmSatelliteGeometryParameters;
end;


procedure SGP_Visible (AVisible: Boolean); StdCall;
begin
FormVisible (frmSatelliteGeometryParameters, AVisible);
end;

// * * *

end;

library xLib; // your DLL

uses
Launcher,
ASatelliteGeometryParameters;

exports
Init,
SGP_Init,
SGP_Visible;

begin
end;

For what I use unique prefix for wrappers function? Because there are
many forms in the project. Each form must have wrappers for methods
Show(), Visible(), and so on. And wrappers will have equal names. Of
course, it is possible to rename wrappers in the exports clause

exports
Launcher.Init name ▒Launcher_Init▓
AsatelliteGeometryParameters.Init name ▒SGP_Init▓,
AsatelliteGeometryParameters.Visible name ▒SGP_Visible▓;

but it is the same.

Well, you can create an import library form your DLL, then import it to
the your ⌠main■ program. If development system, which you use, doesn▓t
allow making an import library, you can load your DLL by your hands with
the help of LoadLibrary function. Enjoy!

Best regards,

Alex

Alexander V. Yeryomin
-----------------------------------------
Project Manager, Senior System Architect
Debugging and Analysis Tools Dept.
Excelsior (http://www.Excelsior-USA.com/)
Tools for your success! (R)


Peter Below (TeamB)

unread,
May 19, 2001, 9:55:55 AM5/19/01
to
In article <3B04D066...@Excelsior-USA.com>, Alexander Yeryomin wrote:
> Many developers have problems with using forms that created in Delphi
> and placed into DLL. Of course, if an application using DLL is made in
> Delphi as well as DLL there is no problem. And what is more, it is
> possible and ever better to use packages instead “native” DLL, because
> packages are only special DLLs used by Delphi.
>
> However, how can we use Delphi’s DLLs from foreign applications, i.e.
> applications that built with the help of other development systems such
> Microsoft VS or Excelsior XDS M2? You can simply create some form from
> DLL within your “main” program but it doesn’t work correctly, for
> example, switching between form’s controls don’t work by TAB;
> accelerators don’t work, and so on.

This is a problem only if you show the forms modeless. A modal form created
using a scheme like that shown below will work since the modal message loop
it uses does all the Delphi-specific processing.

Procedure ShowDLLForm( appHandle: HWND ); stdcall;
Begin
If appHandle = 0 Then
apphandle := GetActiveWindow;
Application.handle := appHandle;
try
With TDLLForm.Create( Application ) Do
try
ShowModal
finally
Free;
end
except
On E: Exception Do
Application.HandleException( E );
end;
Application.handle := 0;
End;

I use stdcall here so the DLL form can be called even from a non-Delphi
host application without problems. It is generally a good idea to not let
exceptions escape the DLL, that makes it possible to use the DLL with
non-Delphi apps safely.

Using modeless forms in a DLL with non-Delphi host apps is problematic due
to the additional message processing the VCL message loop does and which
the host apps loop of course does not do. And due to that fact that
handling a modeless window properly requires the message loop code to
*know* about this window, so it can call IsDialogMessage for this window in
the message loop. This is a Windows design issue, really, for which you
have to blame MS <g>. Tracking several modeless windows (non-MDI) in an
API-style Windows app used to be a royal pain in the behind. The VCL
designers avoided this problem by replicating the functionality of
IsDialogMessage (and accelerator handing) in the VCL, using internal
messages like CM_DIALOGKEY. Which are unfortunately dependent on the
special message processing done by the VCL message loop. As are other
things like hint handling and action update events.

Peter Below (TeamB) 10011...@compuserve.com)
No e-mail responses, please, unless explicitly requested!
Note: I'm unable to visit the newsgroups every day at the moment,
so be patient if you don't get a reply immediately.

Alexander Yeryomin

unread,
May 23, 2001, 5:28:34 AM5/23/01
to
"Peter Below (TeamB)" wrote:

> This is a problem only if you show the forms modeless. A modal form created
> using a scheme like that shown below will work since the modal message loop
> it uses does all the Delphi-specific processing.

Dear Peter , you're right,. Of course, I know that this code
works fine. But the problems are exactly using modeless forms.
As I wrote, it is possible to separate messages from forms
and windows of main application. Thanks for your reminder
to catch an exception within DLL, it's really good style.
By the way, using my solution guarantees that any exceptions
will be caught within Delphi Application's messages loop,
at the least, by default exception handler, and no exceptions
will be thrown outside of DLL.

> Tracking several modeless windows (non-MDI) in an API-style Windows app
> used to be a royal pain in the behind. The VCL designers avoided this problem
> by replicating the functionality of IsDialogMessage (and accelerator handing)

Thank VCL designers for accepting this headache by themselves.

> This is a Windows design issue, really, for which you have to blame MS.

Please don't blame MS, they wished to do things good but they have
been doing as usual bad. How could they really do anything otherwise?
:-)

0 new messages