TListBox в FireMonkey. Создание своих стилизованных итемов для TListBox. 1 часть
Компонент TListBox обладает большими возможностями по стилизации своих итемов (элементов). О том, как сделать так же, как на приведенном ниже рисунке, и пойдет речь в этой статье.
Введение
В этом примере будет продемонстрирован один из способов создания своих итемов для ListBox и заполнение их своими данными. В примере буду представлены три типа итемов: по умолчанию и два своих.
Ссылка на архив проекта: http://cc.embarcadero.com/Item/29075
Немного теории
Пара слов о загрузке стилей
Из моей предыдущей статьи вы узнали, что стиль – это по сути иерархия оъектов прикрепленная к стилизуемому компоненту. Как только стиль загружен (иерархия объектов прикреплена к компоненту) – сразу приходит оповещение об этом через событие TStyledControl.OnApplyStyleLookup, компонент получает полный доступ к объектам стиля и, пользуясь этим, задает им нужные значения.
Так же я упоминал, о процессе смены стиля. При смене стиля, прикрепленная иерархия стиля удаляется из ветки компонента, а на ее место вставляется новый стиль. Естественно, что при смене стиля, компоненту нужно заново проинициализировать объекты стиля нужными значениями.
Например, при загрузке стиля кнопки, кнопке нужно указать свое название в соответствующий объект TText у стиля. При смене стиля, нужно это сделать повторно.
Эта информация понадобиться нам для понимания процесса передачи своих данных в стиль ListBoxItem.
Пара слов о работе TListBox
Представьте себе, например, лист бокс с 10 000 элементов. Для каждого элемента нужно загрузить свой стиль. Это требует большого объема дополнительной памяти для хранения каждого стиля в памяти. Однако, реально мы можем видеть только малую часть из всех элементов.
Так зачем же нам грузить стили для всех элементов?
Правильно, не за чем. Именно по этому наш TListBox может иметь сколь угодно большое число элементов, однако стили будут только у видимой части элементов. Это сокращает в разы расход память, однако дает нам дополнительную задачу о передачи данных элементов (TListBoxItem.Text, TListBoxItem.isChecked) в объекты стилей. Для решения этой задачи каждый TListBoxItem хранит в себе значение Text и isChecked. Поэтому при загрузке стиля, он их проставляет в стиль в момент загрузки стиля TListBoxItem.ApplyStyle.
А как быть, если мы хотим задать в стиль свои данные?
Например, хотим использовать TListBox как средство отображения загружаемых файлов. Включить в стиль TProgressBar и отображать в нем процесс загрузки файла.
На этот вопрос есть два способа. Отличающиеся только местом хранения данные для последующей передачи их в стиль.
1 способ. Простой.
Мы храним наши данные у себя в моделе.
Например. Храним в моделе список файлов, их состояния загрузки (загружен или нет) и размеры загруженных частей. Далее в обработчике событий TListBoxItem.OnApplyStyleLookup мы передаем эти данные в соответствующие элементы стиля.
2 способ. Посложнее.
Мы создаем отдельный класс итема отнаследовавшись от TListBoxItem, храним наши данные там и в момент загрузки стиля TStyledControl.ApplyStyle передаем их в стиль. Таким способом сделан, например, TMetropolisUIListBoxItem.
В наших прикладных задачах удобнее использовать 1 способ. Поскольку, как правило формируемые для отображения данные уже где-то созданы и централизованно хранятся (списки, модели и тд). Поэтому в этом примере мы реализуем первый способ.
Практика
Вначале сделаем набросок формы, как на приведенном ниже рисунке. Самое главное для нас – это непосредственно сам TListBox и хранилище наших стилей TStyledBook.
На этой форме опции группы “ListBox options”, позволяют менять некоторые настройки лист бокса:
- Прокрутка итемов с использованием мышки, а не с использованием ScroolBox.
- Многоколоночное отображение.
- Отображение и скрытие CheckBox у итемов.
Группа контролов “Action” позволяет нам заполнить лист бокс итемами с выбранным стилей.
Создание стилей
В этом примере мы создадим два своих стиля для отображения своих данных в элементах ListBox. На каждом из них помимо стандартных данных (Текст и чекбокс), будут еще: индикатор загрузки (TAniIndicator), полоса состояния (TProgressBar), кнопка (TButton) и прямоугольник для отображения цвета (TRectangle). Пример этих стилей приведен ниже.
Для создания стиля, открываем дизайнер стилей (двойное нажатие на TStyleBook ). В панели “Structure” отображаются текущие стили выбранного StyleBook.
Создание стилей производится путем перетаскивания нужных компонентов с “Tool Palette” в дерево стилей на панели “Structure”. Принцип создание стиля такой же, как создание интерфейса формы.
По умолчанию в структуре отображается корневой элемент - контейнер стилей, в него будут складываться все наши стили. Все дочерние узлы считаются стилями.
Чтобы задать имя стиля, нужно использовать свойство TStyledControl.StyleName.
Из доступных компонентов я набросал два стиля (См. рисунки ниже).
Используя свойство StyleName у TLayout верхнего уровня, я условно назвал стили CustomStyle1 и CustomStyle2.
Всем частям стиля, к которым я хочу в дальнейшем получить доступ для задания своих данных, нужно задать имя через StyleName. Именно оно и будет использоваться для поиска в структуре стиля, нужного объекта.
Примечание: Свойство Name не используется для поиска составных частей стиля.
У меня получилось, что каждый стиль содержит следующие именованные составные части:
- TCheckBox.styleName = check
- TText.styleName = text
- TAniIndicator.styleName = aniindicator
- TButton.styleName = button
- TProgressBar.styleName = progress
- TRectangle.styleName = rectangle
Стили почти готовы. Осталось только разобраться со свойством HitTest. Поскольку стиль встраивается в контрол, то все события перемещения мыши, фокус, и тд. в начале перехватываются контролами непосредственно лежащие ближе к нам (по оси Z), а именно контролы стиля. А затем, если они не могут их обработать, то события спускаются вниз до тех пор, пока кто-нибудь их не обработает. HitTest отвечает за то, будет ли контрол перехватывать события мыши или их стоит передать родительскому контролу.
Поскольку для реализации прокрутки элементов TListBox требуется спустить событие мыши до TListBox, то соответственно всем котролам стиля объекта TListBoxItem, не используемым события мыши, нужно проставить HitTest = False.
Например: Кнопке нужны события мыши, потому что благодаря им, она перехватывает события клика кнопки. А вот TText не требуется события мыши, потому как этот объект в нашем случае только отображает данные.
Теперь стили готовы и нам осталось только осуществить связь наших данных с составными частями стилей.
Реализация
Для хранения наших данных элементов, я создал запись следующего вида:
TListItemData = record Text: string; Checked: Boolean; InProgress: Boolean; Progress: Byte; Color: TAlphaColor; Click: TNotifyEvent; end;
В качестве хранилища данных выбрал словарь (“Идентификатор элемента – его данные”). Идентификатор нам понадобиться для дальнейшего получения данных для ListBoxItem:
FItemsData: TDictionary;
Данные в этом примере будут генерироваться произвольным образом:
procedure TFormMain.GenerateItemsData(const ACount: Integer); var I: Integer; ItemData: TListItemData; begin for I := 1 to ACount do begin Inc(LastItemId); ItemData.Text := 'Item ' + IntToStr(LastItemId); ItemData.Checked := Random(2) = 1; ItemData.Progress := Random(100); ItemData.InProgress := Random(2) = 1; ItemData.Color := MakeColor(Random(255), Random(255), Random(255), 255); ItemData.Click := ActionItemClick.OnExecute; FItemsData.Add(LastItemId, ItemData); end; end;
Теперь процедура динамического создания итемов и передачи наших данных в составные части стиля для лист бокса:
procedure TFormMain.CreateListBoxItem(const AItemKey: Integer; const AStyleName: string); var Item: TListBoxItem; Begin // Создаем элемент Item := TListBoxItem.Create(Self); // Вставляем его в листбокс Item.Parent := ListBox; // Через свойство Tag передаю ключ записи из нашего хранилища. // В дальнейшем через него мы получим доступ к данным хранилища FItemsData Item.Tag := AItemKey; // Подвешиваем обработчик, в котором будет происходить непосредственно // передача наших данных в составные части стиля Item.OnApplyStyleLookup := DoItemApplyStyle; // Указываем имя стиля Item.StyleLookup := AStyleName; end; procedure TFormMain.DoItemApplyStyle(Sender: TObject); var Item: TListBoxItem; ItemKey: Integer; StyleObject: TFmxObject; AniIndicator: TAniIndicator; Rectangle: TRectangle; begin Assert(Sender is TListBoxItem, 'This handler is not intended for other objects. Only for TListBoxItem'); if not (Sender is TListBoxItem) then Exit; // Вытаскиваем TListBoxItem, в который загружен стиль, и ключ для получения данных из FItemsData Item := Sender as TListBoxItem; ItemKey := Item.Tag; // TListBoxItem сам хранит значения (текста и CheckBox) и предоставляет нам // свойства Text, IsChecked для их задания. Item.Text := FItemsData[ItemKey].Text; Item.IsChecked := FItemsData[ItemKey].Checked; // StyledData – это один из способов передачи данных в указанный элемент стиля (через StyleName), // используемый механизм RTTI. Фактически внутри идет проверка типа передаваемого значения // TValue и в соответствии с ним происходит задание предопределенных свойств. // Отмечу, что этим способом можно задать значения только для некоторых свойств. Item.StylesData['progress'] := TValue.From(FItemsData[ItemKey].Progress); Item.StylesData['button'] := TValue.From(FItemsData[ItemKey].Click); // Этот способ самый универсальный. // Метод FindStylResource ищет в стиле объект с указанным stylename. StyleObject := Item.FindStyleResource('aniindicator'); if Assigned(StyleObject) and (StyleObject is TAniIndicator) then begin AniIndicator := StyleObject as TAniIndicator; AniIndicator.Enabled := FItemsData[ItemKey].InProgress; end; StyleObject := Item.FindStyleResource('rectangle'); if Assigned(StyleObject) and (StyleObject is TRectangle) then begin Rectangle := StyleObject as TRectangle; Rectangle.Fill.Color := FItemsData[ItemKey].Color; end; end;
Вот в принципе и все важные детали. Остальное смотрите в примере, который можно скачить от сюда http://cc.embarcadero.com/Item/29075 Смотрите, пробуйте, спрашивайте.
Posted by ybrovin on October 11th, 2012 under Firemonkey (RUS) |






