Життя і смерть в режимі run-time

Автор: © Олена Філіппова

Матеріал призначений для початківців програмістів, що вміють працювати з компонентами Delphi в режимі design-time але вже не вважають, що програмувати це означає "накликати мишкою форму". Ніяких цікавих моментів для профі стаття не містить, це виключно навчальний матеріал.

Мета статті відповісти на запитання і показати:


І все це – під час роботи програми (режим run-time).

Пояснення до проекту:


Завантажити проект (відкомпілювалися в Delphi 5):


Проект показує відразу всі перераховані можливості роботи з компонентами, він представляє собою форму, на якій є панель (PanelTest: TPanel), керуючі компоненти і меню. Всі наші компоненти, які ми будемо створювати, будуть розташовуватися на панелі PanelTest. Щоб не плутати тестові компоненти з керуючими. Створювати будемо TEdit, TButton, TCheckBox і TLabel за вибором.


Керуючі елементи форми:



Деякі елементи MainMenu і pmComponent будуть добудовуватися і змінювати свої властивості в run-time.






Створюємо компонент.  


Це дуже просто, головне – знати який саме це компонент і де він буде лежати. Наприклад, створимо TButton на нашій панелі PanelTest.

Var New : TButton;
Begin
/ / Створюємо НОВИЙ екземпляр класу TButton
/ / І кладемо посилання на нього в змінну New
New:=TButton.Create(PanelTest);

/ / Координати лівого верхнього кута нової кнопки на панелі
New.Top:= …;
New.Left:=…;

New.Name:=”Button”;

/ / А ось ця процедура буде викликатися
/ / При натисканні на новеньку кнопку
/ / То є – обробка події OnClick
New.OnClick:=OnClickButton;

/ / Оп! І робимо кнопку видимої на PanelTest
New.Parent:=PanelTest;

End;


Коментарі до коду.
Хоча код короткий і досить простий, для "новачків" варто дати деякі пояснення.
Визначення змінної Var New : TButton НЕ ТВОРИТЬ нової кнопки, а лише говорить про те, що у змінній New буде лежати посилання на екземпляр класу TButton. Цей момент треба зрозуміти дуже чітко. Вибачаюсь перед просунутими початківцями за прописні істини, але одержувані мною численні листи змушують звертати увагу на ці дрібниці. Це досить важливі дрібниці.


Примітка:



Раджу прочитати статтю Максима Ігнатьєва "Подорожуючи по TObject. Або як воно працює." Отже, створюємо новий екземпляр класу TButton, а просто кажучи, нашу кнопку:

New:=TButton.Create(PanelTest);

Конструктор має вхідний параметр

TButton.Create( AOwner : TObject );

де зазначено Owner, тобто "господар", створюваного об'єкта. Господар компонента відповідає за його коректне видалення і звільнення пам'яті. В якості господаря ми передаємо йому панель PanelTest, це означає, що при видаленні PanelTest буде вилучено, і наша кнопка. Якщо в якості параметра вказати nil, то і піклуватися про видалення кнопки доведеться самим. Які коментарі можна додати до прозорих командам New.Top:= … ?
Ну ось, наприклад такі: якщо ми створюємо компонент, для якого потрібно задати не тільки координати лівого верхнього кута, але і його ширину і висоту, то замість чотирьох присвоєнь використовуйте метод TControl.SetBounds . Призначення імені для динамічних компонентів зовсім не обов'язково, вони чудово можуть існувати і без імен. Тільки тоді неможливо буде знайти їх методом FindComponent, що, втім, не завжди необхідно. У нашому випадку я призначаю ім'я всіма компонентами виключно від лінощів – присвоювання New.Name:=”Button” автоматично заповнить поле Text для TEdit і поле Caption для TLabel, TButton і TCheckBox і про це можна не піклуватися. Створена кнопка майже готова до самостійно життя, за одним маленьким винятком … її поки не видно.
Подивіться уважніше Help на тему TControl.Parent, це те саме властивість, який нам потрібен. Parent – це візуальний батько контрола. Присвоєння New.Parent: = PanelTest поміщає кнопку New у масив дочірніх контролів для панелі PanelTest.

property Controls[Index: Integer]: TControl;

Тепер кнопка буде не просто видно на панелі, а буде належати цій панелі. Тобто при можливе переміщення PanelTest або зміні її розмірів, наша кнопка буде залишатися всередині PanelTest. Але і це ще не все. Якщо кнопка нам потрібна не для прикраси форми, то хотілося б, щоб вона адекватно реагувала на деякі події, ну хоча б на своє власне натискання.

