Message методи

Автор: Сергій Галездінов, Королівство Delphi


Дана стаття призначення для більш глибокого розуміння того, як реалізована обробка повідомлень Windows в VCL і як це можна і потрібно використовувати в своїх цілях і використовувати правильно.


Напевно кожен з нас хоча б раз у своїй практиці, але зустрічався з кодом виду:










TForm1 = class(TForm)
private
procedure MyCoolHandler (var Message: TMessage); message WM_USER;
public
{some code here}
end;

procedure TForm1.MyCoolHandler(var Message: TMessage);
begin
Message.Result := 32767;
Caption := “wow, this works!”;
end;


Подібний код зустрічається дуже часто в різного роду радах, прийомах і сприймається відразу як щось органічне, не викликає питань. Ну так, додали реакцію на повідомлення для форми і всього ділов, працює. Ай да чарівники ці розробники Delphi! Можна обробляти повідомлення у формі …


Однак, як це працює? Чому це працює? І як слід правильно це використовувати? Які можуть виникнути проблеми, підводні камені? Давайте подивимося.


Передумови


Логіка виникнення подібного трюку особисто мені абсолютно зрозуміла – розробники Delphi хотіли ясний, зрозумілий і читається код при розробці візуальних компонент. Як усім відомо, при створенні вікна необхідно задати віконну функцію. Тобто певну callback функцію, яка приймає по посиланню повідомлення і повертає результат. У всіх прикладах програм на чистому WinAPI ця віконна функія складається з case з перерахуванням всіх повідомлень, яке вікно хоче обробляти і виклику DefWindowProc для інших повідомлень. Для обробки невеликого числа повідомлень цей case не страшний. Однак якщо вікно обробляє велику кількість повідомлень і обробники повідомлень містять розгалуження, секції винятків і інші заплутують речі, то налагодження, пошук обробки потрібного повідомлення і читання подібного перетворюється на сущий кошмар (Але ж якщо поглянути на кількість контролів і класів в Delphi можна прийти в жах від подібної перспективи).


Розробники RTL пішли хитрим шляхом. Замість городу з case вони придумали хитрий, витончений і дуже цікавий трюк – message методи.


Як це працює


