Жизнь и смерть в режиме run-time

Источник: delphikingdom
Елена Филиппова

Автор: © Елена Филиппова

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

Цель статьи ответить на вопросы и показать :

  • Каким образом можно создавать и располагать на форме визуальные компоненты?
  • Каким образом обрабатывать группы компонент, меняя их свойства "одним махом"?
  • Как назначать обработчики для разных событий?
  • Зачем нужно свойство Tag?
  • Как можно двигать компоненты по форме?

И все это - во время работы программы (режим run-time).

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

Скачать проект (откомпилировано в Delphi 5) :

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

Управляющие элементы формы:

  • rgComponents : TRadioGroup - выбор типа компонента
  • ColorGrid : TColorGrid - выбор цвета фонта для меню "Изменить цвет"
  • MainMenu : TMainMenu - действия с группами компонент
  • pmComponent : TPopupMenu - изменения свойств конкретного компонента и его удаление.

Некоторые элементы 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); // увеличиваем счетчик компонент

			// Cоздаем компонент - вызываем конструктор выбранного класса
			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, ищите ответ в исходниках, экспериментируйте с проектом сколько душе угодно, но только обязательно сами...
Удачи!


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=16335