在使用 Frames 时,有没有办法拥有类似 KeyPreview 的功能?

Posted

技术标签:

【中文标题】在使用 Frames 时,有没有办法拥有类似 KeyPreview 的功能?【英文标题】:Is there a way to have a KeyPreview-like functionality when working with Frames? 【发布时间】:2015-02-12 06:46:31 【问题描述】:

我想在 Frames 中有一个 KeyPreview 功能,我的意思是,当输入(例如,选择框架的控件之一,或者鼠标在里面)在一个框架中时(它会有几个面板和其他控件)然后用户按下的键首先由框架处理。

有没有办法做到这一点?我在 TFrame 中没有找到类似 KeyPreview 的属性。

我使用的是 XE5 版本的 RAD Studio,尽管我主要使用 C++Builder。

【问题讨论】:

@TLama TFrame 没有 KeyDown 事件 我的意思是覆盖 KeyDown 方法,但这似乎不起作用,因此我删除了我的评论。 当你发现时告诉我! ***.com/questions/27116331/…(主题略有不同,但仍然是关于框架如何“吞噬”用户事件) 您不能从 Application.OnMessage 执行此操作吗? 您可以在 Frame 上放置 TApplicationEvents 并处理 OnMessage 事件(处理 WM_KEYDOWN 消息)。 【参考方案1】:

感谢我最近的"When does a ShortCut fire"-investigation,我为您的 Frame 制定了一个独立的解决方案。

简而言之:所有关键信息都输入活动控件的TWinControl.CNKeyDwon。该方法调用TWinControl.IsMenuKey,它遍历所有父母,同时确定消息是否是快捷方式。 Is 通过调用其GetPopupMenu.IsShortCut 方法来实现。我已经覆盖了 Frame 的 GetPopupMenu 方法,如果它不存在则创建一个。请注意,您仍然可以随时将 PopupMenu 添加到 Frame 中。通过继承TPopupMenu 并覆盖IsShortCut 方法,调用Frame 的KeyDown 方法,作为您需要的KeyPreview 功能。 (我也可以分配 OnKeyDdown 事件处理程序)。

unit Unit2;

interface

uses
  Winapi.Messages, System.Classes, Vcl.Controls, Vcl.Forms, Vcl.Menus,
  Vcl.StdCtrls;

type
  TPopupMenu = class(Vcl.Menus.TPopupMenu)
  public
    function IsShortCut(var Message: TWMKey): Boolean; override;
  end;

  TFrame2 = class(TFrame)
    Label1: TLabel;
    Edit1: TEdit;
  private
    FPreviewPopup: TPopupMenu;
  protected
    function GetPopupMenu: Vcl.Menus.TPopupMenu; override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
  end;

implementation

$R *.dfm

 TPopupMenu 

function TPopupMenu.IsShortCut(var Message: TWMKey): Boolean;
var
  ShiftState: TShiftState;
begin
  ShiftState := KeyDataToShiftState(Message.KeyData);
  TFrame2(Owner).KeyDown(Message.CharCode, ShiftState);
  Result := Message.CharCode = 0;
  if not Result then
    Result := inherited IsShortCut(Message);
end;

 TFrame2 

function TFrame2.GetPopupMenu: Vcl.Menus.TPopupMenu;
begin
  Result := inherited GetPopUpMenu;
  if Result = nil then
  begin
    if FPreviewPopup = nil then
      FPreviewPopup := TPopupMenu.Create(Self);
    Result := FPreviewPopup;
  end;
end;

procedure TFrame2.KeyDown(var Key: Word; Shift: TShiftState);
begin
  if (Key = Ord('X')) and (ssCtrl in Shift) then
  begin
    Label1.Caption := 'OH NO, DON''T DO THAT!';
    Key := 0;
  end;
end;

end.

【讨论】:

有一个缺点:TranslateMessage 还没有被调用,所以所有字符都是大写的。我该如何解决? 这看起来不错!我明天试试,让你知道。感谢您的想法! 这很好地解决了我的需求,谢谢!我对大写字符没有问题,因为我只处理特殊键。 @NGLN 这看起来很脏且令人费解。为什么要把菜单带进来? @David 实际上,从活动控件到父框架是一个奇怪的解决方法,但它(有点)有效,它是一个解决方案。这就是为什么。【参考方案2】:

