1

Given a Dynamic-link Library containing a form and 2 exported procedures for showing/hiding the form, and a VCL executable with a form containing 2 buttons and a panel, how do I get the tab key to cycle through both the controls on the form in the exe, and the controls on the form in the dll, which is shown in the panel?

Here's the DPR of the DLL:

library Project18;

{ Important note about DLL memory management: ShareMem must be the
  first unit in your library's USES clause AND your project's (select
  Project-View Source) USES clause if your DLL exports any procedures or
  functions that pass strings as parameters or function results. This
  applies to all strings passed to and from your DLL--even those that
  are nested in records and classes. ShareMem is the interface unit to
  the BORLNDMM.DLL shared memory manager, which must be deployed along
  with your DLL. To avoid using BORLNDMM.DLL, pass string information
  using PChar or ShortString parameters. }

uses
  System.SysUtils,
  System.Classes,
  Winapi.Windows,
  Unit25 in 'Unit25.pas' {Form25};

{$R *.res}

procedure ShowForm(const AParentWindow: HWND); StdCall;
begin
  Form25 := TForm25.Create(nil);
  Form25.ParentWindow := AParentWindow;
  Form25.Show;
end;

procedure CloseForm; StdCall;
begin
  if Assigned(Form25) then
  begin
    Form25.Close;
    FreeAndNil(Form25);
  end;
end;

exports
  ShowForm,
  CloseForm;

begin
end.

And the DPR of the EXE:

program Project19;

uses
  Vcl.Forms,
  Unit26 in 'Unit26.pas' {Form26};

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm26, Form26);
  Application.Run;
end.

Here's the form layout in the DLL:

Unit25 TForm in Project18 DLL

And here's the form layout in the EXE:

enter image description here

Here's the code for the form inside the EXE:

unit Unit26;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls, Vcl.ComCtrls;

type
  TShowForm = procedure(AParentWindow: HWND); stdcall;
  TCloseForm = procedure; stdcall;

  TForm26 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Panel1: TPanel;
    Panel2: TPanel;
    ComboBox1: TComboBox;
    DateTimePicker1: TDateTimePicker;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    LibHandle: THandle;
    ShowForm: TShowForm;
    CloseForm: TCloseForm;
  public
    { Public declarations }
  end;

var
  Form26: TForm26;

implementation

{$R *.dfm}

procedure TForm26.Button1Click(Sender: TObject);
begin
  ShowForm(Panel1.Handle);
end;

procedure TForm26.Button2Click(Sender: TObject);
begin
  CloseForm;
end;

procedure TForm26.FormCreate(Sender: TObject);
begin
  LibHandle := LoadLibrary('Project18.dll');

  @ShowForm := GetProcAddress(LibHandle, 'ShowForm');
  @CloseForm := GetProcAddress(LibHandle, 'CloseForm');
end;

procedure TForm26.FormDestroy(Sender: TObject);
begin
  FreeLibrary(LibHandle);
end;

end.

The code for the form in the DLL is the standard autogenerated stuff.

When I first run the EXE, the form looks like this:

enter image description here

After clicking Button1, to show the form in the DLL within Panel1, it looks like this:

enter image description here

But when I tab through the controls, it skips the ones displayed from the form in the DLL:

enter image description here

And if I click into Edit1, for example, and press tab, it jumps to the next control on the main form in the exe, not the next control in the embedded form.

I looked at How to avoid issues when embedding a TForm in another TForm? and other answers both on Stack Overflow and the wider web, but I can't use a TFrame as the GUI I'm embedding is a TForm in a different DLL.

Do I need to capture the key press for the tabs and then send a Windows Message to the embedded form? To do that I think I would need the HWND of the embedded form, which I don't have.

EDIT: I changed the ShowForm to a function that returned the HWND of the form that is created, but it makes no difference, as even with KeyPreview set to true, the FormKeyPress and FormKeyUp events don't fire when I press tab.

EDIT: Have to add a CM_Dialog message handler to capture the tab key, as per https://community.embarcadero.com/article/technical-articles/149-tools/13071-detecting-tab-key-press

EDIT: But doing so stops the tab key from actually tabbing through the controls on the form in the EXE, so that's no solution at all, plus sending a tab to the embedded form using PostMessage did nothing either.

