Професійна розробка програм за допомогою Delphi5: частина 2

Введення у створення компонентів Delphi


При розробці програм за допомогою Borland Delphi створювати компоненти зручно з наступних причин:



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

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

  3. Простий і ефективний спосіб обміну кодом з іншими програмістами. Є чимало сайтів, наприклад http://www.torry.net/, де можна знайти вільно поширювані компоненти або придбати їх за символічну плату.

Пакети компонентів


У Delphi компоненти зберігаються в пакетах (packages). Список використовуваних пакетів компонентів можна викликати за допомогою пункту меню Component / Install Packages (щоправда, цей діалог чомусь має заголовок Project Options).


Рис.1. Файл проекту з коментарями і пов'язане з ним вікно View Form


За допомогою цього діалогу можна додати новий пакет (Add), видалити наявний (Remove). Видалення означає не фізичне видалення файлу з диска, а видалення посилання з середовища розробки на даний пакет. При додаванні нового пакету компоненти, що зберігаються в ньому, з'являються на палітрі, а при видаленні – навпаки, зникають. Пакет можна не видаляти, а «заховати» його вміст на етапі розробки за допомогою зняття позначки навпроти імені пакета в списку. Можна також переглянути компоненти та їх піктограми (Components). І нарешті, можна відредагувати додані користувачем пакети (Edit) – пакети, що поставляються разом з Delphi, редагувати не можна (кнопка Edit недоступна).


У даному діалозі можна вказати, яким чином створювати проект: з використанням runtime-пакетів або без них. Звідси ясно, що пакети компонентів бувають двох типів: runtime package (пакет, що працює під час виконання) та design-time package (пакет, який використовується під час розробки). Всі вони представляють собою DLL (виконувані бібліотеки).


Runtime-пакети (розширення *. bpl) поставляються кінцевому користувачеві разом з проектом, якщо проект був скомпільований з включеною опцією Build with runtime packages. Сам додаток (*. exe або *. dll) в цьому випадку виходить невеликим, але разом з ним треба передавати досить об'ємні *. bpl-файли. Згідно з оцінками фахівців поставка проекту з runtime-пакетами дає перевагу в обсязі поставляються файлів, якщо тільки він включає п'ять або більше модулів (*. exe або *. dll), написаних на Delphi. При спільній роботі цих модулів досягається економія ресурсів операційної системи, оскільки один завантажений в ОЗУ пакет обслуговує кілька модулів.


Design-time-пакети (розширення *. dcp) використовуються тільки на етапі розробки. Під час розробки вони підтримують створення компонентів на формі. У скомпільований проект Delphi включає код не з пакету компонентів, а з *. dcu-файлів. Хоча *. dcp-файл генерується з *. dcu-файлу, їх вміст може не збігатися, якщо в *. pas-файл були внесені зміни, і пакет не був перекомпілювати. Компіляція можлива тільки для пакетів, створених програмістами. Це досягається натисненням кнопки Edit у вищезгаданому діалозі. Після цього з'являється форма, яка дозволяє проводити маніпуляції з пакетом.

Рис. 2. Використання похилих шрифтів


Пакет містить дві секції. У секції Contains наведено список модулів, що формують компоненти даного пакета (*. pas-та *. dcu-файли) і їх піктограми (*. dcr-файли). Секція Required містить посилання на інші пакети, необхідні для роботи цих компонентів. Додавання нового компонента до пакету виконується кнопкою Add, видалення наявного – кнопкою Remove. До тих пір поки пакет не буде скомпільований натисканням кнопки Compile, всі зміни, що вносяться до пакету, не будуть з'являтися в середовищі розробки. І нарешті, команда Install доступна в тому випадку, коли вміст пакету видалено з середовища розробки за допомогою зняття позначки навпроти імені пакета в попередньому діалозі.


Команда Option дозволяє вибрати для компіляції пакета опції, аналогічні опцій проекту. У них можна визначити тип даного пакету: працюючий під час виконання, що працює під час розробки, або той та іншої одночасно (тип пакету за замовчуванням). В опціях визначаються каталоги, в яких слід шукати необхідні модулі та зберігати результати компіляції. У них також визначаються дії, необхідні для відладки: перевіряти чи ні діапазон допустимих значень, як здійснювати оптимізацію, як обробляти помилки введення-виведення. І нарешті, в опції може бути включена інформація про версію пакету. Це дуже важливо, якщо додаток поширюється разом з runtime-пакетами: при роботі програми установки інформація про версії дозволить коректно замінити застарілі версії пакетів, і навпаки, при спробі інсталювати пакет більш ранньої версії, що вже наявний на даному комп'ютері, останній не буде перезаписаний.


Шаблони компонентів


Delphi дозволяє створювати найпростіші складові компоненти з декількох звичайних компонентів, обраних на формі під час розробки. Відповідний експерт викликається за допомогою пункту меню Components / Create Component Template. Цей пункт меню доступний, якщо на формі виділено хоча б один компонент. Після його вибору з'являється діалогова панель Component Template Information.

Рис. 3. Ефект промальовування фону символів


У цьому діалозі слід вказати ім'я класу та ім'я сторінки на палітрі компонентів, куди слід помістити новий компонент. Якщо сторінка з такою назвою відсутній на палітрі компонентів, то вона буде створена. Можна також змінити запропоновану піктограму нового компонента, завантаживши відповідний *. bmp-файл.


При створенні шаблону запам'ятовуються як властивості, змінені програмістом в інспектора об'єктів, так і обробники подій, пов'язані з виділеними елементами управління. При цьому обробники подій запам'ятовуються повністю, без фільтрації звернень до інших (не виділеним на формі) компонентів, глобальним змінним, методам і т.д. Відповідно, якщо в іншому проекті такі компоненти (змінні, методи) відсутні, то при спробі скомпілювати такий проект буде отримано діагностичне повідомлення Unknown Identifier.


Коли слід користуватися шаблонами? Перш за все, у випадках, якщо необхідно змінити будь-які властивості, які є за умовчанням в базовому класі. Наприклад, в будь-якому додатку використовується елемент керування для редагування рядки тексту жовтого кольору. Можна помістити компонент TEdit на форму, змінити властивість Color на жовтий, відзначити даний компонент і зберегти як шаблон. Після цього можна звертатися до даного шаблону, і поміщений на форму компонент буде мати жовтий колір. Однак не варто зловживати цією можливістю, адже для елемента керування зі зміненим кольором буде створено новий клас і в пам'яті будуть розмножені всі віртуальні методи. Це негативно позначиться на ресурсах операційної системи.


Використовувати шаблони компонентів зручно також, коли необхідно перенести ряд компонентів разом з обробники подій з однієї форми на іншу. Для цього всі вони виділяються, створюється шаблон компонентів, який і поміщається на нову форму. При цьому будуть перенесені не тільки самі компоненти, але і обробники подій, чого не можна досягти при виклику команд Copy / Paste – в останньому випадку обробники подій будуть втрачені.