如果您当时在表单上只有一个框架,您可以利用表单 KeyPreview 功能并将必要的信息转发到框架。

如果您只是转发信息,则不需要对原始 VCL 代码进行任何更改,只需修改 TFrame 类即可。所以不用担心你可能会破坏整个 VCL 这样做。

这是一个快速的代码示例:

主窗体代码:

unit Unit2;

interface

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

type
  TForm2 = class(TForm)
    Panel1: TPanel;
    ModifiedFrame: TModifiedFrame;
    Edit1: TEdit;
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormCreate(Sender: TObject);
  private
     Private declarations 
  public
     Public declarations 
  end;

var
  Form2: TForm2;

implementation

$R *.dfm

procedure TForm2.FormCreate(Sender: TObject);
begin
  //This is required since I'm asigning frames OnKeyDown event method manually
  ModifiedFrame.OnKeyDown := ModifiedFrame.FrameKeyDown;
end;

procedure TForm2.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  //Forward key down information to ModifiedFrame
  ModifiedFrame.DoKeyDown(Sender, Key, Shift);
  if Key = 0 then
    MessageDlg('Key was handled by the modified frame!',mtInformation,[mbOK],0)
  else
    MessageDlg('Key was not handled!',mtInformation,[mbOK],0);
end;

end.

修改帧代码:

unit Unit3;

interface

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

type
  TModifiedFrame = class(TFrame)
    Edit1: TEdit;
    //Normally this method would be added by the Delphi IDE when you set the
    //OnKeyDown event but here I created this manually in order to avoid crating
    //design package with modified frame
    procedure FrameKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  private
     Private declarations 
    FOnKeyDown: TKeyEvent;
  public
     Public declarations 
    //This is used to recieve forwarded key down information from the Form
    procedure DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  published
    //Property to alow setting the OnKeyDown event at design-time
    //NOTE: In order for this to work properly you have to put this modified
    //frame class into separate unti and register it as new design time component
    property OnKeyDown: TKeyEvent read FOnKeyDown write FOnKeyDown;
  end;

implementation

$R *.dfm

procedure TModifiedFrame.DoKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  //Check to see if OnKeyDownEvent has been assigned. If it is foward the key down
  //information to the event procedure
  if Assigned(FOnKeyDown) then FOnKeyDown(Self, Key, Shift);
end;

procedure TModifiedFrame.FrameKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  //Do something
  if Key = VK_RETURN then
  begin
    MessageBeep(0);
    Key := 0;
  end;
end;

end.

使用类似的方法,您可以转发其他关键事件。

【讨论】:

是的,我有类似的东西,但正在寻找更优雅的东西:) 更优雅?更优雅是什么意思? 嗯,这并不涉及更改使用框架的每个表单。【参考方案3】:

如果您愿意更改VCL代码,这是可行的。

KeyPreview 在TWinControl.DoKeyDown 方法中处理。从代码控件中可以看出,如果KeyPreview被打开,有焦点的控件会查找它的父窗体并调用它的DoKeyDown方法。

function TWinControl.DoKeyDown(var Message: TWMKey): Boolean;
var
  ShiftState: TShiftState;
  Form, FormParent: TCustomForm;
  LCharCode: Word;
begin
  Result := True;

 // Insert modification here

   First give the immediate parent form a try at the Message 
  Form := GetParentForm(Self, False);
  if (Form <> nil) and (Form <> Self) then
  begin
    if Form.KeyPreview and TWinControl(Form).DoKeyDown(Message) then
      Exit;
     If that didn't work, see if that Form has a parent (ie: it is docked) 
    if Form.Parent <> nil then
    begin
      FormParent := GetParentForm(Form);
      if (FormParent <> nil) and (FormParent <> Form) and
      FormParent.KeyPreview and TWinControl(FormParent).DoKeyDown(Message) then
        Exit;
    end;
  end;
  with Message do
  begin
    ShiftState := KeyDataToShiftState(KeyData);
    if not (csNoStdEvents in ControlStyle) then
    begin
      LCharCode := CharCode;
      KeyDown(LCharCode, ShiftState);
      CharCode := LCharCode;
      if LCharCode = 0 then Exit;
    end;
  end;
  Result := False;
