How to detect modifier key change in a control which doesn't have focus?
Asked Answered
H

4

14

Background:

I'm working on a control derived from TCustomControl class which can get focus and which has some inner elements inside. Those inner elements are highlighted if the user hovers them with the cursor, you can select them, move them and so on. Now to the problem...

Problem:

I'm doing different actions with the (let's say) focused element if the user holds CTRL, ALT or SHIFT modifiers. What I would like is to change the mouse cursor if the user hovers the element and holds for instance CTRL key. Pretty simple, you just override the KeyDown and KeyUp methods and check if their Key parameter equals to VK_CONTROL. In code like this way:

procedure TMyCustomControl.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited;
  if Key = VK_CONTROL then
    Screen.Cursor := crSizeAll;
end;

procedure TMyCustomControl.KeyUp(var Key: Word; Shift: TShiftState);
begin
  inherited;
  if Key = VK_CONTROL then
    Screen.Cursor := crDefault;
end;

Even if that wouldn't be the best way to check if the CTRL key was pressed and released (e.g. because of the existing Shift state parameter), it works as expected whe the control has focus, which can even get, but...

My aim is to change the mouse cursor when user hovers the control (or to be precise, a certain element inside it) and holds e.g. that CTRL key even when my control doesn't have focus. One can say, so just override the MouseMove method and ask for the modifier states there. And it would be the way, but...

What if the user stays with the mouse cursor over my control and press and release that CTRL key ? That won't generate any mouse move nor key press event for my control, or am I wrong ? Well, so my question is quite obvious...

Question:

How can I detect modifier key changes if the control doesn't have focus and the user doesn't move with the mouse ? I was thinking about these two options, but I hope there is something I missed:

  • keyboard hook - reliable, but looks quite overkill to me
  • periodical check of the modifier states with a timer - I couldn't live with a delay

So, how would you detect modifier key changes of a control which isn't currently focused ?

Headlock answered 24/10, 2013 at 15:59 Comment(4)
Clearly your child controls are windowed. You can have the children of your control inform their parent about the keyboard event.Shinto
@Andreas, sorry, maybe I don't get your comment right... If you mean those elements I mentioned, no. They are just virtual. There is only one window control, which needs to know the changes of the modifier keys when it's hovered. But now I'm thinking about it. Even the parent form doesn't need to be active, so there is maybe no solution "better" than keyboard hook.Headlock
Seems you already know 2 solutions and most probably there is no better solution. If you will check state of modifiers by timer (you need timer only between mouse enter/leave events) let's say 50 times per second, user will not be able to feel delay, so i guess it is much simplier and more stable than system-wide keyboard hook.Gabriello
@Andrei, you're right. 20ms interval will no one even notice. And I can enable it just when the mouse enters the control and the control isn't focused. Keyboard hook would be really overkill. I just forgot on the situation when the application is not active when I was writing this question. Thanks to all!Headlock
E
5

I would write a message handler for WM_SETCURSOR message to call GetKeyboardState to get the keyboard state (in Delphi you can just call KeyboardStateToShiftState) and based on the result of that (and the hit test) call SetCursor with the appropriate cursor.

For handling WM_SETCURSOR, there's an example in the VCL: TCustomGrid.WMSetCursor in the Grids unit.

Esau answered 24/10, 2013 at 18:59 Comment(2)
I really don't see how this will solve a situation when mouse is over a control (not moving) which is on an inactive form, and user presses CTRL key. how this will trigger a WM_SETCURSOR message or did I miss something?Diseur
@kobik, well, it doesn't resolve this situation (with inactive form) so as the other answers. However, in active form this message is being sent even for controls whose doesn't have focus and so it's closest to my needs and also the proper way to handle cursor changing (when the mouse is not captured) hence my accept. Still, I will need to workaround the situation when the form is inactive.Headlock
U
8

If your control is not focused, its own key events will not be triggered. However, what you can do instead is have your control instantiate a private TApplicationEvents component internally, and use its OnMessage event to detect key events being retrieved from the main message queue before they are dispatched to any control for processing. You can then check if the mouse is over your control (better to use GetMessagePos() instead of GetCursorPos() or Screen.CursorPos so that you get the mouse coordinates at the time the messages were generated, in case they are delayed) and update your control's own Cursor property (not the Screen.Cursor property) as needed.

