在使用 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_KEYDOWN
和 WM_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 的功能?的主要内容,如果未能解决你的问题,请参考以下文章