<
  • Главная
Статьи

Обробники подій в Delphi

  1. Автор: Олександр Просторів джерело: RSDN Magazine # 4-2004
  2. Процедурні типи даних
  3. Покажчики на методи
  4. Як працюють обробники подій
  5. Як привласнювати обробники подій
  6. операція @
Автор: Олександр Просторів
джерело: RSDN Magazine # 4-2004
Опубліковано: 08.02.2005
Виправлено: 10.12.2016
Версія тексту: 1.0
ПОПЕРЕДЖЕННЯ

Приклади коду, наведені в цій статті, перевірені на Delphi 6 (Build 6.163) і можуть зажадати незначних змін при використанні інших версій Delphi.

Як правило, навчання Delphi починається з книг, які вчать кидати компоненти на форми, налаштовувати їх властивості через Object Inspector, і з його ж допомогою створювати в програмному коді обробники подій. Потім програміст вчиться працювати з компонентами в програмному коді: змінювати їх властивості під час виконання програми (наприклад - при натисканні змінювати напис на кнопці зі "Старт" на "Стоп" і назад) і створювати нові компоненти безпосередньо в Рантайм. При цьому досить часто недостатньо повно описується робота в цьому режимі з обработчиками подій; в більшості книг мову Object Pascal не розглядається досить глибоко для розуміння механізму, і програміст, бачачи в обробниках щось магічне, не знає, як же працювати з ними в своєму коді.

Перш за все, слід розуміти, що обробники подій - такі ж властивості компонента, як Caption або Left. У них немає нічого чарівного, нічого особливого; як і іншим властивостям, їм просто потрібно присвоїти вираз відповідного типу.

Процедурні типи даних

Реалізація обробників подій спирається на можливість Object Pascal, звану процедурними типами (procedural types). Процедурні типи дозволяють описати змінні (а також параметри процедур і функцій, властивості і т. П.), Значенням яких є процедура (надалі під словом "процедура" може розумітися також функція). В потрібний час ця процедура може бути викликана - причому саме та процедура, яка є значенням змінної в поточний момент; якщо потім змінити значення змінної, в наступний раз викликаної виявиться вже інша процедура. Розглянемо приклад:

type TCalcFunction = function (const A, B: integer): integer; function Add (const A, B: integer): integer; begin Result: = A + B; end; function Sub (const A, B: integer): integer; begin Result: = A - B; end; function Mul (const A, B: integer): integer; begin Result: = A * B; end; procedure Example; var CalcFunction: TCalcFunction; begin CalcFunction: = Add; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); CalcFunction: = Sub; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); CalcFunction: = Mul; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); end;

Перш за все, в ньому описується тип TCalcFunction. Значенням змінної цього типу може бути функція, яка отримує два константних параметра типу integer і повертає результат також типу integer. Аби не заглиблюватися в деталі, можна сформулювати просте і досить точне правило: процедура буде коректним значенням для змінної процедурного типу, якщо її декларація (за можливим винятком імен параметрів) збігається з декларацією в процедурному типі; в іншому випадку компілятор видасть повідомлення про помилку "несумісні типи даних".

Потім в процедурі Example за допомогою однієї і тієї ж змінної - CalcFunction - викликаються три різних процедури, що видають результат відповідно 5, -1 і 6.

Фізично змінні процедурного типу є покажчиками на процедуру / функцію. Після операції присвоювання значенням змінної CalcFunction є адреса відповідної процедури. При використанні змінної у виразі (в процедурі ShowMessageFmt) викликається функція, адреса якої збережений в змінній. Як і для інших покажчиків, коректним значенням для змінної процедурного типу буде nil; зрозуміло, спроба виклику функції при такому значенні змінної призведе до помилки. Для перевірки значення змінної можна використовувати звичайні методи: вираз CalcFunction = nil або виклик функції Assigned (CalcFunction).

Виконавши приведення типів, змінної процедурного типу можна привласнити покажчик на процедуру іншого типу. За коректність такої операції, як і у всіх інших випадках, відповідає програміст. Щоб чинити так, необхідно детальне знання реалізації виклику процедур і передачі параметрів в Object Pascal.

Покажчики на методи

Ключовий момент, часто викликає нерозуміння - звичайні процедури істотно відрізняються від методів об'єктів. Якщо в попередньому прикладі функція Add буде визначена в класі - покажчик на неї не вдасться привласнити змінної CalcFunction ні безпосередньо, ні навіть за допомогою приведення типів.