– частина мови Delphi, ця функціональність реалізована вже в TObject, так що створюючи будь-який клас ви вже маєте можливість обробляти повідомлення (про це трохи пізніше). При додаванні message методу (сигнатура процедури при цьому повинна бути певного виду: procedure Name(var Message: MessageRecord); message [Число]) в клас цей метод розташовується в vmt за адресою, кратному числовому значенню повідомлення, що стоїть після ключового слова message. – динамічні, адже обробка повідомлення повинна бути не тільки у базового класу, але і у його нащадків. Щоб "послати повідомлення", класу треба сформувати струкутру Message (не обов'язково TMessage, про це трохи пізніше) і викликати метод Dispatch потрібного класу. Цей метод робить пошук у vmt адресу методу по зсуві Msg структури Message у класу і, якщо його немає безпосередньо у класу, серед message методів його батьків. Якщо адреса методу не знайдено (Тобто обробка такого повідомлення не присутній ні в одного класу в ієрархії), то виробляється виклик методу DefaultHandler. Таким чином, схема обробки повідомлень в VCL набуває більш зручну для сприйняття і модифікації форму. Замість:










 function WindowProc (hwnd: HWND; uMsg: Cardinal; wParam: WPARAM; lParam: LPARAM): Integer; stdcall;
begin
case uMsg of
WM_NULL:
begin
{Код обробки WM_NULL}
Result := 0;
end;
… {Кілька кілометрів коду з перерахуванням всіх повідомлень}
WM_USER:
begin
{Код обробки WM_USER}
Result := 0;
end
else
Result := DefWindowProc(hwnd, uMsg, wParam, lParam);
end;
end;


отримуємо (приблизно):










TWinControl = class(TControl)
private
procedure WMNull(var Message: TMessage); message WM_NULL;
… {Кілька кілометрів коду з перерахуванням message методів всіх повідомлень}
procedure WMUser(var Message: TMessage); message WM_USER;
protected
procedure DefaultHandler(var Message); override;
procedure WndProc(var Message: TMessage); override;
end;

{ TWinControl }

procedure TWinControl.WMNull(var Message: TMessage);
begin
{Код обробки WM_NULL}
Message.Result := 0;
end;
… {Кілька кілометрів коду методів всіх повідомлень}
procedure TWinControl.WMUser(var Message: TMessage);
begin
{Код обробки WM_USER}
Message.Result := 1;
end;

procedure TWinControl.WndProc(var Message: TMessage);
begin
Dispatch(Message);
end;

procedure TWinControl.DefaultHandler(var Message);
begin
with TMessage(Message) do
Result := DefWindowProc(Handle, Msg, WParam, LParam)
end;


Якщо зобразити вищесказане у вигляді схеми, то отримаємо для класичної обробки:



Після вибірки повідомлення з черги, воно направляетя у віконну процедуру (1), де пробігається через case (2) і направляється в блок обробки повідомлення (3), потім результат повертається як результату віконної функції (4).


Типова схема обробки повідомлень в VCL буде виглядати приблизно так:



Після вибірки повідомлення (1), воно направляється у віконну процедуру WndProc (2). У ній відбувається виклик методу Dispatch (3), який шукає, чи є у TWinControl обробник даного повідомлення (4). Нехай у обробнику буде зроблений виклик inherited. Метод динамічний, тому буде проводитись пошук серед message методів батька (тобто TControl). Для зручності розуміння (хоча на ділі звичайно не так, переконайтеся в цьому, відкривши вікно CPU) пройдемо такий же шлях – через Dispatch. Структура, що містить повідомлення направляється в батьківський клас (4.1, 4.2), в ньому йде пошук обробника. Якщо обробник не знайдений, викликається метод DefaultHandler. Оскільки DefaultHandler у TWinControl перевизначений, то відбудеться його виклик (4.3), в якому буде виклик обробника повідомлень за замовчуванням, тобто для Windows це DefWindowProc. Далі буде воврат в обробник (4) і у віконну процедуру повернеться вже структура зі зміненим значенням поля Result, яке в підсумку віддається як результат обробки повідомлення.


Виходячи з вищеописаного алгоритму, можна зробити декілька висновків:


По-перше, методи динамічні. При цьому не обов'язково писати override або називати метод тим же ім'ям і навіть приймати структуру того ж типу, розміру і з тим же вирівнюванням полів, виклик inherited всередині message методу приведе до виклику message методу батька з передачею туди структури Message, або, якщо у батьківських класів немає обробки цього повідомлення, до виклику DefaultHandler. Це відіграє дуже важливу роль при роботі з VCL.


По-друге, message методи не обов'язково повинні приймати саме тип TMessage, описаний у модулі Messages. Досить того, щоб структура мала перше поле типу DWORD, щоб можна було здійснити перехід за адресою, рівному числовому значенню цього поля. На інші параметри структури не накладається обмежень. VCL використовує TMessage, оскільки всі повідомлення Windows, а так само для користувача повідомлення CN_BASE + xxx мають одну (або схожу за розмірами) структуру. Однак, структуру обов'язково потрібно передавати по посиланню.


Треба зауважити, що на діапазон оброблюваних повідомлень накладається обмеження, а саме від 1 до 49151. Чому 49151? Тому що даний прийом був введений перш за все для обробки повідомлень Windows, а в Windows номери повідомлень від 1 до WM_USER-1 зарезервовані системою, від WM_USER до $ 7FFF – для користувальницьких повідомлень і від WM_APP до $ C000-1 (49151) – для повідомлень на рівні додатку. Від $ C000 до $ FFFF йде діапазон строкових користувальницьких повідомлень рівня докладання, створюваних через RegisterWindowMessage, результат виклику фунції неможливо передбачити на етапі компіляції, тому логіку обробки подібних повідомлень краще робити у віконній процедурі. З приводу віконної процедури також варто відзначити, що спочатку повідомлення йде в статичний метод MainWndProc, а в ньому вже йде безпечний виклик WndProc. Для того, щоб Windows могла прийняти MainWndProc як віконну функцію, VCL використовує функцію Classes.MakeObjectInstance. Функція повертає адресу на процедуру, яку можна віддати Windows як віконну, і яка перенаправляє всі виклики віконної функції в метод класу.


Inherited


Як вже було сказано, message методи динамічні. А це означає, що кожен новий нащадок може перевизначати реакцію на повідомлення. Як і будь-перекриття, його потрібно робити правильно. Виклик inherited НЕ в тому місці або його відсутність може вплинути на логіку роботи контрола, тому потрібно чітко розуміти, для чого потрібен чи не потрібен виклик батьківського обробки повідомлення. Якщо виклику батьківського обробки не проводиться, то стежте за повертаним результатом повідомлення.


Підводні камені


Не завжди в VCL можна вирішити задачу обробки повідомлення виключно перевизначенням message методу. Іноді це не призводить ні до якого результату, тому, що крім обробки повідомлення в message методі йде її обробка і в WndProc. Яскравий тому приклад. Автору питання необхідно було заборонити малювання системної стрілки в меню, що випадає. Але обробка WM_DRAWITEM не призводила ні до чого, тому що в WndProc стан Canvas пункту меню поверталося у вихідне. Тому іноді все-таки доводиться лізти в WndProc, хоч це й негарно:).