Компоненти, створювані за допомогою команди Create Component Template, істотно відрізняються від звичайних компонентів, створюваних стандартним способом (описаним нижче). Візуально головна відмінність полягає в наступному: якщо шаблон включає в себе кілька елементів управління, то, після того як такий компонент поміщений на форму, можна виділити окремий елемент управління і видалити його – при цьому інші збережуться на формі. Для стандартних компонентів, якщо вони включають в себе декілька елементів управління, неможливо виділити один з них і видалити-компонент виділяється і віддаляється цілком.


Створення найпростішого компонента


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


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


Створення компонента починається з вибору пункту меню Component / New components. Після цього відразу ж з'являється діалог New Component.

Рис. 4. Ілюстрація режиму Transparent


У цьому діалозі необхідно визначити клас-предок, ім'я новостворюваного класу, сторінку на палітрі, куди буде поміщений новий компонент, ім'я модуля, що містить реалізацію нового компонента, і шлях до нього. Якщо новий компонент використовує інші модулі, шлях до яких не описаний, то їх необхідно визначити в полі Search Path.


Отже, перша (і, мабуть, головна) завдання – вибір класу-предка. У випадаючому списку в якості класу-предка пропонуються всі компоненти, що є на палітрі, в тому числі і ті, які не входять в стандартну поставку Delphi. Необхідно як класу-предка вибрати клас, який максимально наближений за властивостями до створюваного класу. Для нашої задачі можна, наприклад, вибрати в якості предка TWinControl, але в цьому випадку нам буде потрібно реалізовувати всі візуальні ефекти натискання кнопки і т.д. Тому ми вибираємо як предка TButton.


Ім'я новостворюваного класу має відображати зміст компонента і в жодному разі не збігатися з ім'ям вже зареєстрованого компонента! На етапі заповнення даного діалогу імена на збіги не перевіряються – пригоди, пов'язані з такою помилкою, почнуться пізніше …


При виборі сторінки необхідно знати, що якщо задати ім'я неіснуючої сторінки, то буде створена нова.


І нарешті, при натисканні як кнопки Install, так і кнопки OK, буде створена заготівля для реалізації нового компонента. Однак при натисканні кнопки Install заготівля буде поміщена на палітру компонентів, а при натисканні кнопки OK – просто створена. Рекомендується використовувати кнопку Install. Після того як компонент буде інстальований, його можна помістити на форму. Тепер всі зміни, що вносяться до код реалізації компонента, будуть компілюватися разом з проектом, і програміст відразу ж буде отримувати повідомлення про помилки. Якщо компонент не інсталювати, то для пошуку помилок його необхідно компілювати через редактор пакетів (див. вище) натисканням кнопки Compile, що менш зручно.


Отже, після натискання кнопки Install з'являється ще один діалог, який дозволяє визначити пакет, куди буде поміщений даний компонент.


У цьому діалозі є дві сторінки, на першій з них можна вибрати один з існуючих пакетів, а на другий – створити новий. Дуже бажано давати короткий текстовий опис пакету, саме воно буде показуватися в діалозі, що викликається по команді Component / Install packages (див. вище). Після вибору пакета і натискання клавіші OK викликається редактор пакета, куди автоматично поміщається новостворений модуль реалізації нового компонента. Корисно не закривати його, а зрушити в один з кутів екрану, щоб він міг бути активований натисканням клавіші миші.


Одночасно в редакторі коду буде створена «заготовка» для опису нового компонента:

unit ButtonBeep;
 
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;
 
type
  TButtonBeep = class(TButton)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
  end;
 
procedure Register;
 
implementation
procedure Register;
begin
  RegisterComponents(“Samples”, [TButtonBeep]);
end;
 
end.

У найновішому класі оголошені чотири секції, значення яких детально описано в розділі «Область видимості змінних та методів» попередньої статті даного циклу (КомпьютерПресс № 1 "2001). Крім того, у новому класі визначена процедура Register, яка викликається середовищем розробки Delphi при інсталяції даного модуля як компонента. Вона містить ім'я сторінки на палітрі, куди поміщається даний компонент, і в квадратних дужках – ім'я класу. Взагалі, як параметр метод Register приймає масив типів класів, адже в одному модулі може бути реалізовано кілька компонентів. Тому вони відокремлюються один від одного комами, наприклад:

procedure Register;
begin
RegisterComponents ("Samples", [TButtonBeep, TButtonColor, TMyEdit]);
end;

Продовжимо рішення поставленої задачі – створення кнопки, яка видає писк. Зробимо спочатку тривіально (але як з'ясується потім, невірно) – призначимо обробник події OnClick в конструкторі кнопки. Для цього в секції private визначимо заголовок нового методу BtClick (Sender: TObject) і реалізуємо його в секції реалізації:

procedure TButtonBeep.BtClick(Sender:TObject);
begin
  Beep;
end;

Далі перепишемо конструктор кнопки. Для цього визначимо в секції public заголовок конструктора:


constructor Create(AOwner:TComponent); override;


з обов'язковою директивою override! Реалізуємо його в секції реалізації:

constructor TButtonBeep.Create(AOwner:TComponent);
begin
  inherited Create(AOwner);
  OnClick:=BtClick;
end;

Після цього скомпіліруем компонент. Поставимо зі сторінки Samples кнопку на форму і запустимо проект на виконання. Можна переконатися, що кнопка при натисканні пищить!


Тепер знову перейдемо в середовище розробки і призначимо обробник події OnClick в інспектора об'єктів. У обробнику події виведемо текст у заголовок форми:

procedure TForm1.ButtonBeep1Click(Sender:TObject);
begin
  Caption:=”Test”;
end;

Запустимо проект на виконання і спробуємо натиснути на кнопку. Заголовок форми змінюється, але кнопка пищати перестала! Помилка полягає в тому, що на одну подію кнопки OnClick ми спробували визначити два обробника: один всередині компонента BtClick, а інший призначили за допомогою інспектора об'єктів. Після відпрацювання конструктора TButtonBeep у нас було посилання на перший обробник BtClick. Потім відбувається завантаження ресурсів, оброблювачу події OnClick призначається метод ButtonBeep1Click. При цьому посилання на перший обробник – BtClick – безповоротно втрачається.


Таким чином, при написанні нових компонентів завжди слід враховувати можливість зміни властивостей і обробників подій за допомогою інспектора об'єктів. Якщо будь-яку властивість (подія) не повинно змінюватися, його не слід відображати в інспектора об'єктів. А якщо воно вже відображається, його слід приховати (про це ми поговоримо пізніше). Програміст має повне право змінити будь-які властивості в інспекторі об'єктів, і якщо після цього компонент перестає працювати, в цьому винен розробник компонента, але ні в якому разі не програміст, його використовує.


Як же все-таки коректно вирішити це завдання? Один із способів створення компонентів – переписування вже наявних методів. При розгляді файлу StdCtrls.pas, де реалізовані вихідні коди для компонента TButton, можна відзначити в ньому наявність динамічного методу Click, який можна переписати. Тому знову повертаємося до вихідного коду, створеному експертом Delphi при створенні компонента (прибираємо конструктор і метод BtClick). Потім в секції public визначаємо заголовок методу:

procedure Click; override;  

і наводимо реалізацію методу:

procedure TButtonBeep.Click;
begin
  inherited Click;
  beep;
end;

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


На даному прикладі корисно проаналізувати можливі помилки при написанні коду:



  1. Забута директива override при визначенні заголовка методу Click. Кнопка перестає пищати, отже, метод Click не викликається.

  2. Забутий виклик методу-предка (inherited Click) в реалізації процедури Click. Кнопка продовжує пищати при натисканні, але код в призначеному в інспекторі об'єктів обробнику подій не виконується. Отже, метод Click класу TButton викликає подія OnClick.

Тепер поміняємо піктограму компонента TButtonBeep на палітрі. За замовчуванням для нового компоненту використовується піктограма компонента-предка. Для цього викличемо редактор Image Editor командою Tools / Image Editor. У редакторі викличемо команду File / New / Component Resource File (*. dcr). Після команди Resource / New / Bitmap з'явиться діалог, в якому пропонується розмір піктограми 32х32. Ці розміри за умовчанням слід змінити на 24х24 – такий розмір зобов'язані мати піктограми компонентів! Після натискання кнопки OK слід намалювати будь-яке зображення за допомогою стандартних інструментів, схожих на інструменти редактора Paint. Пам'ятайте, що колір лівого нижнього пікселя є кольором маски – даний колір буде «прозорим».


Після цього необхідно перевизначити ім'я ресурсу з піктограмою, за замовчуванням його ім'я – Bitmap1. Нове ім'я ресурсу зобов'язана співпадати з ім'ям класу – у нашому випадку TButtonBeep.

 

  • натисніть кнопку Compile. Після компіляції пакета з'явиться повідомлення, що компонент TButtonBeep був інстальований у палітрі компонентів. Тепер піктограма компонента відповідає намальованою.

  • Даний приклад слід розглядати як тестове вправу. Перед написанням нового компоненту необхідно подивитися, чи існують аналогічні серед вільно розповсюджуваних компонентів. Є практично будь-які кнопки: прозорі, що втікають, круглі, кольорові і т.д. Приблизно так само йде справа з іншими компонентами – нащадками одного класу. Тому найчастіше доводиться реалізовувати компоненти, що складаються з декількох елементів управління.


    Таким чином, у даному випадку ми вивчили застосування переписування методів для створення нових компонентів.


    Створення складного компонента


    Припустимо, у додатку необхідно ввести список прізвищ клієнтів. У тому ж самому додатку буде потрібно і введення списку телефонів. Введення списку – досить поширена операція, тому слід подумати про реалізацію його у вигляді компонента.


    Для введення нового елемента в список буде потрібно редактор – компонент TEdit. Далі користувач повинен мати можливість переглянути список – знадобиться компонент TListBox. Крім того, будуть потрібні команди для занесення поточного значення з TEdit до списку, редагування вибраного елементу списку і його видалення. Простіше за все ці команди реалізувати за допомогою кнопок. Для спрощення завдання помістимо на форму одну кнопку, при натисканні якої будемо додавати вміст компонента TEdit до списку.


    Отже, ми повинні створити новий компонент, який включав би в себе TEdit, TListBox і TButton. Як завжди, почнемо його створення з команди Component / New Component. Після цього з'являється діалог, в якому слід визначити клас-предок, ім'я класу, ім'я модуля. З ім'ям класу і ім'ям модуля ніяких складнощів не виникає, а от ім'я класу-предка неясно. У нас є три елементи управління. Загальним класом-предком для них є TWinControl. Але якщо в якості класу-предка вибрати його, нас очікує дуже тривала і виснажлива реалізація коду TButton, TEdit і TListBox. У таких випадках необхідно в якості класу-предка вибирати компонент, здатний бути «татом» по відношенню до інших компонентів. Серед стандартних компонентів, що постачаються разом з Delphi, таких три: TPanel, TGroupBox, TScrollBox. Виберемо в якості класу-предка панель, але не сам компонент TPanel, а клас TCustomPanel. Переваги вибору TCustomPanel перед TPanel ми обговоримо нижче.


    Назвемо новий клас ім'ям TListAdd і натиснемо кнопку Install. Після вибору пакета компонент буде встановлений в палітру, звідки його можна помістити на форму новоствореного додатки. Це зручно, оскільки при компіляції проекту модуль компонента також буде компілюватися і при наявності помилок компілятор видасть повідомлення.


    Було б зручно помістити наші елементи управління на будь-яку форму і потім створити з них компонент. У стандартному постачанні Delphi такий експерт відсутня. Тому необхідно буде створювати компоненти самим і розміщувати їх на панелі. Створення елементів управління – TButton, TEdit і TListBox – розумно виконати в конструкторі TCustomPanel, для чого, очевидно, необхідно його переписати. Розмістимо поки елементи управління в квадраті 100х100. Координати їх також необхідно визначати в конструкторі. При цьому слід мати на увазі, що після відпрацювання конструктора будь-якого елемента керування він ще не має батька, тобто не знає, щодо якого вікна йому треба відраховувати координати лівого верхнього кута. Спроба змінити координати дочірнього вікна, у якого відсутня батько, негайно призведе до генерації виключення. Тому першим оператором після виклику конструктора елемента керування буде призначення йому батька, в якості якого виберемо TCustomPanel. Її ж зробимо і їх власником, в цьому випадку не знадобиться переписувати деструктор.


    Отже, в секції uses додаємо модуль StdCtrls, де знаходяться описи класів TEdit, TButton і TListBox, а в секції private визначаємо три змінні:

    private
        FEdit:TEdit;
        FListBox:TListBox;
        FButton:TButton;

    У секції public оголошуємо заголовок конструктора з обов'язковою директивою override:

    constructor Create(AOwner:TComponent); override; 

    Реалізуємо конструктор в секції реалізації:

    constructor TListAdd.Create(AOwner:TComponent);
    begin
      inherited Create(AOwner);
      FButton:=TButton.Create(Self);
      FButton.Parent:=Self;
      FButton.Left:=5;
      FButton.Top:=5;
      FButton.Width:=40;
      FButton.Height:=25;
     
      FEdit:=TEdit.Create(Self);
      FEdit.Parent:=Self;
      FEdit.Left:=50;
      FEdit.Top:=5;
      FEdit.Width:=45;
      FEdit.Height:=25;
     
      FListBox:=TListBox.Create(Self);
      FListBox.Parent:=Self;
      FListBox.Left:=5;
      FListBox.Top:=35;
      FListBox.Width:=90;
      FListBox.Height:=60;
    end;

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


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


    Перший недолік, який кидається в очі, – неадекватна поведінка елементів управління при масштабуванні компонента. При зміні його розмірів розміри і положення елементів не змінюються. Крім того, компонент можна зробити маленьким, так що три елементи управління не вмістяться на ньому. І нарешті, при установці компонента на форму з палітри компонентів простим клацанням миші його розміри також залишають бажати кращого.


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

    Width:=100;
    Height:=100;

    Потім потрібно поліпшити поведінку компоненту при масштабуванні. Для цього необхідно отримати повідомлення про те, що розміри змінилися. При зміні розміру якого-небудь елемента керування система посилає йому повідомлення WM_SIZE. Це повідомлення необхідно перехопити. Для цього в секції private опишемо заголовок перехоплювача повідомлення:

        procedure WMSize(var Message:Tmessage); message WM_SIZE;  

    і в секції реалізації реалізуємо його обробник:

    procedure TListAdd.WMSize(var Message:TMessage);
    begin
      inherited;
      if Width<100 then Width:=100;
      if Height<100 then Height:=100;
      FEdit.Width:=Width-55;
      FListBox.Width:=Width-10;
      FListBox.Height:=Height-40;
    end;

    Перший оператор – виклик обробника WM_SIZE за замовчуванням (inherited). Після його виклику у властивостях Width і Height будуть знаходитися нова ширина і висота панелі. Після цього визначаються мінімальні розміри компонента, в даному випадку – 100х100. Якщо розмір по горизонталі або вертикалі менше мінімального, то йому присвоюється мінімальне значення. Потім відбувається масштабування елементів управління так, щоб вони заповнювали всю панель з невеликими відступами. Скомпілювавши компонент через редактор пакетів, можна вже на етапі розробки відзначити коректне поводження елементів управління на панелі при масштабуванні, а також те, що розмір компоненти не можна зробити менш ніж 100х100.


    Тепер корисно буде запустити весь проект на виконання, спробувати вводити дані в однорядковий редактор документів і натискати кнопку. При цьому нічого в список не додається. І не дивно, що ніде в нашому компоненті не вказано, що треба робити при натисканні кнопки. Для того щоб зробити обробник події, пов'язаного з натисканням кнопки, можна поступити, як при написанні компонента TbuttonBeep, то тобто визначити новий клас – нащадок TButton і переписати метод Click. Однак визначення нового класу вимагає системних ресурсів (розмножуються віртуальні методи). Якщо ми відзначимо компонент на формі і подивимося на інспектор об'єктів, то виявимо, що компонент TlistAdd експонує трохи властивостей і жодної події, в тому числі жодного обробника події кнопки OnClick. Тому те, що в минулій главі ми відкинули як неправильний метод, – перевизначення обробника кнопки OnClick в даному випадку застосовано, оскільки програміст не може в інспекторі об'єктів призначити новий обробник. Отже, в секції private описуємо заголовок нового методу:

        procedure BtClick(Sender:TObject); 

    У реалізації конструктора TListAdd присвоюємо цей обробник обробнику подій FButton.OnClick:

        FButton.OnClick:=BtClick; 

    І нарешті, реалізуємо метод BtClick:

    procedure TListAdd.BtClick(Sender:TObject);
    begin
      if length(FEdit.Text)>0 then begin
        FListBox.Items.Add(FEdit.Text);
        FEdit.Text:=””;
        FEdit.SetFocus;
      end;
    end;

    Спочатку перевіримо, не порожній чи однорядковий редактор: ми не будемо додавати в список порожні рядки. Потім переносимо вміст редактора в список (FListBox.Items.Add (FEdit.Text);) і готуємо редактор вводити наступне значення – а саме, очищаємо його від тексту (який вже перенесено до списку) і переносимо на нього фокус вводу. Тепер після компіляції і запуску програми можна переконатися, що воно працює коректно – при натисканні кнопки вміст редактора переноситься до списку.


    Додавання властивостей і методів


    Якщо поруч з компонентом TListAdd помістити компонент TPanel і порівняти показуване в інспектора об'єктів, то можна відзначити, що для панелі експонується досить велику кількість властивостей і подій, а для TListAdd – тільки декілька властивостей. Тим часом клас TCustomPanel є предком обох компонентів. Для того щоб зрозуміти причину, відкриємо модуль ExtCtrls.pas і розглянемо різницю між класами TCustomPanel і TPanel. Можна відзначити, що всі методи і змінні, які забезпечують функціональність панелі, визначені на рівні класу TCustomPanel. У ньому ж визначено й властивості, які потім відображаються в інспекторі об'єктів для TPanel, тільки ці властивості визначені в секції Protected. Реалізація ж класу TPanel надзвичайно проста: як предка визначається TCustomPanel, і властивості цього класу редекларіруются, але вже в секції published. Стає зрозуміло, що необхідно зробити в класі TListAdd для появи в інспекторі об'єктів властивостей і методів класу TcustomPanel, а саме редекларіровать властивості. У секції published класу TListAdd запишемо:

        property Align;
        property OnMouseDown;

    При редеклараціі властивості не потрібно вказувати його тип і посилатися на змінні або методи читання або запису властивості. Після компіляції компонента через редактор пакетів в інспектора об'єктів можна спостерігати поява властивості Align та події OnMouseDown. Таким чином, для нащадків TCustom …-класів програміст має можливість вибирати, які властивості і події слід відображати в інспектора об'єктів, а які ні. Саме з цієї причини TCustom …-класи рекомендується використовувати в якості предків для створення компонентів.


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

    property BtCaption: string read FButton.Caption write FButton.Caption; 

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

    function GetBtCaption:string;
    procedure SetBtCaption(const Value:string);

    У секції published оголосимо властивість BtCaption:

    property BtCaption: string read GetBtCaption write SetBtCaption; 

    І нарешті, реалізуємо два оголошених методу в секції реалізації:

    function TListAdd.GetBtCaption:string;
    begin
      Result:=FButton.Caption;
    end;
     
    procedure TListAdd.SetBtCaption(const Value:string);
    begin
      FButton.Caption:=Value;
    end;

    Після компіляції компонента за допомогою редактора пакетів в інспектора об'єктів з'являється нова властивість. Зміна значення цієї властивості відображається прямо на етапі розробки.


    Тепер визначимо нову подію. У цьому завданню було б розумним створити подія, що дозволяє програмістові, що використовує такий компонент, аналізувати текст перед занесенням вмісту редактора в список і дозволити або заборонити додавання тексту до списку. Отже, цей метод зобов'язаний як параметр містити поточне значення тексту у редакторі і залежати від логічної змінної, якій програміст може присвоїти значення True або False. Крім того, будь-обробник події в компоненті зобов'язаний залежати від параметра Sender, в якому викликає його компонент передає посилання на самого себе. Це необхідно робити тому, що в середовищі розробки Delphi один і той же обробник події може викликатися з кількох різних компонентів і програміст повинен мати можливість проаналізувати, який саме компонент викликав обробник. Отже, після слова type в секції interface перед визначенням TListAdd визначаємо новий тип методу:

    type
      TFilterEvent=procedure(Sender:TObject; const EditText:string;
    var CanAdd:boolean) of object;

    Далі визначаємо змінну цього типу в секції private:

        FOnFilter:TFilterEvent; 

    І в секції published визначаємо властивість даного типу:

    property OnFilter: TFilterEvent read FOnFilter write FOnFilter; 

    При визначенні нової властивості посилаємося на змінну FOnFilter, а не на методи – вони тут не потрібні. Тепер, якщо скомпілювати компонент за допомогою редактора пакетів, можна виявити появу в інспекторі об'єктів події OnFilter. Проте якщо ми призначимо йому обробник і запустимо проект на виконання, то він може не викликатися. Це відбувається тому, що ми ніде його не викликали в нашому компоненті. Відповідне місце для виклику події OnFilter – обробник події OnClick для FButton, який вже реалізований. Тому ми змінимо код реалізації раніше визначеного методу BtClick:

    procedure TListAdd.BtClick(Sender:TObject);
    var
      CanAdd:boolean;
    begin
      if length(FEdit.Text)>0 then begin
        CanAdd:=True;
    if Assigned (FOnFilter) then FOnFilter (Self, FEdit.Text, CanAdd);
        if CanAdd then begin
          FListBox.Items.Add(FEdit.Text);
          FEdit.Text:=””;
          FEdit.SetFocus;
        end else beep;
      end;
    end;

    Отже, у наведеному вище фрагменті коду визначається логічна змінна CanAdd. При написанні коду, варто враховувати, що програміст може не зробити обробник події OnFilter. Тому встановлюємо значення змінної CanAdd за замовчуванням рівним True – всі рядки додавати до списку. Далі, перед викликом FonFilter, слід перевірити, а чи зробив програміст обробник події. Це досягається викликом методу Assigned, який повертає логічне значення. Для покажчика виклик методу Assigned еквівалентний перевірці P <> nil. Для методу об'єкта ми не можемо використовувати перевірку FOnFilter <> nil, так як метод об'єкта характеризується двома адресами і така перевірка не буде дозволена компілятором. Але виклик методу Assigned чудово перевіряє, чи був зроблений обробник події. Вищенаведений код – Абсолютно стандартний спосіб виклику обробника подій з компонента.


    Залишилося протестувати обробник події. Помістимо два компоненти TListAdd на форму, для одного дозволимо додавання тільки цілих чисел, а для іншого – тільки слів, що починаються з великої англійських букв. Відповідно код для обробників подій OnFilter буде виглядати наступним чином:

    procedure TForm1.ListAdd1Filter (Sender: TObject; const EditText: String;
      var CanAdd: Boolean);
    var
      I,N:integer;
    begin
      Val(EditText,N,I);
      CanAdd:=I=0;
    end;
     
    procedure TForm1.ListAdd2Filter (Sender: TObject; const EditText: String;
      var CanAdd: Boolean);
    begin
      CanAdd:=False;
    if length (EditText)> 0 then CanAdd: = (EditText [1]> = "A") and (EditText [1] <= "Z");
    end;


    Код простий для розуміння, єдиним його нюансом є перевірка того, що текст являє собою не порожній рядок, перед перевіркою першої літери тексту в обробнику події ListAdd2Filter. Проведення такої перевірки обов'язково: рядки в Object Pascal – це об'єкти, і порожній рядку відповідає nil-покажчик. При спробі перевірити першу літеру порожнього рядка додаток спробує дереференсіровать nil, що призведе до виникнення виключення. У даному випадку це не страшно: перед викликом обробника подій FOnFilter з компонента TListAdd перевіряється рядок на ненульову довжину. Однак для компонентів, вихідний текст яких вам недоступний, така перевірка є обов'язковою!


    Приховування властивостей в інспекторі об'єктів


    Припустимо, ви робите компонент для доступу до даних, наприклад, нащадок класу TTable. Припустимо, в цьому компоненті аналізується список таблиць, що є в базі даних, і за якими-небудь ознаками (наприклад, наявність поля певного типу і з певним ім'ям) вибирається одна для роботи. Для нормальної роботи компонента ім'я цієї таблиці має заноситися в властивість TableName. Але ця властивість відображається в інспектора об'єктів! Програміст, який використовує цей компонент, може змінити його значення на етапі розробки, що, припустимо, зробить компонент непрацездатним. І він буде правий! Якщо якісь з властивостей або подій не можна змінювати, вони повинні бути приховані.


    Ми продовжимо роботу над компонентом TListAdd і в якості модельного завдання приберемо з інспектора об'єктів властивість Cursor. Це властивість визначено в секції published в класі TСontrol і відображається в інспектора об'єктів для TListAdd з самого початку розробки компонента. Виходячи з цього можна спробувати перевизначити дане властивість в секції protected. Компілятор дозволить таке перевизначення, але до бажаного результату це не приведе: властивість Cursor як було, так і залишиться в інспекторі об'єктів … Будь-яке властивість, будучи якось визначеним у секції published, буде завжди відображатися в інспекторі об'єктів для всіх нащадків даного класу.


    Щоб приховати властивості з інспектора об'єктів використовуємо дві можливості компілятора Delphi, а саме:



    1. При оголошенні нового властивості з ім'ям, що збігається з назвою вже наявного властивості, раніше певну властивість «затінюється».

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

    Перед початком роботи по приховані властивості Cursor корисно видалити компоненти TListAdd з форми, інакше може відбутися виключення при читанні ресурсу форми. Отже, в секції private оголошуємо змінну FDummy: integer (Ім'я і тип змінної можуть бути будь-якими) і в секції published визначаємо нову властивість:

    property Cursor:integer read FDummy;  

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


    Тепер трохи ускладнити завдання. Припустимо, необхідно, щоб курсор був показаний не у вигляді стрілки, а у вигляді пісочного годинника (crHourGlass). Для того щоб змінити значення властивостей за замовчуванням, нове значення необхідно присвоїти змінної в конструкторі. При спробі в конструкторі присвоїти нове значення Cursor

        Cursor:=crHourGlass; 

    компілятор Delphi видасть діагностичне повідомлення про те, що не можна призначити нове значення змінної, призначеної тільки для читання. Якщо зробити нову властивість «тільки для запису", то компілятор видасть вже інше діагностичне повідомлення – про непорівнянних типи даних. Якщо ж оголосити змінну FDummy: TCursor і зробити її доступною тільки для запису, то компілятор дозволить це присвоєння, але при цьому вигляд курсору не зміниться: він як і раніше буде стрілкою.


    Тривіальне вирішення даної проблеми – оголосити клас-нащадок TCustomPanel, в конструкторі якого потрібно присвоїти нове значення змінної Cursor, а від нього вже виробляти наш компонент TListAdd. У такого рішення є два недоліки:



    1. Воно ресурсномісткою – розмножуються віртуальні методи.

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

    Тому вирішення даної задачі виглядає наступним чином: у конструкторі TListAdd оголошуємо оператор:

        inherited Cursor:=crHourGlass; 

    і все! Цього достатньо для зміни курсору.


    Раніше ми користувалися службовим словом inherited тільки для виклику методу предка. Дана конструкція дозволяє глибше зрозуміти значення inherited як звернення до класу-предка. Можна звертатися і до властивостей, і до методів. При зверненні до властивості його можна як читати, так і присвоювати йому нового значення; при цьому службове слово inherited стоїть ліворуч від знака присвоювання. Аналогічно можна викликати приховані методи предка. Звернення за ієрархією вище, ніж клас-предок, заборонено – конструкція

        inherited inherited Cursor:=crHourGlass; 

    не буде скомпільована.


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


    Використання Hook-процедур для створення компонентів


    Раніше вже згадувалося, що кожен нащадок TWinControl має процедуру, яка приймає і обробляє повідомлення. Якщо є посилання на дескриптор вікна (HWND), то можна визначити адресу цієї процедури і, що більш важливо, підмінити цю адресу і таким чином обробляти одержувані повідомлення своїм способом. Як правило, ніхто не пише повністю обробники всіх повідомлень; частіше викликається старий метод за замовчуванням. При цьому нова процедура використовується як фільтр: при надходженні якого-небудь події виконується код. Фактично це «шпигун» в TwinControl: нас повідомляють про прихід якого-небудь повідомлення і можна виконати якийсь код. При правильній реалізації Hook-процедури TWinControl продовжує працювати як завжди, не підозрюючи, що своїми повідомленнями він ділиться з кимось ще.


    Hook-процедура визначається наступним чином:

    procedure(var Message:TMessage) of object; 

    Вона залежить від змінної типу TMessage, в якій міститься вся інформація про повідомлення. Але визначити цю процедуру – недостатньо. Вона повинна копіюватися для кожного TWinControl, до якого буде приєднана. Це досягається викликом WinAPI-методу MakeObjectInstance. Як параметр цей метод приймає метод об'єкта, робить його копію в пам'яті і повертає адресу нового методу. Зрозуміло, що при цьому резервуються системні ресурси, які необхідно повернути системі. Це досягається викликом методу FreeObjectInstance.


    Ще одна важлива умова: перед руйнуванням TWinControl повинна бути відновлено зв'язок зі старою процедурою обробки повідомлень, інакше ресурси не будуть повернуті системі. Значить, доведеться запам'ятовувати покажчик на стару процедуру, яку можна дізнатися викликом методу Win API GetWindowLong з параметром GWL_WNDPROC. Цей покажчик буде використовуватися також для виклику обробників подій TWinControl за замовчуванням. Зворотний метод – SetWindowLong – використовується для установки Hook-процедури.


    Отже, сформулюємо завдання для наступного вправи. Припустимо, ми хочемо створити компонент, який змушуватиме пищати при натисканні кнопки миші інші компоненти – нащадки TWinControl. Зрозуміло, що даний компонент не слід показувати під час виконання програми, тому в якості його класу-предка виберемо TComponent. Ім'я класу визначимо як TBeepWnd. У секції private визначимо три змінні:

        FOldProc,FNewProc:pointer;
        FControl:TWinControl;

    З назв ясно, що ми будемо запам'ятовувати посилання на стару процедуру у змінній FOldProc, посилання на нову процедуру (після виконання методу MakeObjectInstance) буде зберігатися у змінній FNewProc. І у змінній FControl будемо зберігати посилання на елемент управління, на який в даний момент «повішена» Hook-процедура. Визначимо три методи в цій же секції:

    procedure HookProc(var Message:TMessage);
    procedure HookWindow(W:TWinControl);
    procedure UnhookWindow;

    і в секції implementation реалізуємо їх:

    procedure TBeepWnd.HookProc(var Message:TMessage);
    begin
      case Message.Msg of
        WM_LBUTTONDOWN:begin {Our task}
          Beep;
    Message.Result: = CallWindowProc (FOldProc, FControl.Handle, Message.Msg,
    Message.WParam, Message.lParam);
        end;
    WM_DESTROY: begin {When window is about destroying, remove hook}
    Message.Result: = CallWindowProc (FOldProc, FControl.Handle, Message.Msg,
    Message.WParam, Message.lParam);
          UnhookWindow;
        end;
        {Call default handler}
    else Message.Result: = CallWindowProc (FOldProc, FControl.Handle, Message.Msg,
    Message.WParam, Message.lParam);
      end;
    end;

    У самій Hook-процедурі перехоплюється повідомлення, на яке відбувається реакція – WM_LBUTTONDOWN. Крім того, будь-яка Hook-процедура зобов'язана обробляти повідомлення WM_DESTROY. Це останнє повідомлення, яке передається вікна перед тим, як вона буде зруйнована. Наша реакція – відновити попередній метод викликом описаного нижче методу UnhookWindow. І нарешті, скрізь викликаються обробники повідомлень за замовчуванням за допомогою методу CallWindowProc. Забути обробник події за замовчуванням – те ж саме, що забути inherited в обробнику події, в 80% випадків це приведе до некоректного поводження додатки. Ні в жодному разі не можна забувати привласнювати результат виклику методу CallWindowProc полю Result змінної Message! Код в цьому випадку працювати не буде!

    procedure TBeepWnd.HookWindow(W:TWinControl);
    begin
    if csDesigning in ComponentState then begin {Checking if component at design
    or run-time}
        FControl:=W;
        Exit;
      end; 
    if FControl <> nil then UnhookWindow; {Remove hook if it was previously installed}
      if W<>nil then begin
        FOldProc:=pointer(GetWindowLong(W.Handle,GWL_WNDPROC));
    {Determines address of old procedure}
    FNewProc: = MakeObjectInstance (HookProc); {Make copy in memory}
        SetWindowLong(W.Handle,GWL_WNDPROC,integer(FNewProc)); 
    {Set new procedure}
      end;
      FControl:=W;               {Store reference at control}
    end;

    Цей метод використовується для встановлення нової процедури обробки повідомлень. Спочатку перевіряється, на якому з етапів знаходиться даний компонент: на етапі розробки або на етапі виконання. Якщо компонент знаходиться на етапі розробки, тобто виставлений прапор csDesigning у властивості ComponentState, то зберігається просто посилання на компонент без установки Hook-процедури. Це зроблено для того, щоб уникнути установки Hook-процедури на середу розробки Delphi. Якщо раніше ця процедура була встановлена на іншому елементі управління, вона знімається за допомогою виклику методу UnhookWindow. Після цього запам'ятовується адресу старої процедури (GetWindowLong), робиться копія в пам'яті нової процедури (MakeObjectInstance) і виставляється адресу нової процедури (SetWindowLong). Використовується приведення типів від integer до pointer, і навпаки – викликаються методи вимагають (або повертають) змінні не зовсім підходять типів. І нарешті, посилання на елемент управління запам'ятовується у змінній FControl, яку ми визначили в секції private.

    procedure TBeepWnd.UnhookWindow;
    begin
    if (FControl = nil) or (FOldProc = nil) or (FNewProc = nil) then Exit;
    {No hook was installed}
      SetWindowLong(FControl.Handle,GWL_WNDPROC,integer(FOldProc));
    {Set old window procedure}
      FreeObjectInstance(FNewProc); {Free resources}
      FControl:=nil;           {Initiate variables}
      FOldProc:=nil;
      FNewProc:=nil;
    end;

    Даний метод відновлює старий обробник події. Він викликається з методу HookProc і повинен ще викликатися з деструктора компонента – знімати Hook необхідно як при руйнуванні вікна, так і при руйнуванні даного компонента. Метод SetWindowLong c адресою старого методу відновлює старий обробник повідомлень. Після цього слід повернути ресурси системі викликом методу FreeObjectInstance.


    Отже, базові методи для роботи з Hook-процедурою визначені. Тепер необхідно переписати деструктор, щоб Hook-процедура знімалася при руйнуванні даного компонента:

    destructor TBeepWnd.Destroy;
    begin
      UnhookWindow;
      inherited Destroy;
    end;

    І нарешті, в секції published визначимо властивість, яке буде відображатися в інспекторі об'єктів:


    property Control:TWinControl read FControl write HookWindow;


    Для установки нового компонента посилаємося на раніше визначений метод, який під час виконання програми негайно «повісить» Hook-процедуру на компонент, який стане пищати при натисканні кнопки. Нагадаємо, що замість оператора Beep можна написати будь-який виконуваний код.


    Тестується компонент досить просто: ставиться на форму, на яку ставляться і кілька компонентів-нащадків TWinControl. Після вибору на тлі компонента TBeepWnd при клацанні мишею в інспекторі об'єктів на полі Control розгортається список, в якому присутні всі визначені на формі TWinControl. Слід вибрати один з них і запустити програму. При натисненні лівої кнопки миші на обраному компоненті він видає писк.


    Редактори властивостей і редактори компонентів


    Все, про що розповідалося в попередніх розділах, ставиться до створення коду програми, яке буде поширюватися для користувачів. Проте середовище розробки Delphi дозволяє модифікувати саму себе. Для цього не потрібно знань спеціальної мови, оскільки всі методи для зміни середовища розробки пишуться на Delphi. Тут ці методи, а саме редактори властивостей і редактори компонентів, розглянуті частково – в плані створення інструментів для роботи з компонентами. При читанні матеріалів даного розділу слід чітко розуміти, що кінцевий користувач, який працює з вашим застосуванням, ніколи не побачить ні редактора властивостей, ні редактора компонентів – вони створюються для програмістів і працюють тільки в середовищі розробки Delphi.


    Редактори властивостей


    Під час розробки програми властивості відображаються в інспектора об'єктів. Зверніть увагу: властивості в інспекторі об'єктів редагуються по-різному. Деяким властивостями (Width, Caption) можна визначити тільки нове текстове значення. Властивість типу Cursor надає список, що розкривається, клацнувши по якому можна вибрати значення. Властивість типу TFont має знак «+» ліворуч; при клацанні по ньому воно розгортається, даючи можливість модифікувати окремі поля. Крім того, справа є кнопка з трьома крапками (elliptic button), при натисканні на якій з'являється діалог редактора властивостей.


    Кожне з перерахованих вище властивостей має свій редактор, і великою перевагою середовища розробки Delphi є можливість створити свої редактори властивостей. Нові редактори властивостей досить часто зустрічаються серед розповсюджуваних компонентів. Але до них треба ставитися обережно: спочатку виконати тести на комп'ютері, де при необхідності можна повторно інсталювати Delphi. Як правило, вони створюються кваліфікованими програмістами і претензій до коду не буває, але часто забувають включити до розповсюджуваний редактор властивостей будь-яку DLL. Після інсталяції такого редактора ми отримуємо ряд властивостей, які неможливо редагувати, – старий редактор перекритий, а новий не працює …


    Перед створенням нового редактора властивостей має сенс подумати, чи варто це робити, – серед стандартних редакторів, ймовірно, можна знайти підходящий. Якщо ж доведеться робити редактор властивостей, необхідно дотримуватися правило: слід уникати створення редакторів для стандартних типів даних (integer, string та ін.) Інші програмісти звикли до стандартних редакторам, і ваш може їм не сподобатися. Отже, доведеться проявити скромність і реєструвати редактор для свого класу, а не для класу TComponent. Якщо ваш редактор властивостей сподобається програмістам, більшість з них зможуть самі змінити реєстрацію так, щоб редактор працював для всіх компонентів. Питання реєстрації редактора ми обговоримо нижче.


    Отже, поставимо модельну задачу, для реалізації якої необхідно буде реалізувати редактор властивостей. Припустимо, будь-який компонент має властивість – день тижня. У принципі, для введення дня тижня можна скористатися стандартним редактором з списком, що випадає. Однак ми хочемо, щоб програміст на етапі розробки міг вводити день тижня, задаючи або його порядковий номер (1 – понеділок, 2 – вівторок і т.д.), або текст на національному або англійською мовою. При введенні тексту допускається змішання прописні і рядкові букв.


    Перш за все необхідно створити компонент, в якому буде зберігатися день тижня. Створимо новий компонент викликом команди Component / New component. Як класу-предка виберемо TComponent і дамо новому класу ім'я TDayStore. Після цього встановимо компонент в палітру. Тепер треба вирішити, в якому вигляді зберігати день тижня. Ясно, що для однозначної ідентифікації та економії ресурсів його слід зберігати в вигляді цілого числа з допустимими діапазонами 1-7. Однак, якщо ми зібралися створювати редактор властивостей, слід згадати правило про нестворення нових редакторів для вже наявних типів. Тому визначимо новий тип – TDayWeek, причому всі операції з ним будемо робити як з цілими числами. Визначимо змінну FDay в секції private компонента. Оскільки ця змінна ініціалізується значенням 0 при відпрацюванні конструктора за замовчуванням, а це число знаходиться за межами допустимих значень, необхідно переписати конструктор. На закінчення визначимо властивість DayWeek в секції published для відображення його в інспектора об'єктів. Остаточний варіант компонента виглядає наступним чином:

    type
      TDayWeek=type integer;
     
      TDayStore = class(TComponent)
      private
        { Private declarations }
        FDay:TDayWeek;
      protected
        { Protected declarations }
      public
        { Public declarations }
        constructor Create(AOwner:TComponent); override;
      published
        { Published declarations }
        property DayWeek:TDayWeek read FDay write FDay;
      end;

    implementation
     
    constructor TDayStore.Create(AOwner:TComponent);
    begin
      inherited Create(Aowner);
      FDay:=1;
    end;

    Слід звернути увагу на рідкісну конструкцію визначення нового типу

    TDayWeek=type integer;   

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


    Тепер створимо редактор властивості TDayWeek. Для цього до наявного проекту додамо нову форму, запам'ятаємо її під будь-яким підходящим ім'ям (DayPropE.pas) і виключимо з проекту. Після цього відкриємо форму як окремий файл і будемо реалізовувати в ній редактор властивостей. На першому етапі форма нам не знадобиться, але пізніше ми реалізуємо на ній діалог.


    Модуль для створення редакторів властивостей називається DsgnIntf.pas (Design Interface), у ньому визначені базовий клас TPropertyEditor і класи-нащадки, призначені для редакції стандартних властивостей – TIntegerProperty, TFloatProperty, TStringProperty та ін Механізм роботи редакторів властивостей полягає в наступному:



    1. Він реєструється в середовищі розробки Delphi викликом методу RegisterPropertyEditor. Як параметри цей метод приймає такі значення:

      a) інформація про тип властивостей, для редакції яких призначений даний редактор. Через наявність цієї інформації нам довелося визначати новий тип TDayWeek;


      b) інформація про компонент, в якому застосуємо даний редактор. Редактор буде викликатися не тільки для зазначеного компоненту, але і для всіх його нащадків. Якщо встановити це значення TComponent, редактор буде викликатися для будь-якого компонента;


      c) ім'я властивості, для якого використовується даний редактор. Якщо ім'я – порожній рядок, використовуються два вищезгаданих фільтру;


      d) посилання на сам клас, що описує новий редактор властивостей, – середовище розробки повинна знати, що викликати.


    2. Викликається метод GetValue, коли необхідно вважати поточне значення властивості з компонента. Цей метод для будь-якої властивості повертає рядок, що міститься в інспектора об'єктів.

    3. Викликається метод SetValue, коли програміст ввів нове значення властивості в інспекторі об'єктів. Як параметр передається новий рядок. У методі вона повинна бути проаналізована і приведена до типу редагованого властивості.

    Методи GetValue і SetValue є віртуальними, при їх переписуванні створюються нові редактори властивостей. Отже, тепер можна почати створення нового редактора властивостей.


    Посилаючись в секції uses модуля DayPropE.pas на модуль DsgnIntf і визначимо в секції Interface новий клас:

    type
      TDWPropED=class(TPropertyEditor)
      public
        function GetValue:string; override;
        procedure SetValue(const Value:string); override;
      end;

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

    const
    DayWeek: array [1 .. 7] of string = ("Понеділок", "Вівторок", "Середовище", "Четвер",
    "П'ятниця", "Субота", "Неділя");
    DayWeekEn: array [1 .. 7] of string = ("Monday", "Tuesday", "Wednesday", "Thursday",
    "Friday", "Saturday", "Sunday");
     
    function TDWPropED.GetValue:string;
    begin
      Result:=DayWeek[GetOrdValue];
    end;
     
    procedure TDWPropED.SetValue(const Value:string);
    var
      I,N:integer;
    begin
      I:=1;
      {Checking if programmer has entered name of the day of week
    with national language}
    while (ANSICompareText (DayWeek [I], Value) <> 0) and (I <8) do Inc (I);
     
      {Checking if programmer has entered name of the day of week
    with English language}
      if I>7 then begin
        I:=1;
    while (ANSICompareText (DayWeek [I], Value) <> 0) and (I <8) do Inc (I);
      end;
      if I<8 then begin
        SetOrdValue(I);
        Exit;
      end;
     
    {Checking if programmer has entered order of the day of week}
      Val(Value,N,I);
      if (N>0) and (N<=7) and (I=0) then begin
        SetOrdValue(N);
        Exit;
      end;
     
    {Inform Delphi, that bad value was entered so that restore previous value}
    raise Exception.Create (Format ("Bad day of week% s", [Value]));
    end;

    Усередині редактора властивостей TPropertyEditor визначено метод GetOrdValue, який витягує поточне значення властивості з компонента, якщо воно може бути описано як ординарний тип (тобто тип, що дозволяє впорядковувати змінні, до нього пов'язані). У методі GetValue викликається цей метод, але в якості рядка повертається назва дня тижня, вилучене з масиву. Ця назва і потрапляє в інспектор об'єктів.


    Реалізація методу SetValue виглядає складніше, оскільки поставлена задача досить складна. Спочатку перевіряється, чи дійсно новий текст, набраний в інспекторі об'єктів та передається в даний метод через параметр Value, представляє собою назву дня тижня. Оскільки назва дня тижня вводиться національним алфавітом, для порівняння рядків використовується метод ANSICompareText. Цей метод порівнює рядки без урахування великих і малих літер, причому для конвертації малих літер у прописні використовується поточний мовної драйвер. Потім, якщо Value не виявлено у масиві російських назв днів тижня, пошук здійснюється за англійськими назвами. Якщо назва співпало, то викликається метод SetOrdValue класу TPropertyEditor. Цей метод дозволяє змінити поточне значення ординарного властивості в компоненті. Якщо ж текстове назва дня тижня не було знайдено ні в одній з мов, то перевіряється, чи був введений порядковий номер дня тижня, який може бути цілим числом в діапазоні від 1 до 7. Для цього використовується метод val, який намагається конвертувати рядок в ціле (або дійсний) число і повертає код помилки. Якщо ж значення параметра Value не збігається ні з одним із зумовлених значень, значить програміст помилився при введенні нового значення властивості. Про це необхідно інформувати середовище розробки, що здійснюється генерацією винятку.


    Створений таким чином редактор властивостей залишиться непрацездатним, поки не буде зареєстрований в середовищі розробки. Як вже говорилося, це досягається за допомогою методу RegisterPropertyEditor, який зобов'язана викликати середовище розробки Delphi. Метод, що викликається середовищем розробки, – процедура Register при реєстрації компоненту. Тому в модулі DayStore (де реалізовано компонент TDayStore) в секції implementation посилаємося на модулі DsgnIntf і DayPropE. Зверніть увагу, що посилання повинне знаходитися в секції implementation – нам скоро буде потрібно циклічна посилання! І тепер додаємо один метод до процедури Register в модулі DayStore:

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


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

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

    Ваш отзыв

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

    *

    *