Читаємо help:


property OnClick: TNotifyEvent;

Це властивість визначає реакцію на подію, яка виникає при кліці мишки або натисненні Enter на контрол.

Ліричний відступ:


Дивимося help за темою "Procedural types", тобто "процедурні типи".
Наприклад тип:

type TMyProcedure = procedure ( I : Integer);

це процедурне тип, який є посиланням (покажчиком) на адресу процедури з певним списком параметрів.

Var MyProcedure : TMyProcedure;

Procedure X( I : Integer);
Begin

End;


MyProcedure:=X;


Змінна MyProcedure містить адресу соответствущее процедури. Таким чином процедуру можна передавати як параметр в інші процедури і функції.

type TNotifyEvent = procedure (Sender: TObject) of object;

Тип TNotifyEvent це не просто покажчик на процедуру, це посилання на метод, про це говорить директива "of object". Чим відрізняється посилання на процедуру від посилання на метод?
Насправді тип "Посилання на метод" реально містить два посилання – безпосередньо адресу методу (процедури) і посилання на сам об'єкт, якому цей метод належить. Для того, щоб призначити свій власний обробник події, ми повинні створити процедуру з параметром Sender: TObject, назвемо її OnClickButton. Процедура ця буде нехитра – видається повідомлення про те, яка саме кнопка натиснута.
Для того, щоб зрозуміти яка кнопка натиснута, звернемося до параметра Sender, який передається нам у процедуру. Sender це той компонент, який ініціює подія, тобто в нашому випадку – натиснута кнопка.

Procedure TForm1.OnClickButton( Sender : TObject );
Var Value : String;
Begin
MessageDlg ("Натиснуто кнопка" + TControl (Sender). Name, mtInformation, [mbOk], 0);
End;

І призначимо цю процедуру в якості обробника події OnClick створеної кнопки:

New.OnClick:=OnClickButton;

Тепер наша кнопка прям як справжня!






Лінь, як джерело натхнення.  


Отже, почнемо.
Компоненти будемо створювати при подвійному кліці мишкою на панелі PanelTest. У реальних прикладах в run-time доводиться додавати на форму РІЗНІ компоненти.
У нашому прикладі будемо створювати не тільки кнопки (TButton), а ще й TLabel, TEdit і TCheckBox.
Радіокнопки rgComponents якраз вказують, що саме ми збираємося створювати.
Можна піти найпростішим шляхом, скористаємося вже розібраним кодом, додамо CASE на всі наші випадки і для кожного класу перепишемо цей код, змінюючи тип створюваного компонента. Найпростіший шлях не завжди самий правильний.
По-перше, якщо Вам знадобиться додати ще один тип, доведеться ще дописати шматок коду. А по-друге, ліниво повторювати один і той самий код сто разів. : О)
Те, що не гарно виглядає, з великим ступенем ймовірності, не зовсім вірно реалізовано.
Справжній програміст, людина досить лінивий … Саме це змушує його оптимізувати процес розробки. Саме з цієї причини з'явився перший компілятор: о)
З цієї ж причини ми підемо іншим шляхом.


Посилання на клас.


Добре б передати Delphi тип компонента, примірник якого ми хочемо створити. Тоді б завдання спростилася.
Формуємо массівчік типів, передаємо черговий елемент масиву і Delphi сама викликає потрібні методи саме того класу, який ми їй дали! Але ж ми можемо це зробити: для цієї мети існують посилання на клас – class references. Подивіться help по цій темі, там досить докладно пояснюється це поняття і наводяться приклади. Для прикладу: class of TObject це посилання на клас TObject.

type TClass = class of TObject; 

Змінна типу TClass не може містити примірник TObject, це лише посилання на певний клас. Ми можемо визначити свій власний тип, як посилання на клас, який є батьківським по відношенню до ВСІХ нашим типами створюваних компонент. Нам потрібна посилання на TControl. У Delphi є вже "готові" типи посилань на класи. Крім TClass є ще кілька, в тому числі і TControlClass.

type TControlClass = class of TControl;

Скористаємося ім. Нам знадобиться список класів, з якими будемо працювати:

Type
TListClass = array [ 0..3 ] of TControlClass;

Const
ListClass: TListClass = (TEdit, TButton, TCheckBox, TLabel);


На подію OnDblClick панелі PanelTest розбираємо, що саме ми повинні створити і створюємо це …

procedure TForm1.PanelTestDblClick(Sender: TObject);
Const No : integer = 0;

Var TypeClass : TControlClass;
New : TControl;
Point : TPoint;