також є однією з причин, по якій класичний сабклассінг (тобто перевизначення віконної процедури вікна через SetWindowLong) в Delphi вкрай не рекомендується. Однією з причин, як відомо, є метод Perform у TControl, що перенаправляє повідомлення прямо у віконну процедуру. Якщо всередині коду класу буде виклик Perform, то буде викликаний метод WndProc класу, а не фактична віконна процедура вікна (яка, як відомо виходить з приведення WndProc до виду, яким вимагає Windows через MakeObjectInstance). Однак і message методи тут не найкращі помічники – метод Dispatch направляє повідомлення відразу в обробник, а не у віконну процедуру. Тому проводячи сабклассінг потрібно бути готовим до подібних фокусів.


Якщо читач знайомий з WTL або MFC в сі, то він явно зауважив, що подібний механізм є і там – через карти повідомлень. Там є схожа проблема – якщо повідомлення прийшло через безпосередній виклик SendMessage, то ці повідомлення обходять фільтри повідомлень.


Власна вигода:)


Отже, стало зрозуміло як це працює в VCL. Проте використання message методів не обмежується тільки лише віконними (тобто володіють хендлов) компонентами, і клас TControl тому підтвердження – всі неокони контроли здатні обробляти повідомлення. І віконний компонент відповідальний за перенаправлення повідомлень підлеглим неокони контролем. Саме message методи зробили це можливим! Більше того, їх використання також не обмежується тільки обробкою повідомлень Windows – ви можете з таким же успіхом обробляти свої повідомлення, озброївшись знанням про те, як вони працюють. Приміром, у вас є завдання обміну якимись даними між класами, проте заздалегідь не можна передбачити якими саме і як обробляти результат. Можна наверзти ліс з купи методів, що надають можливість обміну різнотипними даними, а можна скористатися механізмом передачі повідомлень. Наприклад, задавши таку структуру:










type
TDataMessage = record
Msg: DWORD;
Data: Integer;
DataSize: Integer;
Result: Integer;
Tag: Integer;
end;


і 2 повідомлення – передача рядки і передача числа:










const
MSG_STRING = 1;
MSG_INT = 2;


можна обмінюватися рядковими і цілочисельними даними між класами через message методи:










type
TClass1 = class
private
FSomeString: WideString;
procedure OnStringGet (var Msg: TDataMessage); message MSG_STRING;
procedure OnIntegerReceived (var Msg: TDataMessage); message MSG_INT;
end;

procedure TClass1.OnIntegerReceived(var Msg: TDataMessage);
begin
Msg.Result := Msg.Data * 2;
end;

procedure TClass1.OnStringGet (var Msg: TDataMessage);
begin
FSomeString := “test string example”;
Msg.Result := Integer(PWideChar(FSomeString));
end;


var
Msg: TDataMessage;
begin
with Msg do
begin
Msg := MSG_STRING;
Data := 1321564;
end;
Class1.Dispatch(Msg);
Class2.SomeIntValue := Msg.Result;

FillChar(Msg, SizeOf(Msg), 0);
Msg.Msg := MSG_STRING;
Class1.Dispatch(Msg);
Class2.SomeStr := Copy(PWideChar(Msg.Result), 1, 10);



Знову ж таки, взявши на озброєння механізми в VCL, можна писати програми на чистому Вінапу, не місто при цьому багатокілометрових case у віконних процедурах;) Або можна писати свій набір неокони компонент і при цьому спокійно обробляти віконні повідомлення. Це розкриває широкі можливості для розгулу фантазії:)


Кілька слів про передачу рядків і потоках


Якщо ви зважилися використовувати подібний прийом в своїх проектах, то повинні чітко розуміти що і як слід передавати. Рекомендую в структурах не використовувати занадто багато полів, бажано використовувати тільки цілі поля, щоб передавати адреси переданих даних. Якщо ви передаєте рядок string, то пам'ятайте про лічильник посилань (дуже гарна стаття про це ось тут), якщо передаєте PChar, то подбайте про коректне виділення та звільнення пам'яті. Я б порекомендував WideString, оскільки в них немає лічильника посилань.


При використанні message методів в окремому потоці не забувайте про синхронізацію. Dispatch – це не SendMessage, при передачі повідомлення класу в потоці з іншим контекстом зупинки потоку не буде!


Крім того, стежте за часом життя змінних, адреси яких передаєте. Краще всього використовувати адреси глобальних змінних або членів класу. Це ж справедливо і для багатопотокового використання. Не забувайте захищати дані критичними секціями та своєчасно виконуйте синхронізацію.


Сподіваюся, стаття не здалася вам нудною і незрозумілою, і дала вам їжу для роздумів.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*