Відпрацьовуємо майстерність гри на клавіатурі у Делфі (як запрограмувати діалог гарячих клавіш)
28.08.08М
Мой Компьютер, №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;<
Web-droid редактор
Не пропустите интересное!
Підписывайтесь на наши каналы и читайте анонсы хай-тек новостей, тестов и обзоров в удобном формате!
Видеонаблюдение от Ajax: обзор IP-камер DomeCam Mini, TurretCam, BulletCam и видеорегистратора NVR
Компания Ajax Systems продолжила расширение своей экосистемы устройств, представив камеры видеонаблюдения и видеорегистратор для управления видеопотоками. Достаточно консервативный и устоявшийся тип устройств, традиционно для компании подкрепили глубокой проработкой софта и комплектующих.
Новый Volkswagen Transporter будет выпускаться в электро и плагин-гибридной версии
Volkswagen автомобиль электротранспортМикроавтобус Volkswagen Transporter следующего поколения будет представлен в начале 2025 года.
Intel хочет заработать на производстве чипов для Министерства обороны США
Intel война процессор СШАХотя детали соглашения не разглашаются, существуют предположения, что Intel займется созданием чипов с искусственным интеллектом для военных дронов и вычислительных систем, использующих генеративный ИИ в разведке