Unseal answered 24/10, 2013 at 17:3 Comment(11)
Will not work if another app has focus (but maybe it is not required).Gabriello
I realized, that for the sake of completness would be fair to track those modifier changes also when the application (form) is not active. I realized that quite late, when I replied to Andreas' comment. This solution will work, but only if the application is active (focused). I'll probably stay by the timer which I'll enable only when the mouse enters the control and the control is not focused. As @Andrei says, setting the interval to 20ms will no one even notice. Thanks anyway!Headlock
@TLama: I can't even begin to imagine how distractive it would be to have an inactive app responding to things I am doing in another application. So what am I missing here? What is the use case for responding to keyboard/mouse messages when your application is not active?Aguayo
@Marjan, kobik. It's a map control, in which the users can place different types of objects (POIs). And those objects are highlighted when you hover them. User can then specify modifiers for certain actions with those objects (actions like e.g. object copy, object move etc.). So let's say you chose CTRL to be the modifier for object moving. You hover the map, the object is highlighted and if you hold CTRL you can drag & drop the object. What is the important fact here, is that without any modifier pressed, you can drag the map itself. So that's why I'm using different cursors...Headlock
...now imagine the situation that the application with such map is not active. You hover the map, the object inside is highlighted again and if you hold CTRL key you can drag & drop the object as before (which also gives a focus to that application). And I wouldn't feel good if I would show the default map drag cursor if you're holding the CTRL key. You know, you as a user could be surprised that you're moving your object instead of the map. I just want to show what will happen if you press the mouse button over that map control.Headlock
@MarjanVenema But if "responding" means changing the mouse cursor on mouse over even if the window is inactive then almost every Windows app does that.Esau
@MarcusAdams: TApplication.HookMainWindow() is for catching messages that are sent to the TApplication window itself, not messages that are posted to the main message queue, like mouse/keyboard events.Unseal
@TLama: if the user is hovering over your map while your app is inactive, it is not going to receive keyboard input. If the user is holding down CTRL for the purpose of dragging something on your map, they still have to click on the map first, thus giving it focus. You can catch that event, check the current state of the CTRL key, and update the map's Cursor accordingly at that time. There is no good reason to react to keyboard events that are occurring in other apps while your app is inactive.Unseal
@Remy, but think about grids for instance (e.g. string grid). They are showing the sizing cursors to let the user know what happens if they click. TOndrej pointed out the WM_SETCURSOR message in his answer whose handler cares about this update for TCustomGrid and it seems like the best way to go for me so far. I'm still afraid that people will just drag the map (possibly with a modifier pressed) without thinking that they need to switch to the application first. So I would really like to have this feature there.Headlock
@TOndrej: Ah, yes, of course, you are absolutely right. Remnants of a migraine clouding mind and drowning out the mouse hover...Aguayo
@TLama: I missed the combination of key + mouse hover (TOndrej cleared my mind on that). Still appreciate the background information very much. Interesting application you are working on.Aguayo
E
5

I would write a message handler for WM_SETCURSOR message to call GetKeyboardState to get the keyboard state (in Delphi you can just call KeyboardStateToShiftState) and based on the result of that (and the hit test) call SetCursor with the appropriate cursor.

For handling WM_SETCURSOR, there's an example in the VCL: TCustomGrid.WMSetCursor in the Grids unit.

Esau answered 24/10, 2013 at 18:59 Comment(2)
I really don't see how this will solve a situation when mouse is over a control (not moving) which is on an inactive form, and user presses CTRL key. how this will trigger a WM_SETCURSOR message or did I miss something?Diseur
@kobik, well, it doesn't resolve this situation (with inactive form) so as the other answers. However, in active form this message is being sent even for controls whose doesn't have focus and so it's closest to my needs and also the proper way to handle cursor changing (when the mouse is not captured) hence my accept. Still, I will need to workaround the situation when the form is inactive.Headlock
A
3

Remy's answer is likely your solution, but in case you're trying to do this without the restriction of encapsulating it into a control and found yourself here:

You could handle this with a three step process, as I've shown below.

The key things here are:

  1. Set the control's cursor, not the screen's cursor
  2. Use the form's KeyPreview property
  3. Find the control under the cursor

I've used a button to illustrate the process. Be sure to set your form's KeyPreview to True.

procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
var
  myControl: TControl;
begin
  // If they pressed CTRL while over the control
  if ssCtrl in Shift then
  begin
    myControl := ControlAtPos(ScreenToClient(Mouse.CursorPos), False, True);
    // is handles nil just fine
    if (myControl is TButton) then
    begin
      myControl.Cursor := crSizeAll;
    end;
  end;
end;

procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
var
  myControl: TControl;
begin
  // If they released CTRL while over the control
  if not(ssCtrl in Shift) then
  begin
    myControl := ControlAtPos(ScreenToClient(Mouse.CursorPos), False, True);
    if (myControl is TButton) then
    begin
      myControl.Cursor := crDefault;
    end;
  end;
end;

procedure TForm1.Button1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin
  // If they move over the button, consider current CTRL key state
  if ssCtrl in Shift then
  begin
    Button1.Cursor := crSizeAll;
  end
  else
  begin
    Button1.Cursor := crDefault;
  end;
end;
Anurous answered 24/10, 2013 at 16:44 Comment(3)
Keyboard events never trigger if, for example, another application is active/has focus, but TLama needs to have modifiers states any time when mouse hovers control (if i understand task correctly).Gabriello
@kobik, thanks. I updated my answer, plus I realized that the OP needs to encapsulate this into a control, so this wouldn't work for that anyway.Anurous
I don't know, why I wrote Screen.Cursor. Surely I wanted to write just Cursor :-) And yes, this could be the out-of-the-box solution also just for the situation when the application is focused. Sorry, I realized that case when I replied to the first comment here. Thanks anyway!Headlock
G
3