begin
IF (rgComponents.ItemIndex >= 0) AND
(rgComponents.ItemIndex < rgComponents.Items.Count)
Then Begin

/ / Отримуємо посилання на обраний клас
TypeClass:=TControlClass(ListClass[rgComponents.ItemIndex]);

Inc (No); / / збільшуємо лічильник компонент

/ / Створити компонент – викликаємо конструктор обраного класу
New:=TypeClass.Create(PanelTest);

Point:=PanelTest.ScreenToClient(Mouse.CursorPos);

New.Top:=Point.y;
New.Left:=Point.x;

/ / Ім'я = назва класу + номер нового компонента
New.Name:=New.ClassName + IntToStr(No);
New.Tag:=1;

/ / Навішуємо меню по правій кнопці
TEdit(New).PopupMenu:=pmComponent;

/ / Якщо це кнопка – призначимо обробник
IF TypeClass = TButton
Then TButton (New). OnClick: = OnClickButton;

/ / І поміщаємо новенького на панель
New.Parent:=PanelTest;

End;

end;


Все! І ніяких CASE "ів: о)

Коментар:

Point:=PanelTest.ScreenToClient(Mouse.CursorPos);

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






Зміна властивостей групи компонент.  


Ви звернули увагу на рядок New.Tag:=1 ?
Що це за властивість і в чому його сенс?
Це ціле число типу LongInt (довжиною 4 байти), яке є повноцінним властивістю компонента і ніяк не враховується середовищем. Delphi на нього не звертає уваги. Можете там що-небудь зберегти. Про всяк випадок.
Отже, привласнимо нашим новеньким Tag рівний 1. Рядок

TEdit(New).PopupMenu:=pmComponent;

пов'язує наші компоненти з popup-меню pmComponent, яке тепер буде викликатися при натисканні правої кнопки на компоненті. Чи не дивуйтеся приведенням до типу TEdit, справа в тому, що властивість PopupMenu у класу TControl визначено як protected і може бути використано тільки його нащадками. Щоб не перебирати всі наші типи, вибираємо один і використовуємо його для проникнення до потрібного нам властивості. Так як PopupMenu є у всіх наших класів і воно то саме, які наслідували від TControl.PopupMenu, то присвоєння буде вірним. Меню pmComponent містить три опції:

Tag = 1
Tag = 2
——
Видалити компонент

Перша встановлює властивість Tag рівним 1, друга міняє його на 2. І остання опція видаляє компонент по нашому бажанню. Зверніть увагу на основне меню вікна, а саме на його пункт "Зміна кольору" Будемо змінювати колір шрифту наших компонентів по вибраному умові. Всі три пункти меню "Зміна кольору" використовують один і той же оброблювач:

procedure TForm1.AllColorClick(Sender: TObject);
Var i : Integer;
begin
IF TControl(Sender).Tag = 0
Then For i:=0 To PanelTest.ControlCount-1 Do
TEdit (PanelTest.Controls [i]). Font.Color: = ColorGrid.ForegroundColor
Else For i:=0 To PanelTest.ControlCount-1 Do
IF TWinControl (PanelTest.Controls [i]). Tag = TControl (Sender). Tag
Then TEdit (PanelTest.Controls [i]). Font.Color: = ColorGrid.ForegroundColor

end;


Sender у цій процедурі – обраний пункт меню. Зверніть увагу в самому проекті, як розставлено властивість Tag у пунктів меню "Зміна кольору".
Перебираємо в циклі масив Controls панелі PanelTest, він містить наші компоненти, і змінюємо колір шрифту за умовою – якщо значення Tag обраного пункту меню збігається зі значенням Tag чергового компонента, це наш клієнт! Тут ми змінювали властивість у всіх компонентів, незавсимого від їх типу. А в меню "Очистити" проводиться майже така ж операція, але умовою виступає належність до певного типу. Якщо ми хочемо очистити TEdit (тобто очистити його властивість Text), то перебір списку Controls буде виглядати так:

For i:=0 To PanelTest.ControlCount-1 Do
Begin
IF (PanelTest.Controls[i] Is TEdit)
Then TEdit(PanelTest.Controls[i]).Text:=””
End;

У видаленні компоненту немає ніяких складнощів. У нашому прикладі тільки треба зрозуміти, що саме видаляється.
Скористаємося властивістю TPopupMenu.PopupComponent : TComponent , Визначальним компонент, над яким у цей момент натиснули праву кнопку миші і викликали це меню.

pmComponent.PopUpComponent.Free;

На малюнку показаний момент роботи проекту:







Таємниче властивість Tag.  


Було б прикро, якщо Delphi дозволяла б програмістам зберігати для своїх потреб тільки цілочисельний ознака. А хіба мало які в кого потреби? Якщо уважно прочитати Help, то в самій останньому рядку нам відкриється істина: у властивості Tag можна зберігати що завгодно довгою 4 байти (Any 32-bit value such as a component reference or a pointer)! Це може бути ціле число, а може бути посилання (pointer). Наприклад, посилання на компонент. Відчуваєте, які відкриваються перспективи?! Пункт "Відзначити CheckBox" основного меню в design-time має тільки один пункт (смужку вважати не будемо). За змістом завдання в цей пункт повинні додаватися підпункти зі списком створених до цього моменту TCheckBox "ів. При натисканні на потрібний пункт, відповідний TCheckBox на панелі буде відзначатися (Checked: = True). Чудное (наголос на перший склад) властивість Tag допоможе дуже витончено вирішити це завдання. Добудовуватися меню буде в момент його виклику (подія OnClick), бо навіщо нам в інші моменти ця інформація?

procedure TForm1.miCheckBoxClick(Sender: TObject);
Var Item : TMenuItem;
i : Integer;
begin
/ / Очищаємо список
For i:=TMenuItem(Sender).Count-1 DownTo 2
Do TMenuItem(Sender).Delete(i);

/ / Формуємо свіжий варіант меню за поточним списком CheckBox "ов
For i:=0 To PanelTest.ControlCount-1 Do
IF (PanelTest.Controls[i] is TCheckBox)
Then Begin

Item: = NewItem (TCheckBox (PanelTest.Controls [i]). Caption, 0, False, True,
miCheckedClick , 0 , “”);

TMenuItem(Sender).Add(Item);

Item.Tag:=LongInt(PanelTest.Controls[i]);

End;

end;


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

Item.Tag:=LongInt(PanelTest.Controls[i]);

Приведення типів до LongInt необхідно для компілятора, інакше він не дозволить нам це привласнення.
У Delphi клас сам по собі є посиланням, тобто PanelTest.Controls [i] містить не сам компонент, а його адресу. Властивості Tag присвоюються адресу відповідного компонента. Власне і все. Ви звернули увагу на те, що ми відразу при створенні пункту меню прив'язали до нього обробник на подію OnClick? Ось цей обробник:

procedure TForm1.miCheckedClick(Sender: TObject);
Var i : Integer;
begin

/ / Функція Ptr конвертує 4 байти в тип Pointer.
//function Ptr(Address: Integer): Pointer

IF TMenuItem(Sender).Tag = 0
Then Begin
For i:=0 To PanelTest.ControlCount-1 Do
IF PanelTest.Controls[i] Is TCheckBox
Then TCheckBox(PanelTest.Controls[i]).Checked:=True
End
Else TCheckBox(Ptr(TMenuItem(Sender).Tag)).Checked:=True;

end;


IF TMenuItem (Sender). Tag = 0 – перевірка на пункт "Відзначити все", тут ми користуємося старим способом перебору масиву PanelTest.Controls.
А от якщо це пункт для одиничного TCheckBox "а … використовуємо властивість Tag іншим манером: о) В принципі непогано б ще й перевіряти, що повертає Ptr. Приблизно так

Var TagPtr : Ponter;

TagPtr:=Ptr(TMenuItem(Sender).Tag);

IF TagPtr <> nil
Then IF TagPtr Is TCheckBox
Then TCheckBox(TagPtr).Checked:=True;


