Відпрацьовуємо майстерність гри на клавіатурі у Делфі (як запрограмувати діалог гарячих клавіш)

 | 11.19

М

Мой Компьютер, №4, 21.01.2008

Компонент «гаряча клавіша»

Клавіші у нас будуть не тільки глобальні, але й локальні, а оскільки перші мало чим відрізняються від других, то застосуємо наслідування. Буде два компоненти — TLocalMHK та TGlobalMHK. І поводитися ми будемо з ними не так, мовляв, щоб кинув на форму і забув, адже повідомлення про натиснення хоткея отримує саме форма, а не компоненти, отже вони будуть, так би мовити, пасивні. Щоб вони працювали, форма в процедурі WMHotKey має повідомити всі глобальні компоненти-хоткеї про це. Для локальних аналогічні дії треба зробити в FormKeyDown.

Але писати однаковий код кожного разу при створенні нового проекту не доведеться: ми збережемо клас з модифікованою формою і в новому проекті буде достатньо змінити TForm1 = class(TForm) на TForm1 = class(TMHKForm). Але якщо для форми все одно треба писати додатковий код, то навіщо створювати окремі компоненти для хоткеїв, якщо можна весь код написати в FormKeyDown- та WMHotKey-форми?

Для локальних — так, але у нас ще є глобальні, а їх треба реєструвати і дереєструвати, та ще морочитись з id. Ось це і буде інкапсулювати TGlobalMHK, зводячи додатковий код для форми до необхідного мінімуму. Є спосіб, за допомогою якого компонент на формі зможе реагувати на події форми без її відома: для цього він в конструкторі замінює віконну процедуру форми на нову, яка є методом цього компонента, в новій віконній процедурі відстежує необхідні повідомлення і, виконавши необхідні дії, викликає стару віконну процедуру. В деструкторі треба стару повернути на місце. От тільки якщо таких компонентів буде декілька, то ті, що створяться першими, будуть в прольоті.

Кожен компонент буде мати процедуру Check. Форма, проходячись по всіх компонентах, буде викликати Check з параметром TMHotKey для локального і id для глобального хоткея. Компонент «локальна гаряча клавіша» буде мати властивості:

  • власне гаряча клавіша, яка визначає комбінацію, при натисненні якої компонент буде спрацьовувати і викликати процедуру OnHotKey, а також Execute для асоційованого TAction’а;
  • Enabled для довільного вмикання та вимикання компонента;
  • Action для вибору асоційованого TAction’а;
  • подію OnHotKey.

Помістимо його в модуль LocalMHK:

 TLocalMHK = class(TComponent)

 private

 protected

 FMHotKey: TMHotKey;

 FOnHotKey: TNotifyEvent;

 FEnabled: Boolean;

 FAction: TBasicAction;

 procedure SetMHotKey(mhk: TMHotKey); virtual;

 public

 constructor Create(AOwner: TComponent); override;

 function Check(mhk: TMHotKey): boolean;

 published

 property MHotKey: TMhotKey read FMHotKey write SetMHotKey;

 property OnHotKey: TNotifyEvent read FOnHotKey write FOnHotKey default nil;

 property Action: TBasicAction read FAction write FAction default nil;

 property Enabled: Boolean read FEnabled write FEnabled default true;

 end;

В конструкторі ми просто ініціалізуємо (обнулюємо) поля класу. Деструктор в цьому класі взагалі не потрібен:

constructor TLocalMHK.Create(AOwner: TComponent);

begin

 inherited Create(AOwner);

 Self.MHotKey := MHKNone;

 Self.FEnabled := true;

end;

Сетер для властивості MHotKey.

procedure TLocalMHK.SetMHotKey(mhk: TMHotKey);

begin

 Self.FMHotKey := mhk;

end;

Навіщо такий сетер? Та без нього спокійно можна обійтись, це в TGlobalMHK він потрібен, отже я вирішив зробити ці два класи більш схожими.

Якщо була натиснута наша комбінація (CompareMHK), і ми активні (FEnabled), то викликаємо подію OnHotKey і вмикаємо Action, якщо два останніх у нас є. Check повертає true за збігом комбінацій:

function TLocalMHK.Check(mhk: TMHotKey): boolean;

begin

 Result := false;

 if Self.FEnabled and not MHKIsNone(Self.FMHotKey) and CompareMHK(mhk, Self.FMHotKey) then begin

 if Assigned(FAction) then FAction.Execute;

 if Assigned(FOnHotKey) then FOnHotKey(Self);

 Result := true;

 end;