Відмінність методів об'єктів в тому, що крім звичайних параметрів вони отримують ще один, прихований параметр - покажчик на сам об'єкт. Цей покажчик доступний в тілі методу як Self, а також неявно використовується у всіх випадках, коли метод звертається до полів, властивостей або методів об'єкта. При виклику методу об'єкта завжди використовуються два покажчика: наприклад, оператор Form1.Close означає виклик методу Close (покажчик) об'єкта Form1 (покажчик). Покажчики на позначення об'єктів - об'єктні процедурні типи або method pointers - зберігають в собі обидва цих адреси, тому фізично вони реалізуються двома "звичайними" покажчиками і займають вісім байт в пам'яті. Таким чином, їх не вдасться привласнити змінної CalcFunction (яка реалізована одним "звичайним" покажчиком і займає чотири байти) ні безпосередньо, ні за допомогою приведення типів. Покажчики на методи класів (class methods) реалізовані аналогічно, але замість покажчика на об'єкт містять покажчик на клас.

При роботі з об'єктами попередній приклад може бути переписаний наступним чином:

type TCalcFunction = function (const A, B: integer): integer of object; function TTestForm.Add (const A, B: integer): integer; begin Result: = A + B; end; function TTestForm.Sub (const A, B: integer): integer; begin Result: = A - B; end; function TTestForm.Mul (const A, B: integer): integer; begin Result: = A * B; end; procedure Example; var CalcFunction: TCalcFunction; begin CalcFunction: = TestForm.Add; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); CalcFunction: = TestForm.Sub; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); CalcFunction: = TestForm.Mul; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); end;

У декларацію типу TCalcFunction додаються ключові слова of object - це вказує, що значенням буде не просто процедура, але метод класу. Крім того, при присвоєнні значення змінної CalcFunction потрібно вказати об'єкт, метод якого присвоюється - саме цей об'єкт буде значенням Self при виклику методу. Тут присутній синтаксична неоднозначність: в разі, якщо процедуру Example зробити методом класу TTestForm, явне вказівку об'єкта стане необов'язковим; колишній синтаксис (CalcFunction: = Add) призведе до присвоєння значення Self.Add. Це звичайне для об'єктів угоду краде зовнішню відмінність між procedural types і method pointers і часто призводить до непорозумінь.

Слід мати на увазі, що змінна об'єктного процедурного типу підтримує покажчик на об'єкт, і цей об'єкт може бути знищений в будь-який час, залишивши покажчик на себе некоректним. Виклик процедури після цього, швидше за все, призведе до тих чи інших неприємних наслідків.

При використанні об'єктних процедурних типів порівняння з nil стає некоректним - замість нього необхідно використовувати функцію Assigned. Присвоєння змінній значення nil залишається коректним і обнуляє обидва покажчика: на об'єкт і на процедуру. Процедура Assigned перевіряє на рівність nil тільки покажчик на процедуру; таким чином, можна викликати метод об'єкта, передавши йому в якості Self значення nil.

Як працюють обробники подій

Відповідь на це питання - "дуже просто". Розглянемо, наприклад, як реалізований обробник події TButton.OnClick (опустивши несуттєві подробиці, але зібравши разом весь код, який має до цього відношення):

type TNotifyEvent = procedure (Sender: TObject) of object; TControl = class (TComponent) private FOnClick: TNotifyEvent; protected procedure Click; dynamic; property OnClick: TNotifyEvent read FOnClick write FOnClick; end; procedure TControl.Click; begin if Assigned (FOnClick) then FOnClick (Self); end;

Клас TButton успадковується від TControl. і публікує властивість OnClick. Object Inspector, бачачи властивість процедурного типу, розміщує його на сторінці обробників подій. При натисканні на кнопку клас TButton викликає метод Click, і той викликає відповідний обробник події, якщо останній присвоєно.

Як привласнювати обробники подій

Розглянемо кілька прикладів. Для початку припустимо, що ми хочемо створити на формі групу з 25 (5x5) кнопок, присвоївши кожної один і той же обробник події OnClick. Це робиться так:

type TTestForm = class (TForm) procedure FormCreate (Sender: TObject); private procedure OwnButtonClick (Sender: TObject); end; procedure TTestForm.FormCreate (Sender: TObject); var i, j: integer; begin for i: = 0 to 4 do for j: = 0 to 4 do with TButton.Create (Self) do begin Caption: = Format ( 'Button% d% d', [i, j]); Top: = 100 + 30 * i; Left: = 100 + 90 * j; Width: = 80; Height: = 25; OnClick: = OwnButtonClick; Parent: = Self; end; end; procedure TTestForm.OwnButtonClick (Sender: TObject); begin ShowMessageFmt ( 'Натиснуто кнопка% s', [TButton (Sender) .Caption]); end;

Тут слід звернути увагу, що ми присвоюємо оброблювачу події метод об'єкта TestForm, і саме ця форма буде Self-му в тілі обробника події. Для того щоб дістатися до властивостей натиснутою кнопки, використовується параметр Sender. Сам метод OwnButtonClick в нашому випадку доданий "руками" в визначення класу TTestForm, але з тим же успіхом можна скористатися обробником події, створеним для одного з компонентів на формі.

Процедура, що привласнюється оброблювачу події, може бути визначена в будь-якому класі, не обов'язково у формі. Припустимо, наприклад, ми робимо клас, який підтримує список форм в порядку переходу користувача між ними - наприклад, для реалізації функціональності на зразок кнопки "Назад".

type TActiveFormWatcher = class private FForms: TComponentList; protected procedure ActiveFormChanged (Sender: TObject); public constructor Create; end; constructor TActiveFormWatcher.Create; begin FForms: = TComponentList.Create (false); Screen.OnActiveFormChange: = ActiveFormChanged; end; procedure TActiveFormWatcher.ActiveFormChanged (Sender: TObject); begin FForms.Add (Screen.ActiveForm); end;

Тут оброблювачу події Screen.OnActiveFormChange присвоюється метод класу TActiveFormWatcher. Аналогічної технікою можна скористатися всюди; навіть якщо ви пишете звичайну процедуру, ви можете описати невеликий службовий клас і привласнити його метод оброблювачу створюваного компонента.

Недоліком такого підходу (так само як і створення класу-Сінглтона) є те, що необхідно піклуватися про явне створенні і знищенні об'єкта. У загальному випадку слід йти саме цим шляхом; тим не менш, для повноти картини опишемо пару хакерських прийомів, які можна використовувати в цьому випадку.

Щоб працювати з класом, не створюючи об'єктів цього класу, можна скористатися методами класу (class methods). Наприклад, привласнити обробник OnClick довільній кнопці можна наступним чином:

type TButtonClicker = class class procedure ButtonClick (Sender: TObject); end; class procedure TButtonClicker.ButtonClick (Sender: TObject); begin ShowMessageFmt ( 'Натиснуто кнопка% s', [TButton (Sender) .Caption]); ShowMessageFmt ( 'Self.Name =% s', [Self.ClassName]); end; procedure AssignButtonClick (const Button: TButton); begin Button.OnClick: = TButtonClicker.ButtonClick; end;

Власне, це варіант я б навіть не назвав хакерських; за винятком привласнення (де могло знадобитися приведення типів) все абсолютно коректно. У тілі обробника можна використовувати Self, причому він, як і належить, відповідає в цьому випадку класу об'єкта. Переваги такого підходу обмежені неможливістю опису змінних класу; якби не це, такий підхід був би коректніше створення Сінглтона (точніше, був би найбільш зручним варіантом Сінглтона).

Можна також присвоїти оброблювачу події покажчик на звичайну процедуру, яка не є методом класу. У цьому випадку попередній приклад буде виглядати так:

procedure ButtonClick (Self: TButton; Sender: TButton); begin ShowMessageFmt ( 'Натиснуто кнопка% s', [Sender.Caption]); ShowMessageFmt ( 'Self =% s', [Self.Name]); end; procedure AssignButtonClick (const Button: TButton); var Method: TMethod; begin Method.Code: = @ButtonClick; Method.Data: = Button; Button.OnClick: = TNotifyEvent (Method); end;

В цьому випадку в процедурі, призначеної бути обробником події, необхідно явно описати ще один, додатковий параметр, який і буде отримувати значення Self. Правильний метод його опису залежить від угоди про зв'язки (calling convention), використовуваного в процедурному типі і методі. Для моделі register (яка використовується для всіх стандартних обробників і прийнята за замовчуванням для всіх процедур в Delphi) цей параметр повинен йти першим, а за ним - всі інші, так, як вони описані в типі обробника події. Для приведення типів використовується спеціальний тип TMethod, описаний в модулі System; він являє собою внутрішню реалізацію об'єктного процедурного типу.