end;

要更改该行为,您需要更改 TWinControl.DoKeyDown 代码以扫描帧,或者为您要使用的每个 TWinControl 后代拦截 WM_KEYDOWNWM_SYSKEYDOWN,最后添加 KeyPreview 字段到基础框架类。

最好的选择可能是声明IKeyPreview 接口,并在扫描父窗体/框架时测试父窗体是否实现该接口。如果没有找到,您可以回退到原始代码。这将包含仅对 TWinControl.DoKeyDown 方法的 VCL 代码更改,并且您可以在需要的地方轻松地在 Frames 中实现接口。

注意:在 Windows 上,具有焦点的控件接收关键事件。因此,只有在某些控件具有焦点时,上述修改才能找到框架。鼠标是否悬停在框架上不会对功能产生任何影响。

更详细的代码如下所示:

Frame 必须实现的接口定义:

  IKeyPreview = interface
    ['D7318B16-04FF-43BE-8E99-6BE8663827EE']
    function GetKeyPreview: boolean;
    property KeyPreview: boolean read GetKeyPreview;
  end;

查找实现IKeyPreview接口的父框架的函数,应该放在Vcl.Controls实现部分的某处:

function GetParentKeyPreview(Control: TWinControl): IKeyPreview;
var
  Parent: TWinControl;
begin
  Result := nil;
  Parent := Control.Parent;
  while Assigned(Parent) do
    begin
      if Parent is TCustomForm then Parent := nil
      else
      if Supports(Parent, IKeyPreview, Result) then Parent := nil
      else Parent := Parent.Parent;
    end;
end;

TWinControl.DoKeyDown修改(插入以上原代码):

var
  PreviewParent: IKeyPreview;

  PreviewParent := GetParentKeyPreview(Self);
  if PreviewParent <> nil then
    begin
      if PreviewParent.KeyPreview and TWinControl(PreviewParent).DoKeyDown(Message) then
        Exit;
    end;

【讨论】:

感谢 DAlija 的想法和详细的代码,但我担心在这种情况下修改 VCL 库并不是一个真正的选择。如果在 Delphi 中我会三思而后行,使用 C++Builder 我只会颤抖。我想知道为什么 Embarcadero 没有添加开箱即用的方法,至于我能找到的,这是一个反复出现的问题。我了解它可能导致的问题,但您可以做的其他几件事也是如此。 由于修改只是在实现部分,这不应该有那么大的问题,但我理解你的不情愿。我也不喜欢更改 VCL 代码。 是的,我遇到了很多问题低级:/无论如何谢谢!我想我会走丑陋的路线,将 Key 处理程序添加到 Frame 并从使用框架的表单中调用它。 Err,除非我的眼镜让我失望(而且他们经常这样做),否则 OP 没有给出 Delphi 版本的指示。 BC 在 D7 和 XE6 中的 Controls.Pas 之间发现不少于 1649 个不同的部分,在 TWinControl.DoKeyDown 中仅在两者之间就有 19 个更改的行。所以我想知道在“时间限制”的情况下建议更改 VCL 代码的收费是否明智。 @MartynA 你是对的,我没有说我使用的是什么版本:它是 XE5。我猜这在以后的版本中没有改变?

以上是关于在使用 Frames 时,有没有办法拥有类似 KeyPreview 的功能?的主要内容,如果未能解决你的问题,请参考以下文章

在过滤R data.frames时更新因子水平[重复]

有没有办法在 JAVAFX 上实现像“渲染”这样的属性?

有没有办法在 JAVAFX 上实现像“渲染”这样的属性?

使用“Frames”进行 Web 检查器分析:当时间线中没有任何内容时查找性能问题的原因

有没有办法在 vuejs 路由器中拥有动态路由/组件?

Python Plotly Dash 有没有办法在资产文件夹之外使用 Css 文件? / 拥有多个资产文件夹