Search code examples
lazarus

VCL/LCL – a form in DLL – no Application taskbar window, cannot minimize the main form


I have one problem, and I tried to search a solution but can't achieve what I want. Sorry if that is actually simple, please just point me to correct way of how to do it.

So! I have a C program that is a loader. It must call my DLL written in Delphi or Lazarus (Free Pascal). The DLL is actually a standalone GUI application: during debugging I conditionally compile it as EXE and it working.

My build script compiles it as DLL with one entry point that must execute it just as it works standalone. I expect exactly the same behavior, but I can do some things different (especially setting the Application icon) if needed.

Loader is a console-style program but compiled without a console – no windows, no anything. It just loads DLL and calls a function.

Problem is that when I build even empty default project with one form as an EXE – it will actually have "master" Application (.Handle <> 0) window in taskbar. So I can set its title independently from main form caption.

But when the same thing is inside a DLL – there is no Application window (.Handle = 0), the title will be the form caption, but the most important bug: a form cannot be minimized!

In Delphi 7 it goes background under other windows (but taskbar thing stays!); in Lazarus it just minimizes to nowhere (hided, no way to restore anymore); both without any minimizing animation.

Other than that, my application seems to behave normally. This is only issue I have.

OK, I know that forms in libraries is a bad thing to do, but:

  1. I’m fine to instantiate "another" VCL completely independent from host’s instance, maybe even in different thread.

  2. There is no VCL in my particular host application! For me, it must work exactly as it will in EXE alone…

I searched something about Application.Handle in DLL, and now understand than I need to pass a handle to host’s Application object, so DLL will be joined with others host forms, but I have none! It’s even not Delphi… (and Application:=TApplication.Create(nil); didn’t help either)

Anything of following will probably help me:

  • A) How to instruct VCL to create a normal Application object for me? How it does it when in EXE, maybe I can copy that code?

  • B) How to create a suitable master window from C (proper styles, etc.) to pass it’s handle to DLL? Also, I believe, in Free Pascal there is no direct access to TApplication handle value, so I couldn’t probably assign it.

  • C) How to live without a taskbar window, but have my form (good news: my program has only one form!) to minimize correctly (or just somehow…)?

I now you all love to see some code, so here it is:

// default empty project code, produces valid working EXE:
program Project1;

uses Forms, Unit1 in 'Unit1.pas' {Form1};
{$R *.res}

begin
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

+

// that's how I tried to put it in a DLL:
library Project1;

uses Forms, Unit1 in 'Unit1.pas' {Form1};
{$R *.res}

function entry(a, b, c, d: Integer): Integer; stdcall;
begin
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
  Result := 0;
end;

exports
  entry;

begin
end.

I specially crafted entry() function to be callable with rundll32, just for testing.

Also, I tried to put the body directly to "begin end." initialization section – same wrong behavior.

// To call a DLL, this can be used:
program Project1;

function entry(a, b, c, d: Integer): Integer; stdcall; external 'Project1.dll';

begin
  entry(0, 0, 0, 0);
end.

Also, CMD-command "rundll32 project1.dll entry" will run it instantly. (Yeah, that way I might get a handle that Rundll gives me, but it isn’t what I want anyway.)

Last notes: (a) the DLL must be compiled in Lazarus; actually first thing I thought that it is a bug in LCL, but now when tested in Delphi7 I see the same; and since Delphi case is more simpler and robust, I decided to put here that; (b) my C loader doesn’t call LoadLibrary, it uses TFakeDLL hack (that OBJ file was tweaked to work without Delphi wrapper) and loads my DLL from memory (so I don’t have a handle to DLL itself), but otherwise their behavior is the same.


Solution

  • Okay, thanks to @Sertac Akyuz, I tried with .ShowModal:

    // working Delphi solution:
    library Project1;
    
    uses Forms, Dialogs, SysUtils, Unit1 in 'Unit1.pas' {Form1};
    {$R *.res}
    
    function entry(a, b, c, d: Integer): Integer; stdcall;
    begin
      Result := 0;
      Application.Initialize;
      Form1 := TForm1.Create(nil);
      try
        Form1.ShowModal;
      except
        on e: Exception do
          ShowMessage(e.message);
      end;
      Form1.Free;
    end;
    
    exports
      entry;
    
    begin
    end.
    

    There is still no Application window (taskbar title equal to form caption), but now my form can be successfully minimized (with system animation). Note that for EXE compilation I have to go default way with Application, because when I tried to create the form like this – it started to minimize to desktop (iconify) instead of the taskbar.

    It works perfect in empty default Lazarus project too. But when I tried to implement it to my production program, it gave me "Disk Full" exception at .ShowModal!

    That thing was frustrating me little earlier (and that’s why I got rid of modality altogether, tried it no more), but now I was determined enough to get the bottom of this.

    And I found the problem! My build script doesn’t pass "-WG" ("Specify graphic type application") compiler option. Looks like something in LCL was using console-alike mode, and modality loop failed for some reason.

    Then, I had another very confusing issue that I want to share. My form’s OnCreate was rather big and complex (even starting other threads), and some internal function give me access violation when tried to do some stuff with one of controls on the form. It looked like the control is not constructed yet, or the form itself…

    Turns out that the actual call Form1:=TForm1.Create(nil); obviously will leave the global variable "Form1" unassigned during FormCreate event. The fix was simple: to add Form1:=Self; in the beginning of TForm1.FormCreate(Sender: TObject);

    Now everything is working without any problems. I can even use other forms with a normal Form2.Show(); if I firstly add them to my entry() function, like Form2:=TForm2.Create(Form1);

    (edit: minor note, if you would use Lazarus and try to run entry() function from any different thread than one that loaded DLL library itself – then you should put MainThreadID:=GetCurrentThreadId(); just above Application.Initialize;)

    Yay, this question is solved!