如何在运行时嵌入到 EXE 中另一个 TForm 的 TPanel 中的 DLL 中的 TForm 上的控件中切换到 TForm 上的控件,并获得正确的 Tab 顺序?

问题描述

给定一个包含表单和 2 个用于显示/隐藏表单的导出程序的动态链接库,以及一个包含 2 个按钮和一个面板的表单的 VCL 可执行文件,我如何让 Tab 键在两个控件之间循环在exe中的窗体上,在dll中的窗体上的控件,显示在面板中?

这是 DLL 的 DPR:

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.

以及 EXE 的 DPR:

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.

这是 DLL 中的表单布局:

Unit25 TForm in Project18 DLL

这是 EXE 中的表单布局:

enter image description here

这是EXE中表单的代码:

unit Unit26;

interface

uses
  Winapi.Windows,Winapi.Messages,System.SysUtils,System.Variants,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.

DLL 中表单的代码是标准的自动生成的东西。

当我第一次运行 EXE 时,表单如下所示:

enter image description here

点击Button1后,在Panel1内的DLL中显示窗体,如下所示:

enter image description here

但是当我通过 Tab 键浏览控件时,它会跳过 DLL 中表单中显示的那些:

enter image description here

例如,如果我单击 Edit1 并按 Tab,它会跳转到 exe 主窗体上的下一个控件,而不是嵌入窗体中的下一个控件。

我在 Stack Overflow 和更广泛的网络上查看了 How to avoid issues when embedding a TForm in another TForm? 和其他答案,但我无法使用 TFrame,因为我嵌入的 GUI 是不同 DLL 中的 TForm。

我是否需要捕获选项卡的按键,然后将 Windows 消息发送到嵌入式表单?为此,我想我需要嵌入表单的 HWND,而我没有。

编辑:我将 ShowForm 更改为返回所创建表单的 HWND 的函数,但这没有区别,因为即使 KeyPreview 设置为 true,按下 Tab 时 FormKeyPress 和 FormKeyUp 事件也不会触发.

编辑:必须添加一个 CM_Dialog 消息处理程序来捕获 Tab 键,根据 https://community.embarcadero.com/article/technical-articles/149-tools/13071-detecting-tab-key-press

编辑:但是这样做会阻止 Tab 键在 EXE 中的窗体上的控件中实际使用 Tab 键进行切换,因此这根本不是解决方案,而且使用 PostMessage 将选项卡发送到嵌入的表单也没有任何作用。

如何在使用 Tab 键时获得控件焦点以包含嵌入表单中的控件?

编辑:如果我向 EXE 添加第二个表单,再添加两个按钮,并对该表单执行相同的步骤(关于显示和隐藏),它具有相同的行为,即使第二个表单在EXE 而不是 DLL。

显然,仅设置表单 ParentWindow 不足以使其正确处理。如果我将表单 Parent 属性设置为 TPanel,而不是使用 ParentWindow,它确实适用于 EXE 中的第二个表单。这似乎表明必须将 TWinControl 传递给 DLL。

编辑:不确定这是否可以完成。正如这个问题中所述,它不能,

How to get instance of TForm from a Handle?

我查看了这个问题中提到的 CreateParented,

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

它仍然不允许选项卡流入创建的控件。

编辑:我现在发现的一个部分解决方案是在 EXE 中为应用程序提供一个 OnMessage 事件处理程序,它使用 TMsgIsDialogMessage 传递给 DLL 中的表单,通过对示例程序进行这些更改:

  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,'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;

只有两个问题。

  1. 该选项卡在具有焦点的任何窗体的控件之间循环。它不会从 DLL 中切换到窗体上的控件,也不会从这些控件中跳出到 EXE 中窗体上的控件。
  2. 虽然 DLL 中的表单具有焦点,但该表单上控件的选项卡顺序是相反的。

enter image description here

我大概可以编写代码,在退出EXE窗体的最后一个控件时,利用Winapi.Windows.SetFocus()将焦点移到DLL窗体中,同样地,在最后一个控件退出时,将焦点设置回EXE窗体DLL 中的选项卡。

对于处理反向 Tab 键顺序还没有答案。我知道这不是控件的 TabOrder 属性。他们是正确的。也许我需要在 DLL 中使用另一个 WM_KEYDOWN 消息处理程序?

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...