end;

Я не навів процедуру реєстрації компонента в палітрі, адже вона генерується автоматично при використанні майстра створення компонентів. Пораджу тільки всі компоненти складати на окрему вкладку палітри, наприклад, MHotKeys.

Перейдемо до TGlobalMHK. З’явилось два нових поля. FId треба для RegisterHotKey та для Check — пам’ятаєте, в WMHotKey приходить саме ідентифікатор, і спрацювати повинен той TGlobalMHK, у якого такий же id. Registered потрібний для читання результату виклику RegisterHotKey, щоб можна було дізнатись про невдачу:

TGlobalMHK = class(TLocalMHK)

 private

 FRegistered: boolean;

 FId: integer; 

 protected

 procedure SetMHotKey(mhk: TMHotKey); override;

 public

 constructor Create(AOwner: TComponent); override;

 function Check(id: integer): boolean;

 procedure Unregister;

 published

 property Registered: boolean read FRegistered default false;

 end;

В конструкторі найголовніше — генерація id, яку компонент робить сам. Ідентифікатор має бути унікальним, для цього слід дізнатись про кількість раніше створених TGlobalMHK. На щастя, це робиться легко одним циклом. За допомогою індексованої властивості форми Components отримуємо доступ до всіх її компонент і is’ом перевіряємо, чи є цей компонент TGlobalMHK.

constructor TGlobalMHK.Create(AOwner: TComponent);

var f: TForm;

 var i: integer;

begin

 inherited Create(AOwner);

 Self.FId := 0;

 f := Self.Owner as TForm;

 for i := f.ComponentCount — 1 downto 0 do

 if f.Components[i] is TGlobalMHK then inc(Self.FId);

end;

Сетер для MHotKey. В ньому вже недостатньо просто змінити відповідне поле FMHotKey, як у попередньому, потрібно ще звільнити стару і зареєструвати нову комбінацію клавіш:

procedure TGlobalMHK.SetMHotKey(mhk: TMHotKey);

begin

 inherited SetMHotKey(mhk);

 Self.Unregister;

 Self.FRegistered := RegisterMHotKey((Self.Owner as TForm).Handle, Self.FId, Self.MHotKey);

end;

Дереєстрація винесена в окремий метод для зручності. В ній просто викликаємо UnregisterMHotKey, попередньо перевіривши, чи має компонент власника. Власник (форма) потрібний для отримання хендла вікна. Є ще одна причина, про яку пізніше. Поки що завважте, що цей метод у нас public, і ззовні він нам знадобиться.

procedure TGlobalMHK.Unregister;

begin

 if Self.Owner <> nil then UnregisterMHotKey((Self.Owner as TForm).Handle, Self.FId);

end;

Check аналогічний попередньому, лише порівнюються тепер ідентифікатори.

function TGlobalMHK.Check(id: integer): boolean;

begin

 Result := false;

 if Self.FEnabled and (Self.FId = id) then begin

 if Assigned(FAction) then FAction.Execute;

 if Assigned(FOnHotKey) then FOnHotKey(Self);

 Result := true;

 end; 

end;

А чи нічого ми не забули? А звільнити гарячу клавішу після знищення компонента? Пишемо деструктор і в ньому робимо Self.Unregister. Е, ні, стійте. Справа в тому, що в деструкторі ми вже не знаємо свого власника, тож не зможемо й отримати хендл вікна.

Мені ця ситуація сподобалась, тому опишу її детальніше. Будь-яким компонентом може володіти інший, але не всі компоненти можуть бути володарями. При знищенні володар має звільнити всі компоненти, якими володіє. Форма володіє всіма компонентами, на ній розміщеними, навіть якщо ті знаходяться на іншому компоненті, наприклад, TPanel. Ось ієрархія класа форми:

TForm

TCustomForm

TScrollingWinControl

TWinControl // тут знищується хендл

TControl

TComponent // тут знищуються підвладні компоненти

TPersistent

TObject

Деструктори викликаються згори вниз. Ось частина деструктора TWinControl:

destructor TWinControl.Destroy;

var

 I: Integer;

 Instance: TControl;

begin

 …

 // знищується хендл

 if FHandle <> 0 then DestroyWindowHandle;

 …

 inherited Destroy;

end;

Компоненти знищуються пізніше. Частина деструктора Tcomponent:

destructor TComponent.Destroy;