I can't tell if it would be less overkill than using a hook, but one option would be to use "raw input". If you register your control accordingly, it will receive input also when it's not active. Sample implementation to decide..:

type
  TMyCustomControl = class(TCustomControl)
    ..
  protected
    ..
    procedure CreateWindowHandle(const Params: TCreateParams); override;
    procedure WMInput(var Message: TMessage); message WM_INPUT;
  ..
  end;

uses
  types;

type
  tagRAWINPUTDEVICE = record
    usUsagePage: USHORT;
    usUsage: USHORT;
    dwFlags: DWORD;
    hwndTarget: HWND;
  end;
  RAWINPUTDEVICE = tagRAWINPUTDEVICE;
  TRawInputDevice = RAWINPUTDEVICE;
  PRawInputDevice = ^TRawInputDevice;
  LPRAWINPUTDEVICE = PRawInputDevice;
  PCRAWINPUTDEVICE = PRawInputDevice;

function RegisterRawInputDevices(
  pRawInputDevices: PCRAWINPUTDEVICE;
  uiNumDevices: UINT;
  cbSize: UINT): BOOL; stdcall; external user32;

const
  GenericDesktopControls: USHORT = 01;
  Keyboard: USHORT = 06;
  RIDEV_INPUTSINK = $00000100;

procedure TMyCustomControl.CreateWindowHandle(const Params: TCreateParams);
var
  RID: TRawInputDevice;
begin
  inherited;

  RID.usUsagePage := GenericDesktopControls;
  RID.usUsage := Keyboard;
  RID.dwFlags := RIDEV_INPUTSINK;
  RID.hwndTarget := Handle;
  Win32Check(RegisterRawInputDevices(@RID, 1, SizeOf(RID)));
end;

type
  HRAWINPUT = THandle;

function GetRawInputData(
  hRawInput: HRAWINPUT;
  uiCommand: UINT;
  pData: LPVOID;
  var pcbSize: UINT;
  cbSizeHeader: UINT): UINT; stdcall; external user32;

type
  tagRAWINPUTHEADER = record
    dwType: DWORD;
    dwSize: DWORD;
    hDevice: THandle;
    wParam: WPARAM;
  end;
  RAWINPUTHEADER = tagRAWINPUTHEADER;
  TRawInputHeader = RAWINPUTHEADER;
  PRawInputHeader = ^TRawInputHeader;

  tagRAWKEYBOARD = record
    MakeCode: USHORT;
    Flags: USHORT;
    Reserved: USHORT;
    VKey: USHORT;
    Message: UINT;
    ExtraInformation: ULONG;
  end;
  RAWKEYBOARD = tagRAWKEYBOARD;
  TRawKeyboard = RAWKEYBOARD;
  PRawKeyboard = ^TRawKeyboard;
  LPRAWKEYBOARD = PRawKeyboard;

//- !!! bogus declaration below, see winuser.h for the correct one
  tagRAWINPUT = record
    header: TRawInputHeader;
    keyboard: TRawKeyboard;
  end;
//-
  RAWINPUT = tagRAWINPUT;
  TRawInput = RAWINPUT;
  PRawInput = ^TRawInput;
  LPRAWINPUT = PRawInput;

const
  RIM_INPUT = 0;
  RIM_INPUTSINK = 1;
  RID_INPUT = $10000003;
  RIM_TYPEKEYBOARD = 1;
  RI_KEY_MAKE = 0;
  RI_KEY_BREAK = 1;

procedure TMyCustomControl.WMInput(var Message: TMessage);
var
  Size: UINT;
  Data: array of Byte;
  RawKeyboard: TRawKeyboard;
begin
  if (Message.WParam and $FF) in [RIM_INPUT, RIM_INPUTSINK] then
    inherited;

  if not Focused and
      (WindowFromPoint(SmallPointToPoint(SmallPoint(GetMessagePos))) = Handle) and
      (GetRawInputData(Message.LParam, RID_INPUT, nil, Size,
      SizeOf(TRawInputHeader)) = 0) then begin
    SetLength(Data, Size);
    if (GetRawInputData(Message.LParam, RID_INPUT, Data, Size,
        SizeOf(TRawInputHeader)) <> UINT(-1)) and
        (PRawInput(Data)^.header.dwType = RIM_TYPEKEYBOARD) then begin
      RawKeyboard := PRawInput(Data)^.keyboard;

      if (RawKeyboard.VKey = VK_CONTROL) then begin
        if RawKeyboard.Flags and RI_KEY_BREAK = RI_KEY_BREAK then
          Cursor := crDefault
        else
          Cursor := crSizeAll; // will call continously until key is released
      end;
      // might opt to reset the cursor regardless of pointer position...


      if (RawKeyboard.VKey = VK_MENU) then begin
        ....
      end;

    end;

  end;
end;
Gassaway answered 27/10, 2013 at 2:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.