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:
And here's the form layout in the EXE:
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:
After clicking Button1, to show the form in the DLL within Panel1, it looks like this:
But when I tab through the controls, it skips the ones displayed from the form in the DLL:
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.
- 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.
- While the form from the DLL has the focus, the tab sequence of the controls on that form are in reverse order.
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?