begin

 …

 // виклик процедури знищення компонентів

 DestroyComponents;

 …

 inherited Destroy;

end;

Як же саме знищуються компоненти?

procedure TComponent.DestroyComponents;

var

 Instance: TComponent;

begin

 // цикл по всіх компонентах

 // Instance — поточний компонент

 while FComponents <> nil do

 begin

 Instance := FComponents.Last;

 if (csFreeNotification in Instance.FComponentState)

   or (FComponentState * [csDesigning, csInline] = [csDesigning, csInline]) then

   RemoveComponent(Instance)

 else

   Remove(Instance);

 // виклик деструктора компонента

 Instance.Destroy;

 end;

end;

До знищення компонента проходить виклик такої собі Remove (RemoveComponent теж викликає Remove). І подивившись її код, ми знайдемо рядок AComponent.FOwner := nil, після цього викликається деструктор компонента, отже, все правильно: в деструкторі ми вже не знаємо свого власника. А хоч би й знали, так форма давно звільнила свій хендл. Ось так. Взагалі, блукати по коду VCL досить цікаво.

Рішення цієї проблеми потребуватиме додаткового коду в клас форми: в її деструкторі треба пройтись по всім TGlobalMHK і зробити їм Unregister. Ось для чого цей метод є public.

Рушій гарячих клавіш

Зробимо тепер форму з підтримкою TLocalMHK та TGlobalMHK:

TMHKForm = class(TForm)

 private

 FDisableNextKey: boolean;

 procedure WMHotKey(var Msg : TWMHotKey); message WM_HOTKEY;

 public

 constructor Create(AOwner: TComponent); override;

 destructor Destroy(); override;

 procedure KeyDown(var Key: Word; Shift: TShiftState); override;

 procedure KeyPress(var Key: Char); override;

 end;

В конструкторі забезпечуємо перехоплення натиснень клавіш для дочірніх елементів, щоб працювали локальні хоткеї.

constructor TMHKForm.Create(AOwner: TComponent);

begin

 inherited Create(AOwner);

 Self.KeyPreview := true;

end;

В деструкторі проходимось по глобальним хоткеям та дереєструємо їх:

destructor TMHKForm.Destroy;

var i: integer;

begin

 for i := Self.ComponentCount — 1 downto 0 do

 if Self.Components[i] is TGlobalMHK then

   (Self.Components[i] as TGlobalMHK).Unregister;

 inherited Destroy;

end;

Далі KeyDown та WMHotKey. Для всіх хоткеїв викликаємо Check, щоб вони могли спрацювати:

procedure TMHKForm.KeyDown(var Key: Word; Shift: TShiftState);

var i: integer;

 s: String;

begin

 inherited KeyDown(Key, Shift);

 Key2String(Chr(Key), s);

 if s = ‘None’ then exit;

 for i := Self.ComponentCount — 1 downto 0 do

 if (Self.Components[i] is TLocalMHK) then begin

   if (Self.Components[i] as TLocalMHK).Check(MHKFromKeyDown(Key, Shift)) then begin

    Self.FDisableNextKey := true;

    break;

   end;

 end;

end;

procedure TMHKForm.WMHotKey(var Msg: TWMHotKey);

var i: integer;

begin

 for i := Self.ComponentCount — 1 downto 0 do

 if (Self.Components[i] is TGlobalMHK) then

   if (Self.Components[i] as TGlobalMHK).Check(Msg.HotKey) then break;

end;

Ще маленьке доопрацювання. Коли ми натискаємо комбінацію клавіш, наприклад Shift+A, в якому-небудь полі вводу, то хоткей спрацює, але і в полі з’явиться нова літера, тому в ReyPress ми можемо заборонити вивід символу в залежності від флагу FDisableNextKey.

procedure TMHKForm.KeyPress(var Key: Char);

begin

 inherited KeyPress(Key);

 if Self.FDisableNextKey then begin

 Key := #0;

 Self.FDisableNextKey := false;

 end;

end;

Зручне введення комбінацій клавіш

Компонент для введення хоткеїв можна було б зробити зі звичайного Edit’а, але той має деякі недоречні властивості, тому зробимо на основі TCustomEdit:

TMHotKeyEdit = class(TCustomEdit)

 private

 FMHotKey: TMHotkey;

 FMHK: TLocalMHK;

 procedure SetMHotKey(Value: TMHotkey);

 

 protected

 procedure KeyDown(var Key: Word; Shift: TShiftState); override;<

Robo User
Robo User
Web-droid editor