Продолжим тему обзора нового подхода по разделению контрола на модель и презентацию, описанную здесь. И в этой статье рассмотрим практическое применение этого подхода на примере добавления функции авто завершения ввода в TEdit.
Код рабочего проекта: Пример для XE8, Пример для XE10
Содержание
Первое, что мы должны решить, где хранить список предлагаемых слов для подстановки. Ответ прост – естественно в модели TEdit. Но класс модели TCustomEditModel не имеет для этого зарезервированные поля/свойства вроде SuggestionList. Поэтому естественное желание заключается в расширении класса модели и добавлении в свой класс этого поля. Но удержимся от этого порыва, и посмотрим, что модель имеет для нас кое-что другое в обход создания нового класса.
Модель позволяет хранить данные любых типов без наследования
Давайте получше взглянем на базовый класс модели TDataModel. Он является наследником класса TMessageSender, который в свою очередь предоставляет функциональность по отправке сообщений. TDataModel добавляет дополнительно механизм для хранения данных любого типа с использованием TValue. Это значит, что мы можем сохранить в TDataModel любые наши данные, любых типов не создавай отдельного класса модели. Для этого используется тип универсального хранения данных любого типа TValue.
Я не буду рассматривать о всех возможностях TValue. Основная идея в том, что TValue позволяет поместить в себя данные любого типа. С остальными подробностями можно ознакомиться тут (XE7 Doc wiki).
А раз мы можем поместить в модель наши данные, то значит:
- Данные будут автоматически доступны нам в нашем представлении.
- Не требуется создание отдельного класса модели.
Ниже приведен пример для задания и обратного получения данных строкового типа, массива и обработчика события в модель поля ввода TEdit.
uses System.Rtti; //... type TStringArray = TArray<string>; var ArrayList: TStringArray; EventHandler: TNotifyEvent; begin { Setting/Getting string value } Edit1.Model.Data['string_value'] := TValue.From<string>('My string value'); if Edit1.Model.Data['string_value'].IsType<string> then ShowMessage(Edit1.Model.Data['string_value'].AsString); { Removing data } Edit1.Model.Data['string_value'] := TValue.Empty; { Setting/Getting value of array type } ArrayList := ['Apple', 'ARC', 'Auto', 'Allday', 'Alltime']; Edit1.Model.Data['array_value'] := TValue.From<TStringArray>(ArrayList); if Edit1.Model.Data['array_value'].IsType<TStringArray> then ArrayList := Edit1.Model.Data['array_value'].AsType<TStringArray>; { Setting/Getting event handler } Edit1.Model.Data['event_handler_value'] := TValue.From<TNotifyEvent>(EventHandler); if Edit1.Model.Data['event_handler_value'].IsType<TNotifyEvent> then EventHandler := Edit1.Model.Data['event_handler_value'].AsType<TNotifyEvent>(); end;
Data – это свойство индексного типа введенного на уровне TDataModel, осуществляющее задание и получение значения данного по указанному имени (Ключ – Значение). Имя данных регистро зависимый.
TValue.From<T> — оборачивает данные типа T в TValue.
Если вы хотите удалить свои данные из модели, достаточно задать TValue.Empty для вашего имени данных.
Обратите внимание, что в примере выше, я дополнительно добавил проверку значения на ожедаемый тип значения. В данном примере она не нужна, поскольку я знаю, что я задаю и сразу же я беру это же значение. Однако она потребуется в будущем, чтобы быть уверенным в том, что в моделе лежит значение нужного типа.
При установке или получении данных модель отправляет уведомления в презентацию с кодами сообщений:
- MM_DATA_CHANGED – данные изменились.
- MM_GETDATA – запрос на получение данных из модели.
И значением типа
TDataRecord = TPair<string, TValue>;
Где первое значение – название ключа, второе — само значение.
Теперь вернемся к нашему примеру.
Для нашего примера предлагаемые слова будем хранить в виде массива строк TArray<string>. В моделе ключ для этого массива назовем “suggestion_list”. Вот таким вот способом выглядит задание нашего списка слов в модель любого TEdit:
var SuggestionList: TArray<string>; begin SuggestionList := ['Apple', 'Arc', 'Auto', 'Ask', 'Allday', 'Alltime', 'Orange', 'Pineapple']; Edit1.Model.Data['suggestion_list'] := TValue.From<TArray<string>>(SuggestionList);
Теперь мы знаем, как в любой презентационный контрол передать любые данные. Давайте перейдем к рассмотрению создания нашего представления.
Создание и регистрация нового представления на базе TStyledEdit
Для хранения нашего нового представления лучше создать отдельный файл. В дальнейшем можно будет его легко повторно использовать в других проектах, путем простого добавления файла к вашему проекту. Назовем файл: FMX.Edit.Autocomplete.pas
Создаем новое представление. В качестве базового класса выберем TStyledEdit – это стилевая презентация TEdit. Именно она используется по умолчанию во всех TEdit. Назовем наш класс представления TStyledAutocompleteEdit. Приставка Styled — означает, что эта презентация использует стили и не нативная. Название представления может быть любым и ни как не влияет на дальнейшее использование в TEdit.
Для версий до XE9 (включительно)
Сразу же создадим класс посредник для доступа к нашему представлению. Как мы помним из 1 части статьи, в фабрике мы регистрируем не представления, а их прокси. Поэтому вначале создаем прокси для нашего представления.
Создаем класс посредник представления, называем его «TStyledAutocompleteEditProxy«.
Перекрываем метод CreateReceiver, который должен вернуть наше представление. По сути этот метод и осуществляет связь прокси с презентацией.
uses FMX.Edit.Style, FMX.Controls.Presentation; type TStyledAutocompleteEdit = class(TStyledEdit) end; TStyledAutocompleteEditProxy = class(TPresentationProxy) protected function CreateReceiver: TObject; override; end; implementation { TStyledAutocompleteEditProxy } function TStyledAutocompleteEditProxy.CreateReceiver: TObject; begin Result := TStyledAutocompleteEdit.Create(nil); end; end.
Для версий с XE10
uses FMX.Edit.Style, FMX.Controls.Presentation, FMX.Presentation.Style; type TStyledAutocompleteEdit = class(TStyledEdit) end; TStyledAutocompleteEditProxy = TStyledPresentationProxy(TStyledAutoCompleteEdit); implementation end.
На текущий момент мы создали свое представление на базе стилевого представления эдита и создали прокси для доступа к представлению.
Теперь нам нужно зарегистрировать наше представление, чтобы оно автоматически стало использоваться нашими TEdit. В первой части я рассказывал о фабрике представлений и о процессе загрузки представления контролом. Поэтому сейчас просто приведу код регистрации.
Есть два варианта, как нам зарегистрировать наше представление:
- Заменить стандартное на наше. В этом случае все поля ввода TEdit в приложении будут использовать наше представление вместо штатного.
- Отдельно зарегистрировать наше представление. В этом случае все TEdit в приложении продолжат использовать стандартное представление по умолчанию. Чтобы подключить наше, нужно будет дополнительно им указать название нашего представления.
Замена представления TEdit по умолчанию на своё
Если мы идем первым путем, то мы вначале удаляем из фабрика прокси стандартного представления, а потом регистрируем с таким же именем прокси нашего представления. Название стилизованного представления TEdit — «Edit-Style«.
uses FMX.Presentation.Factory; initialization TPresentationProxyFactory.Current.Unregister('Edit-style'); TPresentationProxyFactory.Current.Register('Edit-style', TStyledAutocompleteEditProxy); finalization TPresentationProxyFactory.Current.Unregister('Edit-style'); TPresentationProxyFactory.Current.Register('Edit-style', TStyledEditProxy); end.
Теперь если вы запустите приложение, то все поля TEdit будут использовать наше представление.
Регистрация дополнительного представления TEdit
Если же мы идем вторым путем, то вначале выбираем имя для нашего представления, например, «AutocompleteEdit-style«. А затем регистрируем его прокси.
uses FMX.Presentation.Factory; initialization TPresentationProxyFactory.Current.Register('AutocompleteEdit-style', TStyledAutocompleteEditProxy); finalization TPresentationProxyFactory.Current.Unregister('AutocompleteEdit-style'); end.
Теперь нам нужно указать для наших полей ввода TEdit, чтобы они использовали наше представление вместо стандартного. Для этого у всех презентационных контролов есть специальное событие
TPresentedControl.OnPresentationNameChoosing(Sender: TObject; var PresenterName: string);
Которое вызывается до момента поиска презентации в фабрике. PresentationName содержит название представления, которое будет запрошено у фабрики. При помощи этого события, вы можете подставить название вашего представления вместо стандартного. В нашем случае нужно вернуть «AutocompleteEdit-style«.
procedure TForm8.Edit1PresentationNameChoosing(Sender: TObject; var PresenterName: string); begin PresenterName := 'AutocompleteEdit-style'; end;
Реализация функции AutoComplete
Теперь займемся реализацией самой функции AutoComplete. Добавим в наше представления поле для хранения списка слов с предложениями FSuggestions.
Для создания выпадающего списка воспользуемся компонентом TPopup (выпадающий список) с помещенным внутрь списком слов TListBox. Добавим поля для хранения этих компонентов и добавим код по их созданию в презентацию:
uses FMX.Edit.Style, FMX.Controls.Presentation, FMX.Controls.Model, FMX.Presentation.Messages, FMX.Controls, FMX.ListBox, System.Classes, System.Types; type TStyledAutocompleteEdit = class(TStyledEdit) private FSuggestions: TArray<string>; FPopup: TPopup; FListBox: TListBox; FDropDownCount: Integer; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; end; implementation { TStyledAutocompleteEdit } constructor TStyledAutocompleteEdit.Create(AOwner: TComponent); begin inherited; FPopup := TPopup.Create(nil); FPopup.Parent := Self; FPopup.PlacementTarget := Self; FPopup.Placement := TPlacement.Bottom; FPopup.Width := Width; FListBox := TListBox.Create(nil); FListBox.Parent := FPopup; FListBox.Align := TAlignLayout.Client; FDropDownCount := 5; end; destructor TStyledAutocompleteEdit.Destroy; begin FPopup := nil; FListBox := nil; inherited; end;
FDropDownCount будет содержать количество слов показываемых в выпадающем списке без прокрутки.
PlacementTarget — указывает контрол относительно которого будет появляться выпадающий список.
Placement — указывает местоположение выпадающего списка относительно PlacementTarget.
Пока в этом примере FDropDownCount будет жестко задаваться в конструкторе. Домашнее задание, сделать задание этого свойства через модель TEdit.
Обратите внимание, что в деструкторе мы не удаляем FPopup и FListBox. Эти компоненты автоматически будут удалены при удалении представления, так как они прикреплены к презентации. Все объекты структуры компонентов формы удаляются рекурсивно. Поэтому наши контролы, так же будут автоматически удалены. Так же для корректной отработки ARC на мобильных платформах, не забываем обнулить ссылки на них.
Общий алгоритм работы подстановки
Когда пользователь вводит новую букву в TEdit, мы фильтруем общий словарь со словами и выбираем те слова, которые начинаются с введенного текста в TEdit. Далее если в результате фильтрования у нас есть слова, то заполняем выпадающий список этими словами и отображаем его, иначе скрываем выпадающий список.
Если пользователь выбирает слово в выпадающем списке, то мы должны вставить его в текущее поле TEdit.
Получение представлением слов из модели
Как в прошлом разделе мы отметили, при изменении данных в модели, модель шлет сообщение с кодом MM_DATA_CHANGED при изменении данных в модели через Data. Перехватываем это сообщение в презентации, чтобы вытащить список слов для подстановки.
type TStyledAutocompleteEdit = class(TStyledEdit) private FSuggestions: TArray<string>; FPopup: TPopup; FListBox: TListBox; FDropDownCount: Integer; protected procedure MMDataChanged(var AMessage: TDispatchMessageWithValue<TDataRecord>); message MM_DATA_CHANGED; //.... implementation procedure TStyledAutocompleteEdit.MMDataChanged(var AMessage: TDispatchMessageWithValue<TDataRecord>); var Data: TDataRecord; begin Data := AMessage.Value; if Data.Value.IsType < TArray < string >> and (Data.Key = 'suggestion_list') then FSuggestions := AMessage.Value.Value.AsType<TArray<string>>; end;
Заполнение выпадающего списка словами
Метод заполнения списка нашими словами будет таким:
procedure TStyledAutocompleteEdit.RebuildSuggestionList; var Word: string; begin FListBox.Clear; FListBox.BeginUpdate; try for Word in FSuggestions do if Word.ToLower.StartsWith(Model.Text.ToLower) then FListBox.Items.Add(Word); finally FListBox.EndUpdate; end; end;
Пробегаемся по всем нашим словами из словаря и сравниваем текущий ввод с каждым словом. Те слова, которые начинаются с введенного текста попадают в TListBox. Внутри стилевого представления мы имеем доступ к модели TEdit.
Примечание: Вы можете попробовать сделать разные критерии подбора слова из словаря: например, сделать подбор по последнему введенному слова в TEdit, а не по всему тексту.
Вычисление размера выпадающего списка
После заполнения выпадающего списка словами, нам нужно вычислить высоту выпадающего списка на основании количества слов и FDropDownCount:
procedure TStyledAutocompleteEdit.RecalculatePopupHeight; begin FPopup.Height := FListBox.ListItems[0].Height * Min(FDropDownCount, FListBox.Items.Count) + FListBox.BorderHeight; FPopup.PopupFormSize := TSizeF.Create(FPopup.Width, FPopup.Height); end;
Если меняется ширина TEdit и у нас открыто выпадающий список, мы должны изменить ширину списка равной ширине TEdit. Когда TEdit меняет размер, он отправляет сообщение с кодом PM_SET_SIZE. Перехватываем и адаптируем ширину попапа к ширине контрола:
type TStyledAutocompleteEdit = class(TStyledEdit) private //... protected procedure PMSetSize(var AMessage: TDispatchMessageWithValue<TSizeF>); message PM_SET_SIZE; //.... implementation procedure TStyledAutocompleteEdit.PMSetSize(var AMessage: TDispatchMessageWithValue<TSizeF>); begin inherited; FPopup.Width := Width; end;
Следим за вводом текста и обновляем список слов
Ловим момент изменения текста в TEdit через виртуальный метод представления DoChageTracking.
procedure TStyledAutocompleteEdit.DoChangeTracking; function HasSuggestion: Boolean; var I: Integer; begin I := 0; Result := False; while not Result and (I < Length(FSuggestions)) do begin Result := FSuggestions[I].ToLower.StartsWith(Model.Text.ToLower); if not Result then Inc(I) else Exit(Result); end; end; function IndexOfSuggestion: Integer; var Found: Boolean; I: Integer; begin Found := False; I := 0; Result := -1; while not Found and (I < FListBox.Count) do begin Found := FListBox.Items[I].ToLower.StartsWith(Model.Text.ToLower); if not Found then Inc(I) else Exit(I); end; end; begin inherited; if HasSuggestion then begin RebuildSuggestionList; RecalculatePopupHeight; Index := IndexOfSuggestion; if Model.Text.IsEmpty then FListBox.ItemIndex := -1 else FListBox.ItemIndex := Index; FPopup.IsOpen := True; end else FPopup.IsOpen := False; end;
Навигация по выпадающему списку при помощи клавиатуры
Добавляем управление выбора слова в выпадющем списке с клавиатуры:
procedure TStyledAutocompleteEdit.KeyDown(var Key: Word; var KeyChar: Char; Shift: TShiftState); begin inherited; case Key of vkReturn: if FListBox.Selected <> nil then begin Model.Text := FListBox.Selected.Text; Edit.GoToTextEnd; FPopup.IsOpen := False; end; vkEscape: FPopup.IsOpen := False; vkDown: if FListBox.Selected <> nil then FListBox.ItemIndex := Min(FListBox.Count - 1, FListBox.ItemIndex + 1); vkUp: if FListBox.Selected <> nil then FListBox.ItemIndex := Max(0, FListBox.ItemIndex - 1); end; end;
Все готово.
Теперь можно запустить приложение и посмотреть результат.
В текущем примере не сделан выбор слова мышкой из выпадающего списка. Это домашнее задание для любознательных. Чтобы его сделать нужно повесить обработчик выбора элемента в TListBox.
Итог
Получено представление для TEdit, которое добавляет функцию автозаполнения поля. Теперь если вы хотите добавить поддержку автозаполнения в любом проекте, достаточно добавить файл презентации в ваш проект.
Код рабочего проекта: Пример для XE8, Пример для XE10