How can my TComponent intercept the ESC key and handle it?
Asked Answered
O

1

8

In my TComponent, there is a point where I want to listen to key events and intercept the ESC key and handle it in my component, consume/"eat" the keystroke, so that for example the owner form won't handle it at that stage. Just like in TDragObject when you start do drag and cancel it by pressing ESC.

Problem is that TDragObject has AllocateHWnd that is notified by it's owner form with CN_KEYDOWN. But no one notifies my component.

Do I need to replace the form's WindowProc with my own? If yes, then how to do it correctly "by the book" so to speak?


Just to be 100% clear:

TMyComponent = class(TComponent)

I made a small test and it seems to work:

TMyComponent = class(TComponent)
  private
    FOldWindowProc: TWndMethod;
    FParentForm: TCustomForm;
    procedure FormWindowProc(var Message: TMessage);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;    
end;

...

constructor TMyComponent.Create(AOwner: TComponent);
begin
  if not (AOwner is TWinControl) then
    raise Exception.Create('TMyComponent.Create: Owner must be a TWinControl');
  inherited Create(AOwner);
  // hook parent form
  FParentForm := GetParentForm(TWinControl(Owner));
  if Assigned(FParentForm) then
  begin
    FOldWindowProc := FParentForm.WindowProc;
    FParentForm.WindowProc := FormWindowProc;
  end;
end;

destructor TMyComponent.Destroy;
begin
  // unhook parent form
  if Assigned(FParentForm) then
    FParentForm.WindowProc := FOldWindowProc;
  inherited;
end;

procedure TMyComponent.FormWindowProc(var Message: TMessage);
begin
  FOldWindowProc(Message);
  if Message.Msg = CM_CHILDKEY then // CM_CHILDKEY -> CM_DIALOGKEY -> CM_DIALOGCHAR
  begin
    OutputDebugString('CM_CHILDKEY');
    if Message.WParam = VK_ESCAPE then
    begin
      Beep;
      // do my stuff...
      Message.Result := 1; // consume keystroke
    end;
  end;
end; 

I'm wondering if this is the right/only approach.

Ochoa answered 15/1, 2013 at 21:33 Comment(12)
Dragging involves a new modal loop. That's not an option for you. Hard to see how you can do this cleanly without cooperation from the host.Persona
@DavidHeffernan, the OP said "like in TDragObject", it's just an example and I assume the OP just wants the ESC key, nothing more, nothing less. ESC is a Dialog key. I just don't have the 1 minute required to look up the code/API/windows message right now.Freely
@Cosmin A drag operation's modal loop owns and pumps the queue. And so can get hold of key presses. But a component on a form doesn't have that luxury. How do you propose to get inside the app's message loop?Persona
@DavidHeffernan, I was thinking of the WM_GETDLGCODE handler with the DLGC_WANTALLKEYS; Don't have time to test.Freely
@Cosmin Surely the dialog manager does not have anything to do with a VCL form.Persona
@DavidHeffernan of course it does, and it's blocking the keys used for dialog navigation. It's (for example) what a TMemo does in order to get the Tab, Return and Arrows keys. Grep the message in the VCL sources.Freely
@Cosmin OK. Presumably the control would need to have input focus.Persona
@Ochoa Like I said, you'll need to hook the main message queue. Off the top of my head, not sure how to do that.Persona
@DavidHeffernan, I actually got some results, but not 100% it's all super duper :)Ochoa
If " there is a point where I want to listen to key events" implies only a specific duration, you don't have to replace the window procedure for the entire lifetime of the component. BTW there are other approaches, like installing a message hook (SetWindowsHookEx), or having your own message loop... Discussing about the right approach, I guess, would require knowing the exact scenario.Kob
CM_CHILDKEY will fire for input messages delivered to VCL controls. But not to non-VCL controls. That's probably fine for you. The way you replace WindowProc is fine. Apart from the bit where you write TWinControl(Owner). That's invalid.Persona
Check if you get WM_CANCELMODE messages. That's the usual way that Windows tells you the Esc key (or other event that cancels a capture/drag) must end.Grenadines
A
4

One way might be to create a TApplicationEvents object inside of your component, and then use its OnMessage event to peek at messages from the main thread message queue, such as keystrokes, before the rest of the VCL processes them.

Aschim answered 16/1, 2013 at 1:42 Comment(5)
Can components realistically do that? What if the app assigns OnMessage? Won't that be a conflict?Persona
TApplicationEvents is designed as a multicasting class. Multiple instances receive the same events. IOW, when a single message arrives in the queue, all assigned TApplicationEvents.OnMessage handlers get dibs at it, if everyone uses TApplicationEvents, that is. If someone uses TApplication.OnMessage directly then TApplicationEvents.OnMessage can break, and vice versa, yes. It is not a perfect system, but it is better than nothing.Aschim
OnMessage events from multiple TApplicationEvents components will be fired in reverse order of when those components are created.Tamaratamarack
Do you think it's a better solution than hooking WindowProc like I did?Ochoa
The alternative is to use SetWindowsHookEx() to create a thread-specific keyboard hook or getmessage/wndproc hook. That would be more isolated than trying to hook the VCL events.Aschim

© 2022 - 2024 — McMap. All rights reserved.