На користь такого підходу можна знайти два аргументи. По-перше, в цьому випадку ми легко можемо передати в обробник події будь-який параметр Self (так, в прикладі, ми передаємо немов би власний метод кнопки). По-друге, знаючи дійсні типи параметрів, ми можемо використовувати їх при декларації процедури і уникнути зайвих привидів типів: так, в ButtonClick параметри Self і Sender визначені як TButton, хоча, взагалі кажучи, коректніше було б описати їх як TObject.

Нарешті, існує ще один, найбільш некоректний прийом. Зовсім не обов'язково створювати об'єкт для того, щоб можна було скористатися його методом. Класичним прикладом тут буде процедура TObject.Free, яка добре і правильно працює з покажчиком на об'єкт, що має значення nil. Можна скористатися цим підходом і привласнити оброблювачу покажчик на метод не створеного об'єкта, наприклад, так:

type TButtonClicker = class procedure ButtonClick (Sender: TObject); end; procedure TButtonClicker.ButtonClick (Sender: TObject); begin ShowMessageFmt ( 'Натиснуто кнопка% s', [TButton (Sender) .Caption]); end; procedure AssignButtonClick (const Button: TButton); begin Button.OnClick: = TButtonClicker (nil) .ButtonClick; end;

В даному випадку клас TButtonClicker використовується тільки для того, щоб процедура ButtonClick була сумісна з типом властивості TButton.OnClick. Вона не використовує Self, тому замість посилання на об'єкт можна вказати значення nil; зрозуміло, в цьому випадку будь-яка спроба, пряма або непряма, використовувати Self призведе до помилки "access violation".

На користь цього підходу, мабуть, можна сказати тільки те, що він найбільш "простий" - не вимагає знання методів класів або приведення типів через TMethod. У той же час він найбільш небезпечний і не має ніяких переваг перед іншими методами; тому правильним буде не використовувати його.

операція @

Особливим моментом, про який необхідно згадати, є операція @ (взяття адреси). Взагалі кажучи, її слід було б використовувати при всіх операціях з процедурними типами. Так, процедуру Example з самого першого прикладу було б в деякому сенсі правильніше записати так:

procedure Example; var CalcFunction: TCalcFunction; begin CalcFunction: = @Add; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); CalcFunction: = @Sub; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); CalcFunction: = @Mul; ShowMessageFmt ( 'CalcFunction (2, 3) =% d', [CalcFunction (2, 3)]); end;

Зверніть увагу на використання операції @ в привласнення. Результат в цьому випадку буде абсолютно таким же: компілятор відстежує операції з процедурними типами і при необхідності додає неявну операцію взяття адреси. У той же час в ряді випадків таке міркування неможливо: так, при використанні TMethod нам довелося написати @ButtonClick через те, що поле TMethod.Code визначено як pointer, а не як процедурний тип. Аналогічно, в процедурі Example ми могли б написати:

if CalcFunction = Add then

і тим поставити компілятор перед вибором: чи то ми хотіли порівняти покажчики, чи то - викликати дві функції і порівняти їх результати. У випадку, якби TCalcFunction визначав функцію без параметрів, обидва цих тлумачення були б синтаксично вірні. Щоб уникнути конфліктів в таких випадках компілятор завжди використовує виклик процедури, а для порівняння покажчиків необхідно використовувати операцію @ - тобто писати

if @CalcFunction = @Add then

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

Дякую Максима Гумерова, також відомого як Slicer [Mirkwood] , За цінні зауваження та коментарі

Ця стаття опублікована в журналі RSDN Magazine # 4-2004. Інформацію про журнал можна знайти тут


Новости
  • Виртуальный хостинг

    Виртуальный хостинг. Возможности сервера распределяются в равной мере между всеми... 
    Читать полностью

  • Редизайн сайта

    Редизайн сайта – это полное либо частичное обновление дизайна существующего сайта.... 
    Читать полностью

  • Консалтинг, услуги контент-менеджера

    Сопровождение любых интернет ресурсов;- Знание HTML и CSS- Поиск и обновление контента;-... 
    Читать полностью

  • Трафик из соцсетей

    Сравнительно дешевый способ по сравнению с поисковым и контекстным видами раскрутки... 
    Читать полностью

  • Поисковая оптимизация

    Поисковая оптимизация (англ. search engine optimization, SEO) — поднятие позиций сайта в результатах... 
    Читать полностью