Как определить изменение ключа-модификатора в элементе управления, который не имеет фокуса?

История:

Я работаю над элементом управления, полученным из класса TCustomControl, который может получить фокус и который имеет некоторые внутренние элементы внутри. Эти внутренние элементы выделяются, если пользователь наводит их курсором, вы можете выбрать их, переместить их и так далее. Теперь к проблеме...

Проблема:

Я делаю разные действия с (если скажем) сфокусированным элементом, если пользователь держит модификаторы CTRL, ALT или SHIFT. Я хотел бы изменить курсор мыши, если пользователь наведет элемент и удерживает, например, клавишу CTRL. Довольно просто вы переопределите методы KeyDown и KeyUp и проверьте, равен ли их параметр Key VK_CONTROL. В коде, подобном этому:

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;

Даже если это не лучший способ проверить, была ли нажата и отпущена клавиша CTRL (например, из-за существующего параметра состояния Shift), она работает так, как ожидается, управление имеет фокус, что может даже получить, но...

Моя цель - изменить курсор мыши, когда пользователь наводит элемент управления (или, точнее, определенный элемент внутри него) и удерживает, например. что клавиша CTRL, даже если мой элемент управления не имеет фокуса. Можно сказать, поэтому просто переопределите метод MouseMove и попросите указать там состояния модификатора. И это будет так, но...

Что делать, если пользователь остается с курсором мыши над моим элементом управления и нажмите и отпустите клавишу CTRL? Это не вызовет какого-либо движения мыши или нажатия клавиши для моего контроля, или я ошибаюсь? Ну, так мой вопрос совершенно очевиден...

Вопрос:

Как я могу определить изменения ключа-модификатора, если элемент управления не имеет фокуса, и пользователь не перемещается с помощью мыши? Я думал об этих двух вариантах, но, надеюсь, что-то я пропустил:

  • клавиатурный крючок - надежный, но выглядит довольно переполняющим для меня.
  • периодическая проверка состояния модификатора с помощью таймера - я не мог жить с задержкой

Итак, как бы вы обнаружили изменения ключа-модификатора элемента управления, который в настоящее время не сфокусирован?

Ответ 1

Я бы написал обработчик сообщения для сообщения WM_SETCURSOR, чтобы позвонить GetKeyboardState, чтобы получить (в Delphi вы можете просто вызвать KeyboardStateToShiftState) и на основе результата этого (и теста теста) SetCursor с помощью соответствующего курсора.

Для обработки WM_SETCURSOR есть пример в VCL: TCustomGrid.WMSetCursor в блоке Grids.

Ответ 2

Если ваш элемент управления не сфокусирован, его собственные события ключа не будут активированы. Однако вместо этого вы можете создать свой собственный экземпляр частного TApplicationEvents компонента и использовать его OnMessage, чтобы обнаружить ключевые события, которые извлекаются из основной очереди сообщений, прежде чем они будут отправлены в любой элемент управления для обработки. Затем вы можете проверить, находится ли мышь над вашим контролем (лучше использовать GetMessagePos() вместо GetCursorPos() или Screen.CursorPos, чтобы вы получили координаты мыши на время, когда сообщения были сгенерированы, в случае их задержки) и обновить собственный собственный элемент управления Cursor (не свойство Screen.Cursor) по мере необходимости.

Ответ 3

Ответ Remy скорее всего будет вашим решением, но в случае, если вы пытаетесь сделать это без ограничения инкапсуляции его в элемент управления и оказались здесь:

Вы можете обработать это с помощью трехэтапного процесса, как показано ниже.

Ключевыми здесь являются:

  • Установите курсор управления, а не курсор на экране
  • Использовать свойство KeyPreview
  • Найдите элемент управления под курсором

Я использовал кнопку, чтобы проиллюстрировать процесс. Не забудьте установить форму KeyPreview на 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;

Ответ 4

Я не могу сказать, будет ли это меньше перепроизводства, чем использование крючка, но один из вариантов - использовать raw input". Если вы зарегистрируете свой контроль соответственно, он также получит вход, если он не активен. Пример внедрения для решения..:

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;