Наконец-то разжился Delphi XE2 и пробую работать с FireMonkey. Довольно интересно, FireMonkey выглядит гибкой штукой. Особенно привлекает кроссплатформенность. Расскажу о том, что успел понять.
Решил начать с простенькой кастомизации компонента, чтобы получше разобраться с тем, как все устроено внутри. Под руку попался TTabControl, мне не хватает «крестика» на каждой вкладке, чтобы их можно было закрывать как в браузере. Очевидно, что наследоваться необходимо от класса TTabItem.
Небольшое лирическое отступление. Вообще, все визуальные компоненты в FireMonkey делятся на две категории: примитивные (наследники TShape) и стилизованные (наследники TStyledControl). Примитивные не делают ничего особо полезного, кроме рисования различных квадратов и окружностей на своей канве, по сути очень напоминая старый добрый TGraphicControl в VCL. А вот стилизованные уже интереснее — они имеют стиль.
Важно отметить, что стиль — это не разноцветная «шкурка», а контрол. Например, стиль TButton состоит из набора прямоугольников, текста и эффектов к ним применяемых. Фактически, говоря в терминах VCL, в FireMonkey любой стилизованный компонент очень похож на фрейм. Т.е. имеет разделенные код и визуальное представление, которое в широких рамках можно менять.
Вернемся к вкладкам TTabControl’а. Довольно интересный для меня момент, что стиль вкладки не имеет ничего общего с её содержимым и описывает всего-лишь саму кнопку. В качестве родителя для компонентов, которые должны на ней располагаться, вкладка использует отдельный контейнер типа TContent. Это позволяет отрисовывать вкладку в два этапа: сначала рисовать кнопку, а затем располагать TContent на теле TTabControl’а-родителя, если эта вкладка активна. За подробностями отсылаю к методам TTabItem.Realign и TTabControl.Realign.
Становится ясно, что добавление «крестика» нужно начать с изменения стиля вкладки. Способ рисования я подсмотрел у TCheckBox и реализовал, добавив в стиль компонент TPath (неплохое описание этого компонента есть тут) с соответствующими свойствами. А также эффект для него, чтобы при наведении курсора «крестик» подсвечивался.
Способ привязки стиля к кастомному компоненту описан в справке. Суть в том, чтобы сохранить текст стиля в ресурс. Не очень удобно, честно говоря.
Сохраняю измененный стиль в файл ClosableTabItem.style. Текст стиля у меня в итоге получился вот такой, очень похоже на DFM, я не зря сравнил с фреймами :)
object TLayout StyleName = 'CloseableTabItemStyle' Position.Point = '(276,419)' Width = 145.000000000000000000 Height = 22.000000000000000000 object TRectangle Align = alContents Position.Point = '(0,2)' Locked = True Width = 148.000000000000000000 Height = 20.000000000000000000 Padding.Rect = '(0,2,-3,0)' HitTest = False Fill.Color = xFFEFEFEF Stroke.Color = xFF898C95 Corners = [crTopLeft, crTopRight] Sides = [sdTop, sdLeft, sdRight] object TRectangle Align = alContents Width = 148.000000000000000000 Height = 20.000000000000000000 HitTest = False Fill.Kind = bkGradient Fill.Gradient.Points = < item Color = x24F4F4F4 end item Color = x24EAEAEA Offset = 0.499000012874603300 end item Color = x4E868686 Offset = 0.500000000000000000 end> Stroke.Color = xC84F4F4F Corners = [crTopLeft, crTopRight] Sides = [sdTop, sdLeft, sdRight] object TFloatAnimation Duration = 0.000000999999997475 Trigger = 'IsSelected=false' StopValue = 1.000000000000000000 PropertyName = 'Opacity' end object TFloatAnimation Duration = 0.000009999999747379 Trigger = 'IsSelected=true' StartValue = 1.000000000000000000 StopValue = 0.001000000047497451 PropertyName = 'Opacity' end end object TColorAnimation Duration = 0.000009999999747379 Trigger = 'IsMouseOver=true;IsSelected=false' StartValue = xFFEFEFEF StopValue = xFFA5D9FF PropertyName = 'Fill.Color' end object TColorAnimation Duration = 0.000009999999747379 Trigger = 'IsMouseOver=false;IsSelected=false' StartValue = xFFA5D9FF StopValue = xFFEFEFEF PropertyName = 'Fill.Color' end object TColorAnimation Duration = 0.000000999999997475 Trigger = 'IsSelected=false' StartValue = xFFFEFEFE StopValue = claWhite PropertyName = 'Fill.Color' end object TColorAnimation Duration = 0.000009999999747379 Trigger = 'IsSelected=true' StartValue = claWhite StopValue = xFFFEFEFE PropertyName = 'Fill.Color' end object TRectAnimation Duration = 0.000009999999747379 Trigger = 'IsSelected=true' StartValue.Rect = '(0,2,-1,0)' StopValue.Rect = '(0,0,-1,-1)' PropertyName = 'Padding' end object TRectAnimation Duration = 0.000009999999747379 Inverse = True Trigger = 'IsSelected=false' StartValue.Rect = '(0,2,-1,-1)' StopValue.Rect = '(0,0,-1,0)' PropertyName = 'Padding' end object TText StyleName = 'text' Align = alClient Position.Point = '(3,3)' Locked = True Width = 121.000000000000000000 Height = 15.000000000000000000 Padding.Rect = '(3,3,3,2)' HitTest = False Text = 'item' WordWrap = False end object TPath StyleName = 'close' Align = alFitRight Position.Point = '(132,5)' Width = 11.000000000000000000 Height = 10.000000000000000000 Padding.Rect = '(5,5,5,5)' Data.Path = { 1200000000000000000000000000000001000000CDCC4C3E0000000001000000 0000003F9A99993E01000000CDCC4C3F00000000010000000000803F00000000 010000000000803FCDCC4C3E010000003333333F0000003F010000000000803F CDCC4C3F010000000000803F0000803F01000000CDCC4C3F0000803F01000000 0000003F3333333F01000000CDCC4C3E0000803F01000000000000000000803F 0100000000000000CDCC4C3F010000009A99993E0000003F0100000000000000 CDCC4C3E010000000000000000000000030000000000000000000000} object TGlowEffect Trigger = 'IsMouseOver=true' Enabled = False Softness = 0.400000005960464400 GlowColor = claDarkred Opacity = 0.899999976158142100 end end end end
Сооветственно, нужен и RC-файл:
ClosableTabItemStyle RCDATA "ClosableTabItem.style"
Теперь, продолжая следовать справке, пишем метод загрузки объекта стиля:
function TClosableTabItem.GetStyleObject: TControl; var S: TResourceStream; const Style = 'ClosableTabItemStyle'; begin if (FStyleLookup = '') then begin if FindRCData(HInstance, Style) then begin S := TResourceStream.Create(HInstance, Style, RT_RCDATA); try Result := TControl(CreateObjectFromStream(nil, S)); Exit; finally S.Free; end; end; end; Result := inherited GetStyleObject; end;
Вот и все. Почти. Но уже можно писать тестовое приложение. Бросаем на форму TTabControl и кнопку, которая будет добавлять вкладки:
procedure TForm1.Button1Click(Sender: TObject); var NewTab: TClosableTabItem; begin NewTab := TClosableTabItem.Create(TabControl1); NewTab.Parent := TabControl1; NewTab.Text := 'New Tab'; end;
Кажется, можно запускать. Крестик появился и даже подсвечивается, но ничего полезного не делает :)
Чтобы вкладка смогла закрываться, нужно добавить еще немножко кода. Мелочи вроде объявления переменных FOnCloseQuery и FOnClose опускаю.
Не придумал ничего лучше, чем присваивать обработчик OnClick в момент применения стиля. Судя по коду компонентов FireMonkey, кажется, это вполне нормальная практика.
procedure TClosableTabItem.ApplyStyle; var CloseButton: TfmxObject; begin inherited; CloseButton := FindStyleResource('close'); if (CloseButton <> nil) and (CloseButton is TControl) then TControl(CloseButton).OnClick := CloseButtonClick; end; procedure TClosableTabItem.CloseButtonClick(Sender: TObject); var CanClose: Boolean; TabControl: TTabControl; begin CanClose := True; if Assigned(FOnCloseQuery) then FOnCloseQuery(Self, CanClose); if CanClose then begin if Assigned(FOnClose) then FOnClose(Self); TabControl := Self.Parent as TTabControl; Self.Free; TabControl.Realign; end; end;
Простенько, но на данном этапе мне большего и не нужно. Полный код можно скачать здесь.
А теперь плохая новость. В рантайме все работает, но я не нашел никакого способа добавить свой новый тип вкладки в Items Designer. И похоже, что та же самая проблема есть у всех списковых контролов: TListBox, TGrid и т.п. Сначала мне подход к их реализации очень понравился, а вот теперь даже как-то сомневаюсь. Поиск в интернете показал, что я с этой проблемой не одинок.
Справка молчит, в коде я тоже не нашел ничего. Неужели никак? Это было бы крайне неприятно.
update: QC #107177
1. DesweR
17 Июл 2012 5:32 дп
По идее, теперь нужно создать дизайн-тайм пакет и в нём регистрировать новый класс для компонентного редактора (по аналогии, как регистрируются сами компонентные редакторы, редакторы свойств и т.п.), но вчера прошерстил весь Tools API и что-то ничего похожего не нашёл…
2. Роман Янковский
17 Июл 2012 7:03 дп
Дизайн-тайм пакет я сделал, это как раз понятно. А вот что делать дальше… тайна покрытая мраком. Пробовал регистрировать класс всеми способами, какие нашел.
3. DesweR
17 Июл 2012 7:48 дп
Я и говорю, что по идее, нужно некой специальной функцией RegisterXXX, из Tools API, зарегистрировать свой класс TTabItem’а для редактора компонента TTabControl, но беда в том, что такой специальной функции нет или я по крайней мере не нашёл.
Вечером попробую поставить триальный пак от TMS
http://www.tmssoftware.com/site/tmsfmxpack.asp
и глянуть Items Designer в их гридах, если там появятся их собственные TTabItem’ы, то можно дальше копать.
4. DesweR
17 Июл 2012 8:34 дп
Хотя мне всё больше и больше кажется, что для собственных Item’ов придётся писать собственный компонентный редактор…
5. DesweR
17 Июл 2012 5:19 пп
Глянул… всё таки наверное свой писать.
6. Роман Янковский
17 Июл 2012 6:01 пп
Это ужасно. Такое можно объяснить только сыростью FireMonkey.
Я планирую еще на эту тему писать, но уже сейчас могу сказать, что складывается стойкое впечатление, будто при разработке FireMonkey никто не учитывал, что кто-то захочет наследоваться от стандартных компонентов и их расширять. Данный редактор — это далеко не единственное, во что я воткнулся.
7. DesweR
17 Июл 2012 6:18 пп
По-хорошему надо оформить на QC репорт.
8. Роман Янковский
17 Июл 2012 6:48 пп
Да, так и сделаю.
9. DesweR
18 Июл 2012 6:24 дп
Ссылка на твой репорт:
http://qc.embarcadero.com/wc/qcmain.aspx?d=107177
Сам проголосовал, может ещё кто-нибудь проголосует.
10. Firemonkey. От простого к сложному #8. Разработка программы "Я математик". | Delphi в Internet
9 Авг 2012 8:57 пп
[…] в блог Роман.Янковский.me и посмотреть как там автор расширял TTabControl в FireMonkey и к какие выводы были сделаны. А я тем временем […]