Це загальний випадок – захист від помилкових ситуацій.
У нашому прикладі в TMenuItem (Sender). Tag нічого іншого бути і не може, так що перевіряти не обов'язково …
Хоча, з перевіркою надійніше: о) Що б ще такого придумати цікавенького? ..
А ось, наприклад, давайте для кожної кнопки міняти в run-time текст виведеного нею повідомлення (пам'ятаєте нашу процедуру OnClickButton?).
Де зберігати цей текст для кожної кнопки? Створити масив або динамічний список рядків і синхронізувати його зі списком компонент?
Можна звичайно … але навіщо так складно?
Адже в цьому чудовому властивості Tag можна зберігати і рядки.


Змінимо трохи код створення компонент:

Var  …
MessTag : PChar;

IF TypeClass = TButton
Then Begin
TButton(New).OnClick:=OnClickButton;

/ / Виділяємо пам'ять під рядок
GetMem (MessTag, Length ("Натиснуто кнопка №" + IntToStr (No)) +1);
StrCopy (MessTag, PChar ("Натиснуто кнопка №" + IntToStr (No)));

TButton(New).Tag:=LongInt(MessTag);
End
Else New.Tag:=1;



І процедура OnClickButton зміниться відповідним чином:

Procedure TForm1.OnClickButton( Sender : TObject );
Var Value : String;
Begin
Value:=PChar(Ptr(TButton(Sender).Tag));

MessageDlg(Value ,mtInformation,[mbOk],0);

End;


Зміна рядка прив'яжемо до меню pmComponent, додавши туди ще один пункт. Зверніть увагу, що пункт цей має сенс тільки для кнопок. Тому перед показом popup-меню, на подію OnPopup, робимо відповідний пункт видимим або невидимим.

procedure TForm1.pmComponentPopup(Sender: TObject);
begin
pmTag1.Checked:= TPopupMenu(Sender).PopupComponent.Tag = 1;
pmTag2.Checked:= NOT pmTag1.Checked;

pmLine.Visible: = TPopupMenu (Sender). PopupComponent Is TButton;
pmNewMessage.Visible:=pmLine.Visible;

end;


Так! Перед тим, як записати адресу нового рядка в Tag, звільнимо пам'ять від попередньої, адже вона нам більше не потрібна.

procedure TForm1.pmNewMessageClick(Sender: TObject);
Var Value : String;
MessTag : PChar;
begin
MessTag:=PChar(TButton(pmComponent.PopupComponent).Tag);
Value:=MessTag;
IF InputQuery ("Змінити повідомлення",
"Для кнопки" + TButton (pmComponent.PopupComponent). Name, Value)
Then Begin
FreeMem(MessTag);
GetMem(MessTag , Length(Value)+1);
StrCopy(MessTag , PChar(Value));
TButton (pmComponent.PopupComponent). Tag: = LongInt (MessTag);
End;

end;


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






Рух – це життя  


Наостанок, для повного пожвавлення картини, зробимо наші компоненти рухливими. Дозволимо їм вільно переміщатися по панелі, тягаючи їх мишкою. Це буде не drag & drop, А зовсім звичайне переміщення, як в режимі design-time. Для його реалізації нам знадобиться обробити самим дві події пересувається компоненти:



OnMouseDown – Якщо натиснута ліва кнопка мишки, запам'ятаємо її поточний стан у змінній DragPoint і і будемо вважати ці координати точкою відліку.
 
OnMouseMove – Якщо натиснута ліва кнопка мишки, пересуваємо компонент за новими координатами, зрушуючи його так само, як зрушилася мишка щодо нашої точки відліку. Тим самим компонент буде плавно пересуватися слідом за мишкою.

Пишемо ці процедури самі, список параметрів повинен відповідати описах цих подій (для цього знову до Help "у). У принципі можна подивитися, як створює обробники цих подій сама Delphi і звідти переписати параметри.

procedure TForm1.ControlMouseDown (Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
DragPoint:=Point(X , Y );
end;

procedure TForm1.ControlMouseMove (Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
IF (ssLeft IN Shift) Then
Begin
TControl (Sender). Left: = TControl (Sender). Left + x – DragPoint.X;
TControl (Sender). Top: = TControl (Sender). Top + y – DragPoint.y;
End;
end;


Природно, що при створенні компонент необхідно додати призначення цих процедур як обробників потрібних нам подій.


New.Top:=Point.y;
New.Left:=Point.x;

TLabel(New).OnMouseDown:=ControlMouseDown;
TLabel(New).OnMouseMove:=ControlMouseMove;


Ну, а тепер, мишку в руки і сміливо вперед!

Разом …


У статті я досить докладно розібрала деякі питання, деякі торкнулася побіжно.
Не дала прямої відповіді на питання, чому не можна писати “(Edit + IntToStr(i)).Text:=”””. У самому проекті залишено кілька забавних нюансів, від яких би треба позбутися (наприклад, під час перетягування компонентів спрацьовує подія OnClick, що робить кнопки особливо нав'язливими).
Це не від ліні, в цьому була стратегічна задумка … Ніякої Інтернет з усіма його конференціями, статтями та прикладами ніколи не замінить програмісту власного досвіду. Тільки те, що видобуто (розібрано і зрозуміло) своїми силами, запам'ятовується надовго і приносить користь.
Досвіду треба набиратися обов'язково. Читайте help, шукайте відповідь в исходники, експериментуйте з проектом скільки душі завгодно, але тільки обов'язково самі …
Удачи!

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


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

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

Ваш отзыв

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

*

*