Link to home
Start Free TrialLog in
Avatar of Wim ten Brink
Wim ten BrinkFlag for Netherlands

asked on

I need a working sample...

Some might have noticed that I asked a question before about how to dock a form inside a DLL into my main application. ( https://www.experts-exchange.com/questions/20425092/Docking-forms.html ) I got an answer that satisfied me more than enough to continue experimenting. But I still experience some problems so I'm going to challenge the master brains here.

Provide me a working sample of the following:
* Main.exe contains a menubar, a listview and a panel. The menubar has the options File/Exit (Close the application) and Help/About (Does a ShowMessage). The Listview has three items: "Form1A", "Form1B" and "Form2". The panel is the location where the child forms will be docked.
* Library1.dll is a DLL that contains two forms. Form1A is a simple form with a nice image which is loaded during runtime. The filename for this image is 'Image1.bmp'. All this form has to do is show this image in a TImage control. Form1B is a more complex form. It will have a listbox, editbox, checkbox and two buttons. (OK & Cancel) If the checkbox is checked, both buttons need to be enabled. If the checkbox is unchecked, the OK button is disabled. If the OK button is pressed, the contents of the edit-field must be displayed with a ShowMessage command.
* Library2.dll is a DLL that shows a DBNavigator and a DBGrid and it needs to display one of the DBDEMOS tabled in a simple grid.

Sounds simple, doesn't it? Well, here's the hard part: I want the whole application to work even if no mouse is connected to the system. This means that we have shortcuts and that the TAB/SHIFT-TAB will walk through the controls. It also means that the TAB key must jump between the docked form on the panel and the listview that selects the forms.
It must all function without the mouse, no matter which control has focus!

There are more restrictions. The forms in the DLLs cannot be ActiveForms and the use of ShareMem is forbidden. And it must work with the VCL Forms, not any other form class.

If you have a working project, either ZIP it, put it on a website and post the link here. Or, post a huge message here containing the full source.

The first solution that works correctly will receive the points assigned to this question. I will reward those who were very close with up to 250 points and an A-grade if their solution has lead to the final solution.

Don't underestimate the toughness of this question. I'm trying to find a good solution at the same time so it will be a race to the clock too! If I find the solution before anyone else does... Well, I can't reward myself... ;-)

Btw... This question is also nice if other people are in a similar situation, where they want to dock forms from another DLL in their application. Nice learning project too!

And a final time-limit... I will need an answer for this problem before Januari 6, 2003. Then I will start a real project that will use this technique, if I have a working sample. Otherwise I will have to tell my customer that its not possible.
Avatar of Wim ten Brink
Wim ten Brink
Flag of Netherlands image

ASKER

Before I forget to mention it... It should mainly work for Windows 2000 or XP Pro. It is nice if it supports older Windows versions but that's not a requirement.
Also, it should compile correctly in either Delphi 5, Delphi 6 or Delphi 7, without any hints or warnings.
hi Workshop_Alex,

do you really need to have TPanel as control for docking? Can it be changed for something else?

When user click on ListView asscoaited item apears in docking place. Does it create and free docked forms every time or all forms just show/hide?

----
Igor
Avatar of geobul
geobul

Hi,
Would you show us how (and where) you are creating and docking these forms? This info is important for me in order to test the right code. Is it something like:

// dll
var
  DllForm1: TDllForm1;

function ShowDllForm1(hApp, hPanel: THandle): THandle; stdcall;
begin
  Application.Handle := hApp;
  if DllForm1 = nil then
    DllForm1 := TDllForm1.Create(Application);
  SetParent(DllForm1.Handle, hPanel);
  Result := DllForm1.Handle;
  DllForm1.Show;
end;

exports ShowDllForm1;

Regards, Geo
There is a way to move focus to the dll form (but not back unfortunately :-)
Set Panel1.TabStop to true and:

// App
var DllForm: THandle;
...
DllForm := ShowDllForm1(Application.Handle, Form1.Panel1.Handle); // show the form
...
procedure TForm1.Panel1Enter(Sender: TObject);
begin
  SetForegroundWindow(DllForm);
end;

Regards, Geo
Okay... Should it be a panel that I use to dock the forms on? No, it could be some other component. Maybe even some component that's a bit smarter while handling docked forms from DLLs.
The Forms in the DLLs don't have to be forms either. They can also be frames or something else that allows users to put controls on it in design-time. Frames might even offer an easier solution to the problem.

But what I want (and need) is a way to make the whole application appear to be similar to an application that would hold all forms in the same executable. Thus, this also means that the user can change focus by using the TAB key to jump from the EXE listview to the DLL controls and back again. (That's one of the hardest parts!)

The forms in the DLL can require a few moments to initialize (building up database connections, ect.) so the creation of the forms is done on demand. But once created they can stay until the executable is finished. But don't worry too much about how this mechanism will work.
In my final product I will let the executable search for all DLLs in a certain folder and then check if they have a function that exports a list of forms. If they do, the DLL is loaded and will stay loaded until the application is done. The Listbox will contain the filename (without extension and path) and the name of the forms from the list, seperated by a dot. (It can also be a treeview, with form names as subbranches of the filename branch, but then I also need to show some form when a filename node is selected. So I prefer a listbox for now.)

At this moment I'm trying all kinds of tricks to get it to work correctly. I've tried forms and frames. I've messed around with the application variable in the DLLs so they look at the application in the main executable. I've hooked into the messageloop to send messages from the executable to the DLL and even investigated which messages are send. I even got a nice situation where images on my form are visible, but the controls aren't...

The test project I'm working on is a bit different than what I described above. But I'll show some parts of it here.

First I have some type declarations:

type
  TGetForm = function: TForm; safecall;
  TGetFormClass = function: TFormClass; safecall;
  TProcessMessage = function(var Message: TMessage): Boolean; safecall;
  TShowForm = procedure(AForm: TForm);
  TRefresh = procedure;
  PDLLItem = ^TDLLItem;
  TDLLItem = record
    Name: string;
    Handle: THandle;
    ProcessMessage: TProcessMessage;
  end;
  PFormItem = ^TFormItem;
  TFormItem = record
    Name: string;
    DLL: Integer;
    FormName: PChar;
    FormClassName: PChar;
    GetForm: TGetForm;
    GetFormClass: TGetFormClass;
    Page: TForm;
  end;

I'm not using all of these at the moment but while testing these things grew on the project... ;-)

Next, to keep things simple, I declared two typed constants, one to hold a list of DLLs to import, the other telling me which forms it contains:


const
  DLLList: array[0..3] of TDLLItem = (
    (Name: 'Simple.frm'; Handle: 0; ProcessMessage: nil),
    (Name: 'Data.frm'; Handle: 0; ProcessMessage: nil),
    (Name: 'Icecream.frm'; Handle: 0; ProcessMessage: nil),
    (Name: ''; Handle: 0)
    );
  ProcList: array[0..4] of TFormItem = (
    (Name: 'Lara Croft'; DLL: 0; FormName: 'LaraForm'; FormClassName: 'LaraFormClass'; GetForm: nil; GetFormClass: nil; Page: nil),
    (Name: 'Simple dialog'; DLL: 0; FormName: 'HelloForm'; FormClassName: 'HelloFormClass'; GetForm: nil; GetFormClass: nil; Page: nil),
    (Name: 'Database access'; DLL: 1; FormName: 'DataForm'; FormClassName: 'DataFormClass'; GetForm: nil; GetFormClass: nil; Page: nil),
    (Name: 'Dummy in EXE'; DLL: 3; FormName: 'DummyForm'; FormClassName: 'DummyFormClass'; GetForm: nil; GetFormClass: nil; Page: nil),
    (Name: 'Icecream'; DLL: 2; FormName: 'Icecream'; FormClassName: 'IcecreamClass'; GetForm: nil; GetFormClass: nil; Page: nil)
    );

The LaraForm shows an image of Lara Croft. HelloForm has some controls and a button saying 'Hello, World.'. DataForm has a form containing a DBGrid that's connected through an ADO connection to an SQL Server database, displaying some imple data. DummyForm is located in the executable itself, not the DLL. Yes, that's possible too. ;-) Icecream is another form containing an image of an icecream. And if I click on that image, a screenshot is made and saved to file, just as some playtest.

I also have to load and unload stuff. This is done on the mainform create and destroy events:

procedure TFormConfig.FormCreate(Sender: TObject);
const
  sFound: array[False..True] of string = (' not', '');
var
  I: Integer;
  Handle: THandle;
begin
  Font.Assign(Screen.MenuFont);
  Left := Screen.DesktopLeft;
  Top := Screen.DesktopTop;
  Width := Screen.DesktopWidth;
  Height := Screen.DesktopHeight;
  for I := Low(DLLList) to High(DLLList) do begin
    if (DLLList[I].Name = '') then begin
      WriteLn(Log, 'Local form.');
      DLLList[I].Handle := GetModuleHandle(nil);
    end
    else begin
      WriteLn(Log, 'Remote form in ', DLLList[I].Name, '.');
      DLLList[I].Handle := LoadLibrary(PChar(ExtractFilePath(ParamStr(0)) + DLLList[I].Name));
    end;
    if (DLLList[I].Handle = 0) then begin
      WriteLn(Log, 'Invalid module.');
    end
    else begin
      DLLList[I].ProcessMessage := TProcessMessage(GetProcAddress(DLLList[I].Handle, 'ProcessMessage'));
    end;
  end;
  for I := Low(ProcList) to High(ProcList) do begin
    ListBox.Items.AddObject(Proclist[I].Name, Pointer(I));
    Handle := DLLList[Proclist[I].DLL].Handle;
    if (Handle <> 0) then begin
      Proclist[I].GetForm := TGetForm(GetProcAddress(Handle, Proclist[I].FormName));
      WriteLn(Log, 'Procedure ', Proclist[I].FormName, sFound[assigned(Proclist[I].GetForm)], ' loaded.');
      Proclist[I].GetFormClass := TGetFormClass(GetProcAddress(Handle, Proclist[I].FormClassName));
      WriteLn(Log, 'Procedure ', Proclist[I].FormClassName, sFound[assigned(Proclist[I].GetFormClass)], ' loaded.');
    end
    else begin
      WriteLn(Log, 'Module containing procedure ', Proclist[I].FormName, ' invalid.');
    end;
  end;
  WriteLn(Log, StringOfChar('=', 80));
end;

Well, it contains a simple trick to get a nice font and to set up the dimensions of the form. Then it starts opening the DLLs and assigning the methods that will create the forms. Log is a special log textfile that I once created for debugging purposes. It is created and destroyed in the initialization/finalization section of another unit and is very thread-safe. Maybe I'll share the code of it one day. :-)

procedure TFormConfig.FormClose(Sender: TObject; var Action: TCloseAction);
var
  I: Integer;
begin
  for I := Low(ProcList) to High(ProcList) do begin
    with ProcList[I] do begin
      if assigned(Page) then begin
        WriteLn(Log, 'Closing active page ', Page.ClassName, '[', Page.Name, '].');
        Page.Free;
        Page := nil;
      end;
    end;
  end;
  WriteLn(Log, StringOfChar('.', 80));
  for I := Low(DLLList) to High(DLLList) do begin
    if (DLLList[I].Handle <> 0) then begin
      WriteLn(Log, 'Free ', DLLList[I].Name);
      Freelibrary(DLLList[I].Handle);
    end;
  end;
  WriteLn(Log, StringOfChar('=', 80));
end;

This will free all pages that I have opened and then unload every DLL.
I also have the following procedure to open a form:

procedure TFormConfig.OpenPage(Index: Integer);
begin
  if Assigned(ActivePage) and assigned(ActivePage.Page) then begin
    ActivePage.Page.ManualDock(nil);
    ShowWindow(ActivePage.Page.Handle, SW_HIDE);
  end;
  ActivePage := @ProcList[Index];
  if Assigned(ActivePage) then begin
    Caption := ActivePage.Name;
    if assigned(ActivePage.Page) then begin
      WriteLn(Log, 'Show page ', ActivePage.FormName);
    end
    else if assigned(ActivePage.GetForm) then begin
      WriteLn(Log, 'New page ', ActivePage.FormName);
      ActivePage.Page := ActivePage.GetFormClass.Create(Panel);
    end;
    ActivePage.Page.ManualDock(Panel, nil, alClient);
    ShowWindow(ActivePage.Page.Handle, SW_SHOW);
  end;
end;

And I have this global variable:

var
  ActivePage: PFormItem = nil;

ActivePage will point to the page that is currently active. If it's nil, no page is active.

But no... This doesn't give me the result I want.
The forms in the DLLs actually have two methods to be created. I can either use the GetForm function and then the DLL will create the form. Or I use GetFormClass to get the class of the form and create the form in the executable. Both options are possible and I don't have any preferences.

I also created one base form that will hold some intelligence and all other DLL forms are inherited from this form. Makes it a bit easier to respond on certain events in a global way. This base form also contains a mainmenu that is merged with the mainmenu of the executable. (A standard &Edit menu.)

I hope that by next week I will be able to put a ZIP file somewhere on my server for others to see what I've been able to come up with so far. The most important thing, however, is that the application should work as if the forms are part of the executable themselves.
-----------------------------------------------------------
Now, you might wonder why I need to take all this trouble... Well, it's quite simple. I will be generating a very modular product for several different users. But these users each have their own specialized configuration screen. Tables can be different, screen layout and logo's can be different. Some users might even have more options than others. And my main problem is that I can't use the registry on the client system and the final version isn't allowed to write to the local machine either. Thus, all settings need to be hard-coded somewhere or retrieved through some hard-coded connection. The application will have very limited access rights. Just enough to connect to a database and exchange data but not more.
Without using files on the local system or accessing the registry, this is quite a difficult task.

ActiveForms would require registrations in the registry, which I'm trying to avoid. Using runtime packages could cause some version conflicts since this application won't be the only Delphi application that the user will get. Besides, I think they are unreliable and way too big to distribute. Including the forms in the executable means that I will have to use several compiler directives to generate a different version for every customer, making testing of all these different executables quite difficult. But if I can get the main executable to work correctly, then get the DLLs to work correctly, testing will be quite easy and this project itself will be a piece of cake.
But to make it easy, I need to get the DLL forms to behave like they're part of the executable itself, as if they're just part of the executable. At this moment I have two weeks to find a solution for this before a decision will be made about creating a multi-DLL version or a collossal single-exe version.
It sounds simple, doesn't it? Add a form to a DLL and dock it in the main executable and make it appear to be part of the executable. Yet, if it was this simple, I probably would have found the answer by myself already.
I know that many people have wondered about this problem too and have given up on this. Well, I don't like to give up on this... It is time we find a good answer for this problem that exists for many years now... :-)
and one more question.

Did you noticed that if you build project and DLL with runtime packages then all works as expected? May be it is better then to invent a lot of code?

Here is sample:


----- PROJECT -------
type
  TDLLFormClassProc = function: TFormClass;

...

procedure TForm1.SpeedButton1Click(Sender: TObject);
var
  H: Integer;
  P: TDLLFormClassProc;
  F: TForm;
begin
  H := LoadLibrary('p_dll1');
  if H <> 0 then
  begin
    P := GetProcAddress(H, 'NewClassForm1');
    if @P <> nil then
    begin
      F := P.Create(nil);
      F.Show;
      F.ManualDock(Panel1, nil, alClient);
    end;
  end;
end;

------- DLL -------
uses
  SysUtils,
  Classes,
  Forms,
  dll_from1 in 'dll_from1.pas' {DLLForm1};

{$R *.res}

function NewClassForm1: TFormClass;
begin
  Result := TDLLForm1;
end;


exports
  NewClassForm1;
end.
>> runtime packages... quite easy and this project itself will be a piece of cake.

not very sure about your opinion about it, it will be very big one. Ok, it was just an idea to avoid of writing lot of code.

Still thinking about how to make some trick.

----
Igor.
Runtime packages could be used but neither I or the customer would like this. The problem with runtime packages is that it adds a lot of additional files to the project containing a lot of stuff that's not used by the project. If other Delphi projects that use runtime packages there's also a risk of version conflicts. (And one of the users has other Delphi projects with runtime pachakes on his system.)
I know the runtime packages aren't that big but there's always a risk that it just can't find one of the packages it need or that one package has been altered because another Delphi project has been added. I don't want to add any files to the Windows System folder either because that would actually require me to create an installation/uninstall tool, which needs to store data locally. This project should just be a single executable and a couple of DLLs that can be copied to any location and the DLLs determine the functionality the executable has. Basically, the binaries are the only things the user gets and nothing more. No changes to the registry settings, no files containing any configuration settings, just 1 exe and a couple of DLLs.
One other hint... (Geez, Am I solving my own problem? :-)

I added an TApplicationEvents to the mainform of the executable and assigned the following OnMessage event:

procedure TFormConfig.ApplicationEventsMessage(var Msg: tagMSG; var Handled: Boolean);
begin
  if Assigned(ActivePage) and Assigned(ActivePage.Page) then begin
    if (Msg.hwnd = Handle) then begin
      WriteLn(Log, 'MainForm handled ', Msg.hwnd);
    end
    else if IsDialogMessage(ActivePage.Page.Handle, Msg) then begin
      WriteLn(Log, ActivePage.Name, ' handled ', Msg.hwnd);
      Handled := True;
    end;
  end;
end;

This code seems to be working quite nice already. But I can't get the focus back to the listbox again... Furthermore, the TAB key seems to walk through the controls on the form in the reverse order for some strange reason. But the solution is getting a bit closer. :-)
ASKER CERTIFIED SOLUTION
Avatar of geobul
geobul

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Hi,
Have you tried my proposal? Does it work as you expected?

Regards, Geo
Sorry, small accident prevented me to access my computer for a while. Oh, well... It also delayed my project for a month so I have more time now to think for a good solution.

The use of SetForegroundWindow has been tried before by me but not with much succes. Also played around with SetParent and a few other Windows APIs but not much result.

I checked your solution and it appears to be on the right track. Too bad it uses a global variable in the DLL to keep the value of the form but I think I can work around it.

Another problem is still hiding and showing the form again when another form is chosen and then the focus moves back to this form. The form cannot be destroyed in-between because the final forms will also maintain database connections and possible changes that still have to be written to the database.
>Too bad it uses a global variable in the DLL ...

Yes, it could be avoided.

>Another problem is still hiding and showing the form again ...

Just an idea...(not tested)

function HideDllForm1: integer; stdcall;
begin
  result := 1; // it doesn't matter
  if DllForm1 <> nil then DllForm1.Hide;
end;
 
exports ShowDllForm1, HideDllForm1;

and usage:
ShowDllForm1(..); // first form
...
HideDllForm1; // second form
ShowDllForm2(..);
...
HideDllForm2;
ShowDllForm1(..); // show first form again

Regards, Geo
Well, I am close to a working example right now, thanks to you, Geo. Yet the solution still isn't good enough. I will put the D7 source I have now on the web, http://www.truth4all.org/FormDLL.zip so maybe someone can have a look at it. I don't think it's very clear yet since it lacks a lot of comments and at least one of the forms won't work since you don't have access to the database used in it. ;-) But since it's just showing the contents of a table, it can easily be replaced by another table.
(Btw. If you delete the file 'debug.dll' the application will stop writing debug information!)

The problems I have so far...
- The taborder on the DLL forms is all wrong. It's in complete reverse order...
- When the DLL form has focus, the application disappears from the task bar and you can't reach it by using ALT+TAB either.
- When the DLL form has focus, the application caption changes color from the focus-color to the unfocussed color.

But it does tab nicely between the main application and the DLL forms. The solution is nearly perfect! :-) Just having to solve these minor annotyances but I know the customer will complain about this. The tab-order isn't a real problem but the loss of focus is.
Well, I am close to a working example right now, thanks to you, Geo. Yet the solution still isn't good enough. I will put the D7 source I have now on the web, http://www.truth4all.org/FormDLL.zip so maybe someone can have a look at it. I don't think it's very clear yet since it lacks a lot of comments and at least one of the forms won't work since you don't have access to the database used in it. ;-) But since it's just showing the contents of a table, it can easily be replaced by another table.
(Btw. If you delete the file 'debug.dll' the application will stop writing debug information!)