RSS Feed

October 11th, 2012 at 4:32 pm
А когда вызывается метод FreeStyle у айтемов выходящих из области видимости (нужно же как-то избавляться от уже загруженного стиля)? Судя по демке CustomListBox - никогда. Добавили 1000 айтемов, проскроллили список до конца, получили расход памяти в 108МБ.
October 12th, 2012 at 2:33 am
какова цель использования внешнего словаря TListItemData? не проще было просто расширить (унаследовать от) TListBoxItem нужными полями? никаких лишних индексов и ключей, заполнения tag и т.п.
т.е. если данные вообще "внешние", то понятно; но для демки можно было заодно проиллюстрировать использование наследования элементов.
October 12th, 2012 at 2:48 am
Terekhov Andrey,
Я с вами согласен, что для меня лично создать новый класс итема проще. Хотя бы потому, что, как вы правильно упомянули, мне не нужно хранить ключи.
Цель проста - продемонстрировать один из двух способов создания своих итемов. Кому-то будет удобнее создать свой класс итема, как вам и мне, а кому-то наоборот, подвязать свои данные из своего источника.
По вашим пожеланиям чуть позже проиллюстрирую еще второй способ, через реализацию своего класса
November 6th, 2012 at 1:38 pm
Спасибо!
Не могли бы Вы разъяснить этот момент:
// Отмечу, что этим способом можно задать значения только для некоторых свойств.
Item.StylesData['progress'] := TValue.From(FItemsData[ItemKey].Progress);
November 7th, 2012 at 1:46 am
Slh,
Да, конечно.
property StylesData[const Index: string]: TValue - это универсальный для всех компонентов способ передачи значений в объекты стиля. Это свойство идет от класса TStyledControl. Оно позволяет по имени (задаваемого через stylename) дочернего объекта стиля задавать некоторые свойства. Как вы видите это свойство работает со значениями TValue. TValue - это низкоуровневая оболочка значения из RTTI. Позволяет получать Rtti информацию о типе передаваемого значения, виде, и многое другое (смотрите System.Rtti.TValue). Поскольку StylesData должен работать с любым типом данных: объекты, примитивные типы, события - поэтому мы использует TValue.
Работает это дело так:
1. Мы приводим наше значение свойства к TValue шаблонными методами TValue.From
TValue.From(FItemsData[ItemKey].Progress);
В данном случае мы взяли значение состояния прогресса для TProgressBar
2. Пытаемся задать его через TStyledControl.StylesData
Item.StylesData['progress'] := TValue.From(FItemsData[ItemKey].Progress);
3. По указанному имени объекта стиля ищется соответствующий объект. Если он не найден, то на этом шаге задание прерывается и значение не устанавливается.
4. Если объект стиля найден, то ему передается это значение TValue в виртуальный метод TFmxObject.SetData, который как раз и будет осуществлять присваивание. Свойство для присваивания, определяется из типа значения и реализацией метода SetData в потомках!!!
На примере TProgressBar:
procedure TProgressBar.SetData(const Value: TValue);
begin
if Value.IsOrdinal then
Self.Value := Value.AsOrdinal
else
if Value.IsType then
Self.Value := Value.AsType
else
Self.Value := Min
end;
Если переданное значение вещественного типа или ordinal типа, то мы задаем свойство Value. Если другого типа, то сбрасываем состояние прогресса на минимум.
Примечание!!!!
Как вы уже поняли, на данный момент таким способом не все свойства можно задать. В основном это свойства "первой" необходимости.
Узнать какие свойства задаются можно в исходном коде метода .SetData
Это старый способ задания. Более универсальный через FindStyleResource. В которым вы можете вытащить сам объект стиля, привести его к нужному классу и напрямую задавать значения любым свойствам и событиям.
June 3rd, 2013 at 5:41 pm
Исходников и FMX.ListBox.pas у меня нет.
Объясните момент. при маустрекинг (вытягивании листа по "сенсорному") или анимации плейлиста просиходит евентс OnApplyStyleLookup и заполняется список уе абы как. ни как не могу врубить, как это все должно корректно работать.
procedure TForm1.Button2Click(Sender: TObject);
var
Item: TListBoxItem;
begin
ClientDataSet1.DisableControls;
try
ClientDataSet1.First;
while not ClientDataSet1.Eof do
begin
Item := TListBoxItem.Create(nil);
Item.Parent := pl;
Item.OnApplyStyleLookup := pl_set;
ClientDataSet1.Next;
end;
finally
ClientDataSet1.EnableControls;
Item.StyleLookup := ‘PLitem’;
end;
end;
procedure TForm1.pl_set(Sender: TObject);
var
Item: TListBoxItem;
begin
Item := TListBoxItem(Sender);
status1.Text := ClientDataSet1.FieldByName(’title’).AsString;
Item.Text := ClientDataSet1.FieldByName(’title’).AsString;
ShowMessage(status1.Text);
end;
когда залистываю мышой до -итем.индекс, то вызывается pl_set и произвольные итемы перезаполняются этой функцией.
June 3rd, 2013 at 5:48 pm
Maksim Nikulin,
TListBox позволяет работает с большим количеством элементов (Items). Чтобы TListBox не занял всю память под хранение стилей для каждого элемента списка, стиль загружается только для видимых итемов. Что у вас и получилось. Обработчик события pl_set вызывается для итема, только тогда, когда итем непосредственно появляется в видимой части.
June 3rd, 2013 at 5:50 pm
Так же начиная с XE3 все стилизованные итемы поддерживают теперь кэш данных стиля. То есть один раз задав данные для элементов внутри стиля, вам не нужно беспокоится о повторном задании данных при выгрузке и загрузке стиля. Доступ к кэшу осуществляется через свойство TStyledCOntrol.StylesData
June 4th, 2013 at 10:32 am
даже в примере customlistfrm.pas:
procedure TfrmCustomList.DoApplyStyleLookup(Sender: TObject);
var
Item: TListBoxItem;
begin
ListBox1.Tag:=ListBox1.Tag+1 ;
Item.StylesData['depth'] := IntToStr(ListBox1.Tag)+’. 32 bit’;
при тягании мышой с маустрекингом листа в -итем.индекс в произвольном(непонятно какой закономерности) итеме меняется значение на новое ListBox1.Tag
вот и как заполнить данные, мне нужен лист с иконками, что бы они не перетирались или как их потом восстанавливать корректно при этом событии?
June 4th, 2013 at 10:48 am
Так если я в стилях добавил поле resolution, то и пусть бы среда САМА уже отрисовывала стиль при доступности визуальном и не трогала не визуальные, применяя автоматом к Item.StylesData['resolution']. а Item.StylesData['resolution'] был бы как итем/массив.
June 4th, 2013 at 4:05 pm
Вообще, как я себе представлял, в "кешах" Item.StylesData['depth'] хранятся данные, а стили сами цепляются ко всем отрисованным итемам с таким названием. все легко и просто!
анн нет, так не бывает! :\
June 4th, 2013 at 4:06 pm
а Item.StylesData['depth'] - что-то вроде хеш массива
June 4th, 2013 at 6:26 pm
Какая у вас версия Rad Studio?
В текущем примере CustomListBox работа идет через кэш стилей. И никакого упоминания о OnApplyStyleLookup речи уже нет.
June 5th, 2013 at 9:39 am
XE3 Up2
в примере customlistfrm.pas добавляю дин. переменную и вуаля, при скроллинге листа за область листинга - перезаполняется переменная.