How do I get the control focus to include the controls from the embedded form when using the tab key?

EDIT: If I add a second form to the EXE, add two more buttons, and do the same steps for that form (with regard to showing and hiding), it has the same behaviour, even though that second form is in the EXE and not in the DLL.

Obviously just setting the forms ParentWindow isn't enough for it to be handled properly. It does work with the second form in the EXE if I set that forms Parent property to be the TPanel, rather than using ParentWindow. This seems to be pointing to having to pass a TWinControl to the DLL.

EDIT: Not sure this can be done. It can't, as noted in this question,

How to get instance of TForm from a Handle?

I looked at the CreateParented mentioned in this question,

How to instantiate a class which normally needs parent(TWinControl) in a dll?

and it still doesn't allow tab to flow through into the created control.

EDIT: A partial solution I have now found is to have an OnMessage event handler for the application in the EXE, which passes the TMsg to the form in the DLL using IsDialogMessage, by making these changes to the example program:

  public
    { Public declarations }
  protected
    procedure MessageHandler(var Msg: TMsg; var Handled: Boolean);
  end;

procedure TForm26.FormCreate(Sender: TObject);
begin
  LibHandle := LoadLibrary('Project18.dll');

  @ShowForm := GetProcAddress(LibHandle, 'ShowForm');
  @CloseForm := GetProcAddress(LibHandle, 'CloseForm');

  Application.OnMessage := MessageHandler;
end;

procedure TForm26.MessageHandler(var Msg: TMsg; var Handled: Boolean);
begin
  if (Msg.message = WM_KEYDOWN) then
  begin
    if IsDialogMessage(FormHandle, Msg) then
      Handled := True;
  end;
end;

Only two problems with this.

  1. The tab cycles through the controls of whichever form has the focus. It doesn't tab into the controls on the form from the DLL, or tab out of those controls to the ones on the form in the EXE.
  2. While the form from the DLL has the focus, the tab sequence of the controls on that form are in reverse order.

enter image description here

I can probably code it to make use of Winapi.Windows.SetFocus() to move the focus into the DLL form when it exits the last control of the EXE form, and similarly set the focus back to the EXE form when the last control is tabbed out of in the DLL.

Don't have an answer yet for handling of the reverse tab order. I know it's not the TabOrder properties of the controls. They are correct. Perhaps I need another WM_KEYDOWN message handler in the DLL?

SiBrit
  • 1,460
  • 12
  • 39
  • Use packages rather than DLLs. Or better still, use a single exe. – David Heffernan Apr 01 '21 at 06:29
  • @DavidHeffernan. Can't use packages or a single EXE. This is a legacy application and this is how it works. It loads functions and their GUI's from DLL's that support the plugin manager interface, and there are a lot of libraries written by third-parties for this application. – SiBrit Apr 05 '21 at 20:13
  • I would expect any solution to require in depth knowledge of the plugin framework. – David Heffernan Apr 06 '21 at 06:21
  • @DavidHeffernan. In-depth knowledge is not needed as I've reproduced the implementation of the exported function in my example from an existing Delphi DLL that uses the plugin manager interface and exhibits the same behaviour. – SiBrit Apr 06 '21 at 22:34
  • This is what I meant by in-depth knowledge. Although I don't think that there is enough to reproduce the scenario. So we are still lacking information. – David Heffernan Apr 07 '21 at 09:28
  • Finding out that I can use IsDialogMessage to pass the WM_KEYDOWN message to the DLL form isn't in-depth knowledge about the plugin manager interface. It's using Windows API from Delphi knowledge. Are you saying that I'm the only one who has that information? I find that hard to believe, although no-one else made the suggestion before I figured it out myself. It's still not a complete answer though. Why is the tab sequence in reverse? – SiBrit Apr 07 '21 at 20:08
  • It's really about what works as a question on this site. – David Heffernan Apr 07 '21 at 21:23
  • @DavidHeffernan. Agree. I'm just not sure how much simpler I can make it. I've reproduced the problem with the smallest example I can make, and now I'm just hoping someone with more Delphi Winapi knowledge will see it and post an answer. I'm loathe to start looking at things like GetNextDlgTabItem. – SiBrit Apr 08 '21 at 02:16

0 Answers0