The problems I have so far...
- The taborder on the DLL forms is all wrong. It's in complete reverse order...
- When the DLL form has focus, the application disappears from the task bar and you can't reach it by using ALT+TAB either.
- When the DLL form has focus, the application caption changes color from the focus-color to the unfocussed color.

But it does tab nicely between the main application and the DLL forms. The solution is nearly perfect! :-) Just having to solve these minor annotyances but I know the customer will complain about this. The tab-order isn't a real problem but the loss of focus is.
Well, Geo. You've helped me more than enough so you deserve the points. I still have some minor issues that need to be solved but I'll create a new question for that so you have a chance to earn even a bit more points... ;-)

Check out http://dll-form.truth4all.org/ if you want to see the test application that I have so far. I will link to this page for my new question too.
MAAAAAAAANY thanks, Alex :-)))

1.About reversed tab order: Perhaps TApplicationEvents is the problem (see your own comment 12/20/2002 06:28AM PST). There is no such thing in my project (I don't use TApplicationEvents).

2.My project disapears from ALT-TAB too (????) but I still have a button on the taskbar (D5, WinXP). I'll have to find out the reason.

3. Yes, main forms caption becomes inactive when a DLL form has the focus. Isn't it normal?

Regards, Geo
Hi,

Did somebody found a solution to this problem? Ie reversed tab, etc?

Thanks
DLL contains its own VCL inside. Its not possible to integrate them like they are from the same application. You can do this using BPL's. Bcouse of this reason samples work correctly when runtime packages are used...