Новый подход разработки компонентов FireMonkey “Контрол – Модель – Презентация”. Часть 2. TEdit с автозавершением

Пример работы AutoComplete в TEditПродолжим тему обзора нового подхода по разделению контрола на модель и презентацию, описанную здесь. И в этой статье рассмотрим практическое применение этого подхода на примере добавления функции авто завершения ввода в TEdit.

Код рабочего проекта: Пример для XE8Пример для XE10


Первое, что мы должны решить, где хранить список предлагаемых слов для подстановки. Ответ прост – естественно в модели TEdit. Но класс модели TCustomEditModel не имеет для этого зарезервированные поля/свойства вроде SuggestionList. Поэтому естественное желание заключается в расширении класса модели и добавлении в свой класс этого поля. Но удержимся от этого порыва, и посмотрим, что модель имеет для нас кое-что другое в обход создания нового класса.

Модель позволяет хранить данные любых типов без наследования

Давайте получше взглянем на базовый класс модели TDataModel. Он является наследником класса TMessageSender, который в свою очередь предоставляет функциональность по отправке сообщений. TDataModel добавляет дополнительно механизм для хранения данных любого типа с использованием TValue. Это значит, что мы можем сохранить в TDataModel любые наши данные, любых типов не создавай отдельного класса модели. Для этого используется тип универсального хранения данных любого типа TValue.

Я не буду рассматривать о всех возможностях TValue. Основная идея в том, что TValue позволяет поместить в себя данные любого типа. С остальными подробностями можно ознакомиться тут (XE7 Doc wiki).

А раз мы можем поместить в модель наши данные, то значит:

  1. Данные будут автоматически доступны нам в нашем представлении.
  2. Не требуется создание отдельного класса модели.

Ниже приведен пример для задания и обратного получения данных строкового типа, массива и обработчика события в модель поля ввода 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. В первой части я рассказывал о фабрике представлений и о процессе загрузки представления контролом. Поэтому сейчас просто приведу код регистрации.

Есть два варианта, как нам зарегистрировать наше представление:

  1. Заменить стандартное на наше. В этом случае все поля ввода TEdit в приложении будут использовать наше представление вместо штатного.
  2. Отдельно зарегистрировать наше представление. В этом случае все 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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *