Проверка маски на полноту ввода в FMX Delphi — часть 2

Проверка маски на полноту ввода в FMX Delphi - часть 2

Дорогие читатели, с наступившим Вас новым годом и грядущим праздником Рождества Христова!

В предыдущей статье я рассказывал как сделать маску ввода в TStringGrid фреймворка FMX. В этой статье маска организовывалась через компонент TEdit, который подставлялся поверх ячейки TStringGrid. Но там же я упомянул о еще одном способе — через событие OnKeyDown.

Также я написал, что об этом методе я расскажу в следующей статье, то есть в этой... Ну что же, давайте рассмотрим этот метод.

Через событие OnKeyDown объекта TStringGrid

Допустим, что наш объект TStringGrid Мы назовем как StringGridAction, а форму как frmActions Объявим процедуру в классе формы:

procedure StringGridActionKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState);

А потом в разделе implementation модуля формы там, где у нас все обработчики, мы опишем нашу процедуру:

procedure TfrmActions.StringGridActionKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState);
begin
    showmessage('Нажата клавиша!');
end;

Теперь у нас есть обработчик. Это просто пробный обработчик. Но у него есть еще одна небольшая проблема. Дело в том, что наш обработчик OnKeyDown отлично срабатывает на любое нажатие кнопки на клавиатуре, когда у нас TStringGrid имеет фокус. Но когда я щелкнул по ячейке, чтобы отредактировать значение и нажал клавишу, то обработчик уже не выдал сообщение «Нажата клавиша!».

Разобравшись в чем дело, я понял, что когда мы начинаем редактировать ячейку фокус передается от TStringGrid его активной ячейке. В отличии от VCL, где обработка нажатия клавиш осуществляется самим DBGrid, в FMX этого не происходит. Я не оговорился про DBGrid. Именно он, а не TStringGrid. Почему? Потому что вся идеология этих двух статей заточена как бы для DBGrid, но поскольку его нет в FMX, то рассматривался весь процесс реализации того же самого функционала в TStringGrid, который заменяет DBGrid в VCL (жаль, что нет аналогичного компонента). Поэтому я и сравниваю как бы немножко разные сетки.

Ну так вот, в FMX непосредственно перед началом редактирования ячейки создается временный редактор, а после редактирования он удаляется. Поэтому вышеупомянутый код не работает. Фокус передается этому временному объекту и событие OnKeyDown объекта TStringGrid просто-напросто не срабатывает.

При работе с компонентами типа TStringGrid, во время редактирования содержимого ячейки появляется специальный элемент управления (по типу TEditInplace (от TEdit)), который отображается поверх самой ячейки. Этот внутренний элемент управляется механизмом компонента, и непосредственного публичного доступа к нему нет.

В процессе поиска ответов на свои вопросы я захотел узнать имя класса этого создаваемого динамического объекта. В документации к FMX я ничего об этом не нашел. Может мне просто не повезло и я не увидел. Но факт фактом. Затем я решил попробовать узнать имя класса с помощью кода:

ShowMessage(ActiveControl.ClassType.ClassName);

И вот столкнулся я с тем: «А в какое событие сетки его прописать, чтобы он сработал?». Дело в том, что все события TStringGrid возникают либо непосредственно перед созданием этого временного объекта, либо сразу после его уничтожения. Не за что и ухватиться. Такой, я бы сказал, серьезный пробел в FMX, который в будущих версиях разработчикам Embarcadero каким то образом нужно решить — так я подумал, но позже мое мнение немного изменилось.

Ну действительно, очень уж не удобно пытаться отловить объект, к которому никак не подобраться. Конечно в FMX есть событие OnCreateCustomEditor объекта TStringGrid. Это событие используется для кастомизации процесса создания редактора ячейки в визуальных компонентах таблиц, таких как TStringGrid. Оно позволяет разработчику перехватывать создание стандартного редактора (обычно это поле ввода типа TEdit) и заменять его собственным компонентом для конкретных нужд приложения. Звучит довольно заманчиво и красиво.

Ну опять-таки, срабатывает данное событие непосредственно перед созданием встроенного редактора ячейки. Оно даёт возможность изменять тип и поведение редактора, задавая собственный интерфейс для ввода значений в ячейку таблицы.

То есть, это получается примерно тоже самое, как мы создавали собственный компонент маски из TEdit и подкладывали его поверх ячейки в предыдущей статье. Но это не совсем так, поскольку мы просто накладывали наш TEdit поверх созданного FMX динамического объекта для редактирования ячейки. А вот событие OnCreateCustomEditor позволяет, на сколько я понял, перехватить сам динамический объект на этапе его создания для того, чтобы вместо него подставить, собственно, свой. А уже там, в своем объекте, например, в TEdit, можно будет использовать событие onKeyDown для контроля ввода. Ну что ж, если так, то это хорошая идея и я зря оклеветал Embarcadero. Давайте проверим это вместе.

А теперь на секундочку возвратимся к тому, что я писал несколькими абзацами ранее о том, что я не могу узнать класс динамически создаваемого объекта для правки ячейки TStringGrid. Я писал, что для этого нет подходящего события. Теперь я попробовал для этого использовать событие OnCreateCustomEditor. Я написал в нем:

procedure TfrmActions.StringGridActionCreateCustomEditor(Sender: TObject;
  const Column: TColumn; var Control: TStyledControl);
begin
      ShowMessage(sender.ClassType.ClassName);
end;

Запустил приложение, стал изменять значение ячейки и получил сообщение с типом класса TStyledGrid. Этот класс обеспечивает стилевое представление для компонентов сетки. и наследуется от FMX.ScrollBox.Style.TStyledCustomScrollBox. Он позволяет настраивать отображение элементов сетки, например, заголовков колонок и рядов, с помощью стилей, а также стилизовать элементы, создаваемые сеткой для редактирования своих значений. Ну что ж, мне кажется мы немного продвинулись в нужном направлении. После этих слов я еще долго мучался и исследовал, но в конце у меня все получилось.

Возможно есть еще один или несколько других способов это сделать, но я остановился на своем. Итак, посмотрим на мой окончательный вариант.

Пусть наш TStringGrid будет называться StringGridAction, а наше форму — frmAction. Посмотрим на фрагмент ее класса:

...
    procedure CellEditKeyDown(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState);
    procedure CellEditEnter(Sender: TObject);
    procedure CellEditExit(Sender: TObject);
    procedure CellEditChange(Sender: TObject);
    procedure StringGridActionCreateCustomEditor(Sender: TObject;
      const Column: TColumn; var Control: TStyledControl);
    procedure StringGridActionCellClick(const Column: TColumn;
      const Row: Integer);
    procedure StringGridActionEditingDone(Sender: TObject; const ACol,
      ARow: Integer);
    procedure Button1Click(Sender: TObject);
  private
     { Private declarations }
      iRow,iCol: integer;
      sDate: String;
  public
    { Public declarations }

  end;

var
   frmActions: TfrmActions;
implementation
...

Сделаем точно также, а именно объявим наши первые четыре пользовательские процедуры, а остальные объявления будут созданы Delphi автоматически позже по мере добавления кода.

Добавим в событие OnCreateCustomEditor объекта StringGridAction следующий код:

procedure TfrmActions.StringGridActionCreateCustomEditor(Sender: TObject;
  const Column: TColumn; var Control: TStyledControl);
var
     iIndexColumn: integer;//Индекс столбца
     MyObject: TComponent;
     MyEdit: TEdit;
begin
     iIndexColumn:=Column.Index;
     if iIndexColumn = 8 then
     begin
          //Либо так (но здесь дает ошибку назначение обработчиков для
          //некоторых событий, например onValidate):
          {MyObject := FindComponent('txtDateCell');
          if Assigned(MyObject) then FreeAndNil(TEdit(MyObject));
          Control:=TEdit.Create(Self);
          Control.Name:='txtDateCell';
          Control.OnKeyDown:=CellEditKeyDown;
          Control.OnEnter:=CellEditEnter;
          Control.OnValidate:=CellEditvalidate;
          Control.StyleLookup := 'EditGridStyle';}
          //либо так:
          MyObject := FindComponent('txtDateCell');
          if Assigned(MyObject) then FreeAndNil(TEdit(MyObject));
          MyEdit:=TEdit.Create(Self);
          MyEdit.Name:='txtDateCell';
          MyEdit.OnKeyDown:=CellEditKeyDown;
          MyEdit.OnEnter:=CellEditEnter;
          MyEdit.OnChange:=CellEditChange;
          MyEdit.StyleLookup := 'EditGridStyle';
          Control:=MyEdit;
          //MyObject мы не уничтожаем, потому что переменные типа
          //TComponent Delphi уничтожает автоматически
     end;
end;

Что мы здесь делаем? Мы проверяем столбец. Если он у нас восьмой (по факту девятый, потому что нумерация начинается с нуля), то процедура выполняется дальше. А дальше мы можем сделать два вида записи: либо как по верхнему варианту, либо как по нижнему.

Верхний вариант показывает ошибку при назначении обработчика некоторых событий, например onValidate. Но если такие события (методом проб и ошибок) не используются, можно воспользоваться и записью по верхнему варианту.

Я выбрал второй вариант — путем создания переменной типа MyEdit.

С помощью переменной MyObject мы проверяем, не создан ли у нас объект с именем txtDateCell. Мы ведь не хотим плодить объекты, поэтому, если он создан, то мы его уничтожаем.

Затем мы создаем объект типа TEdit и присваиваем его объектной переменной MyEdit, которую, кстати говоря, удалять после использования не нужно. FMX сама удаляет динамические объекты, созданные как временные для редактирования ячеек TStringGrid.

Переменную MyObject освобождать также не нужно — Delphi делает это автоматически.

Дальше созданному объекту мы присваиваем имя и подписываемся на события. В конце можно добавить имя стиля для нашего созданного TEdit, чтобы он вписывался в интерфейс нашего приложения. А потом мы параметру COntrol присваиваем наш созданный объект MyEdit.

Теперь посмотрим на обработчик события onEnter нашего созданного объекта:

procedure TfrmActions.CellEditEnter(Sender: TObject);
begin
      if fdActions.State=dsInsert then
         TEdit(Sender).Text:='__.__.____';
      //Обработчик для события OnExit назначаем в обработчике события OnEnter
      //Немного странно, что его нельзя анзначить сразу при создании объекта
      //в событии CreateCustomEditor, но видимо у FMX такая логика.
      TEdit(Sender).OnExit:=CellEditExit;
      //И две нижние характеристики для свойства тоже работают в этом событии
      TEdit(Sender).TextSettings.HorzAlign:=TTextAlign.Center;
      TEdit(Sender).TextSettings.VertAlign:=TTextAlign.Center;
end;

Этот обработчик срабатывает, когда наш созданный объект получает фокус ввода. Здесь все просто, не буду на это останавливаться. Далее рассмотрим событие OnExit:

procedure TfrmActions.CellEditExit(Sender: TObject);
begin
     //По умолчанию считаем, что мы выходим по ESC
     sDate:=TEdit(Sender).Text;
end;

Данное событие возникает, когда объект теряет фокус ввода. В нашем случае потеря фокуса всегда связана с уничтожением временного объекта. OnExit сработает в любом случае, нажали ли вы Enter или отказались от ввода, нажав на ESC. Оба действия приведут к закрытию временного редактора и отследить, а что же нажал пользователь — не возможно. Поэтому будем считать, что по умолчанию пользователь нажал на ESC.

Можно было бы отследить через событие onKeyDown, но меня ждал коварное разочарование, когда я наконец то понял, что при нажатии на Enter или ESC дело до события onKeyDown даже не доходит — оно просто не срабатывает, так как редактор закрывается (уничтожается) раньше, чем срабатывает событие onKeyDown. В итоге наш onKeyDown выглядит вот так:

begin
    //Вызываем процедуру формирования маски
    MaskDate(TEdit(Sender), Key, KeyChar);
end;

Ну что же, будем отслеживать по другому. А здесь мы вызовем только процедуру формирования маски MaskDate, которая описывалась ранее в статье «Компонент маски в FMX Delphi».

Чуть ранее я писал, что событие OnExit будет возникать в любом случае не зависимо от того, нажал пользователь Enter, подтвердив тем самым ввод или отказался от ввода, нажав на ESC. Поэтому в обработчике этого события я написал:

//По умолчанию считаем, что мы выходим по ESC
sDate:=TEdit(Sender).Text; 

Я принял за точку отсвета тот факт, что само по себе закрытие редактора это отказ от ввода. Но у TStringGrid есть особое событие, которое возникает только тогда, когда пользователь подтвердил ввод нажатием Enter. И это событие: OnEditingDone. И в обработчике этого события я напишу следующий код:

procedure TfrmActions.StringGridActionEditingDone(Sender: TObject; const ACol,
  ARow: Integer);
begin
    //Если пользователь нажал Enter, то в ячейку записывается значение
    //переменной sDate
    StringGridAction.Cells[iCol, iRow]:=sDate;
end;

Технически этот код можно написать и в onExit, но тогда он будет срабатывать при любом раскладе: нажали ли вы Enter или ESC. А мне нужно так, чтобы код срабатывал только при нажатии на Enter.

Еще одно очень важное событие — OnChange. Оно возникает перед потерей фокуса компонентом, но только тогда, когда значение компонента менялось (ну или предпринимались такие попытки. Значение ведь вы можете вернуть прежним). Не путать с событием OnChangeTracking. Оно возникает при каждом изменении значения в моменте даже тогда, когда компонент все-еще находится в фокусе. Итак посмотрим на обработчик события OnChange:

procedure TfrmActions.CellEditChange(Sender: TObject);
begin
   //Если набор данных не находится в режиме вставки и при это не находится
   //в режиме редактирования, то переводим его в режим редактирования
   if fdActions.State<>dsInsert then
      if fdActions.State<>dsEdit then fdActions.edit;
end;

Здесь можно было бы написать довольно коротко:

fdActions.edit;

Технически такой код переводил бы набор данных в режим редактирования при потере фокуса компонентом, но при условии того, что значение в нем изменилось. Но не тут то было. На практике временный объект TEdit, созданный для редактирования нашей сетки ведет себя не так как TEdit, размещенный на форме. А именно OnChange в нашем временном объекте срабатывает не так как ожидалось — при потере фокуса, а подобно keyDown, то есть при каждом нажатии на кнопку. Поэтому пишем чуть по другому.

У TStringGrid есть еще одно важное событие onCellClick — происходит при клике мышью на ячейку. Теоретически. Но на практике это событие возникает и при переходе на ячейку с помощью клавиш клавиатуры, что не может не радовать. И так, здесь мы присвоим нашим переменным номер текущей строки и столбца.

procedure TfrmActions.StringGridActionCellClick(const Column: TColumn;
  const Row: Integer);
begin
   iRow := StringGridAction.Row;
   iCol := StringGridAction.Col;
end;

Разместим на форме кнопку, назовем ее «Сохранить» и напишем в ней код:

procedure TfrmActions.cmdPostClick(Sender: TObject);
begin
   if (fdActions.State=dsinsert) or (fdActions.State=dsEdit)  then fdActions.post;
end;

Этот кода сохраняет набор данных, если он находится в режиме редактирования или вставки. Теперь посмотрим на событие OnBeforePost набора данных, которое возникает перед сохранением:

procedure TfrmActions.fdActionsBeforePost(DataSet: TDataSet);
var
    MyObject: TComponent;
begin
    MyObject := FindComponent('txtDateCell');
    if Assigned(MyObject) then sDate:=TEdit(MyObject).Text
       else sDate:=StringGridAction.Cells[iCol, iRow];
    if IsDateСheckMaskString(sDate,Left,Top,Height,Width) then
    begin
       sDate:=FormatDateTime('yyyy-mm-dd',strtodate(sDate));
       fdActions.FieldByName('Дата').value:=sDate;
    end else
    begin
       abort;
    end;
end;

Что я вообще хочу от всего этого? Я хочу от нашего временного объекта добиться стандартного поведения, а именно:

  • нажатие на Enter подтверждает ввод;
  • нажатие на ESC отменяет ввод и возвращает ячейку таблицы к прежнему значению;
  • переход на другую ячейку подтверждает ввод;
  • нажатие на кнопку «Сохранить» на форме подтверждает ввод.

Здесь проверяем наличие нашего созданного временного редактора с именем txtDateCell. Если он есть, то мы берем значение из него и присваиваем его переменной sDate. Эта ситуация, когда мы внесли значение во временный объект и не закрыли его нажатием на Enter или ESC.

Если же такого объекта нет, то переменной sDate присваивается значение ячейки. А оно у нас равняется либо предыдущему значение при нажатии на ESC, либо новому, если мы нажимали на Enter.

Обратите внимание на функцию IsDateСheckMaskString. Это моя пользовательская функция и она проверяет корректность ввода даты, возвращая истину в случае ее корректности. Она очень похожа на функцию IsDateСheckMask, которую я применял в предыдущей статье под названием «Проверка маски на полноту ввода в FMX Delphi — часть 1».

Разница только в том, что в IsDateСheckMask передавался в качестве параметров объект TEdit. В функции IsDateСheckMaskString в качестве параметра передается строка, содержащая дату.

В той же самой статье я писал, что разместил свою функцию в модуль my_proc. Функцию IsDateСheckMaskString я разместил тужа же. Не буду приводить код всех функций модуля, так как я их уже описывал — приведу лишь код нашей последней функции:

function IsDateСheckMaskString(sDate: String; Left: integer; Top: integer; Height: integer; Width: integer): boolean;
var
    dDate: TDateTime;
    bDate: boolean;
begin
    if sDate <> '__.__.____' then
    begin
        if not tryStrToDate(sDate, dDate) then
        begin
           bDate:=false;
           MessageCreate(Left,Top,Height,Width,'Не верная дата!');
           //abort;
        end else bDate:=true;
    end else
    begin
       bDate:=false;
       MessageCreate(Left,Top,Height,Width,'Укажите дату');
       //abort;
    end;
    Result:=bDate;
end;

Ну вот, пожалуй и все. Вроде бы ничего не забыл описать. Используя эту и предыдущую статью вы без труда сможете использовать свою собственную маску как для TEdit, так и для ввода данных через TStringGrid. Так же при вводе, например даты, вы сможете проверять ее корректность.

Вообще, в отношении проверки корректности даты на правильность и полноту ввода можно пойти и другим путем — можно проверять ее непосредственно при вводе. Придется немножко модернизировать код, но мне просто такое поведение интерфейса не нравится.

Еще следует отметить, что для ввода даты в Delphi FMX есть специальный компонент TDateEdit, похожий на обычное текстовое поле, только для даты. У TStringGrid же есть специальный тип столбца для ввода даты, называемый TDateColumn. При добавлении или редактировании даты в этом случае в качестве временного редактора ячейки TStringGrid как раз вызывается все тот же TDateEdit.

Но мне эти компоненты кажутся не удобными, а может я еще просто к ним не привык. Я, кстати никогда не пользовался и в VCL такими компонентами как TMonthCalendar и ему подобных. Всегда использовал маску.

Вторая причина, по которой я не использую в своих проектах TDateEdit заключается в том, что хоть TDateEdit и является своего рода заменой маски, но он служит «маской» только для даты. А как быть, если нужно ввести СНИЛС или государственный номер автомобиля? Все-таки потребуется полноценная маска ввода, созданием которой мы и занимались в статье «Компонент маски в FMX Delphi».

Так что, до новых встреч. Еще раз с праздниками Вас! Желаю крепкого здоровья, счастья, финансового благополучия!

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: