Продолжаем колдовать над проектом. В качестве СУБД в Data Keeper for Android будет использована SQLite. Физически данные хранятся в одном файле, а программа сможет работать с несколькими такими файлами, поэтому пора доработать фрейм DBaseList. Но прежде нужно добавить кое-что в модуль с данными UIData.

TdamUIData

В модуле добавлены два свойства:

  • BasePath – путь к базе
  • Param – массив строковых параметров с доступом по имени параметра

Путь к базе прописывается при инициализации модуля. Место, которое всегда доступно для хранения данных программы – папка с документами, путь к которой можно получить функцией TPath.GetDocumentsPath. Для Windows это будет папка “Мои документы”, для Android – одна из приватных папок приложения, добраться до которой каким-либо образом извне будет проблематично. Но такова концепция безопасности Android.

Функция TPath.Combine соединяет путь к файлу и название файла. Этот способ надежней обычной строковой конкатенации, так как использует актуальные для ОС разделители.

С помощью функции TDirectory.Exists можно проверить наличие директории, а TDirectory.CreateDirectory позволяет её создать.

Param представляет из себя упрощенный вариант работы с файлом инициализации.

uses System.IOUtils, System.IniFiles
...
  TdamUIData = class(TDataModule)
...
  private
    fBasePath: string;
    function GetParam(const AParamName:string):string;
    procedure SetParam(const AParamName:string; AValue:string);
...
  public
    property BasePath: string read fBasePath;
    property Param [const AName:string]:string read GetParam write SetParam;
...

const
  INI_FILE_NAME = 'DataKeeper.ini';

function TdamUIData.GetParam(const AParamName: string): string;
var
  tmpIniFile:TIniFile;
begin
  tmpIniFile := TIniFile.Create(TPath.Combine(TPath.Combine(TPath.GetDocumentsPath, BASE_SUBDIR ),INI_FILE_NAME));
  Result := tmpIniFile.ReadString('COMMON',AParamName,'');
  tmpIniFile.Free;
end;

procedure TdamUIData.SetParam(const AParamName: string; AValue: string);
var
  tmpIniFile:TIniFile;
begin
  tmpIniFile := TIniFile.Create(TPath.Combine(TPath.Combine(TPath.GetDocumentsPath, BASE_SUBDIR ),INI_FILE_NAME));
  tmpIniFile.WriteString('COMMON',AParamName,AValue);
  tmpIniFile.Free;
end;

procedure TdamUIData.Init(AForm: TForm);
begin
  fForm := AForm;
  // задаем папку для хранения файлов БД
  fBasePath := TPath.Combine(TPath.GetDocumentsPath, BASE_SUBDIR );
  if not TDirectory.Exists(fBasePath) then
    TDirectory.CreateDirectory(fBasePath);
end;

Отдельно стоит рассмотреть механизм отображения всплывающих сообщений в Android. Для удобства соответствующая процедура была добавлена в модуль TdamUIData. Тут требуется магия условной компиляции и подключение библиотек Androidapi, а также могущественные заклинания, выполняемые в потоке.

uses
{$IFDEF ANDROID}
  Androidapi.JNI.Widget,
  Androidapi.Helpers,
{$ENDIF}
...

type
  TToastLength = (LongToast, ShortToast);
...

procedure TdamUIData.ShowMessageToast(const AMessage: String; ADuration: TToastLength);
var
  tmpLength: integer;
begin
{$IFDEF ANDROID}
  if ADuration = ShortToast then
    tmpLength := TJToast.JavaClass.LENGTH_SHORT
  else
    tmpLength := TJToast.JavaClass.LENGTH_LONG;
  TThread.Synchronize(nil,
    procedure
    begin
      TJToast.JavaClass.makeText(TAndroidHelper.Context, StrToJCharSequence(AMessage), tmpLength).show
    end);
{$ENDIF}
{$IFDEF WINDOWS}
  ShowMessage(pMsg);
{$ENDIF}
end;Code language: Delphi (delphi)

❓ Увы и ах… более подробней о работе с классами Java я рассказать пока не могу – это отдельная большая тема, на изучение которой может уйти не один месяц…

TfraDBaseList

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

  • ActUpdateList – заполнить список файлами БД
  • ActAdd – создать файл
  • ActEdit – отредактировать название файла
  • ActCopy – скопировать файл
  • ActDelete – удалить файл
  • ActViewDetail – выбрать файл в качестве рабочего

❓ Первоначально для выбора рабочей базы я хотел задействовать нажатие на кнопку-уголок на элементе списка, но пока не смог разобраться в настройках данного компонента. К слову, на моём смартфоне у списка по умолчанию работает свайп* вправо-влево, в результате которого появляется кнопка “Delete”. Это прикольно, но мне не нужно. Надеюсь, что в самое ближайшее время разберусь с настройками TListView, а пока использую кнопку в правом нижнем углу, для которой сменил картинку на более подходящую.

Свайп (Swipe) — это движение пальцем по экрану телефона или планшета в одном направление. Чтобы сделать свайп нужно коснуться пальцем экрана смартфона в одной точке и не отрывая его провести до другой в определенном направлении. К примеру, на телефонах iPhone и Android, чтобы открыть информационную панель, нужно свайпнуть с верхней части экрана вниз.

ActUpdateList

Элегантный способ работы со списком файлов – это получить его с помощью функции TDirectory.GetFiles в виде динамического массива. Затем массив загружается в список: в поле данных записывается полное имя файла, в текст – имя файла без пути и расширения, а в детализацию – дата и время последнего обновления. Чтобы загруженные данные корректно отображались, необходимо установить свойство ItemAppearance = ImageListItemBottomDetail.

Далее следует сортировка, функция которой реализована через класс TDelegatedComparer, использующий дженерик.

Generics – дженерики, параметризованные классы, шаблоны. Обобщенные типы позволяют создавать универсальные методы и классы, к которым можно применять различные типы на выбор.

❓ Тема дженериков лично для меня новая (хотя появилась в 2008 году), поэтому в конце статьи добавил ссылки для самостоятельного изучения.

procedure TfraDBaseList.actUpdateListExecute(Sender: TObject);
var
  i: Integer;
  tmpData: TStringDynArray;
  tmpPath: String;
  tmpItem: TListViewItem;
begin
  inherited;
  //
  tmpPath := TPath.Combine(TPath.GetDocumentsPath, BASE_SUBDIR);
  tmpData := TDirectory.GetFiles(tmpPath, '*' + BASE_EXT);
  lsvMain.BeginUpdate;
  lsvMain.Items.Clear;
  for i := 0 to High(tmpData) do
  begin
    tmpItem := lsvMain.Items.Add;
    tmpItem.Data[DATA_FILENAME] := tmpData[i];
    tmpItem.Text := TPath.GetFileNameWithoutExtension(tmpData[i]);
    tmpItem.Detail := DateTimeToStr(TFile.GetLastWriteTime(tmpData[i]));
  end;
  //
  lsvMain.Items.Sort(TDelegatedComparer<TListViewItem>.Create(
    function(const LeftItm, RightItm: TListViewItem): Integer
    begin
      Result := 0;
      if LeftItm.Text > RightItm.Text then
        Result := 1
      else if LeftItm.Text < RightItm.Text then
        Result := -1;
    end));
  lsvMain.EndUpdate;
end;Code language: Delphi (delphi)

Форма ввода параметров

Для создания/редактирования/копирования от пользователя потребуется ввести название и подтвердить своё намерение. Можно было бы для этого использовать свой фрейм-наследник класса TfraEditFrame (возможно, я так и сделаю потом), но в целях изучения и освоения технологий, предоставляемых мобильными платформами, я решил задействовать TDialogServiceAsync.InputQuery(), которая умеет отображать форму ввода текстовых данных с двумя кнопками OK и CANCEL. У данной процедуры имеется четыре параметра:

  • Заголовок формы
  • Список названий для полей
  • Список значений по умолчанию
  • Процедура обработки ввода данных

Процедура обработки реализуется как анонимная процедура, в которой имеется проверка на правильность ввода и перезапуск действия в случае ошибки пользователя.

❓ Пока не ясно, как заменить текст кнопки “CANCEL” на “Отмена”.

actAdd

Для проверки имени файла на наличие недопустимых символов используется функция TPath.HasValidFileNameChars. При этом оказалось, что у Windows и Android разные требования к названиям файлов.

Функция TFile.Exists позволяет проверить существования указанного файла. В нашем случае, если файл уже существует, то пользователю предлагается ввести другое имя.

Если пользователь ошибся, процедура ввода имени перезапускается. В случае успеха создается файл с указанным именем, затем обновляется список файлов. А для того, чтобы при этом сохранялось введенное ранее значение, его приходится записывать в свойство TAction.HelpKeyword. Это вполне допустимо, так как в проекте не будет использована стандартная система помощи.

Свойство damUIData.BasePath добавлено для того, чтобы хранить в нем название папки для хранения данных. Также в модуле UIData.pas объявлена константа BASE_EXT, в которой хранится расширение для файлов базы данных.

procedure TfraDBaseList.actAddExecute(Sender: TObject);
var
  tmpName: string;
begin
  if actAdd.HelpKeyword = '' then
    tmpName := '<Новая>'
  else
    tmpName := actAdd.HelpKeyword;
  // свойство HelpKeyword используется для хранения названия по умолчанию
  TDialogServiceAsync.InputQuery('Новая база данных', ['Название'], [ tmpName ],
    procedure(const AResult: TModalResult; const AValues: array of string)
    var
      tmpFileName: string;
    begin
      if AResult = mrOk then
      begin
        actAdd.HelpKeyword := AValues[0];
        if (not TPath.HasValidFileNameChars(AValues[0], false)) or (AValues[0]='')  then
        begin
          ShowMessage('Недопустимое имя файла');
          actAdd.Execute; // если нужно перезапустить ввод
          exit;
        end;
        tmpFileName := TPath.Combine(damUIData.BasePath, AValues[0] + BASE_EXT);
        if TFile.Exists(tmpFileName) then
        begin
          ShowMessage('Такой файл уже есть');
          actAdd.Execute; // если нужно перезапустить ввод
          exit;
        end;
        TFile.Create(tmpFileName);
        actUpdateList.Execute;
      end;
      actAdd.HelpKeyword := '';
    end);
end;Code language: PHP (php)

actEdit

Метод actEditExecute перекрывает наследуемый метод класса предка, причем вызов inherited не производится, так как логика работы метода предка не используется.

Для смены имени файла используется функция TFile.Move. Так как папка при переименовании остается прежней, то необходимо извлечь имя папки из полного названия файла с помощью функции TPath.GetDirectoryName.

В реализации можно заметить использование метода damUIData.ShowMessageToast – отображение всплывающих сообщений, которые в Андроиде называют тостами.

procedure TfraDBaseList.actEditExecute(Sender: TObject);
var
  tmpName: string;
begin
  if ItemIndex = -1 then
    damUIData.ShowMessageToast('Выберите запись', ShortToast)
  else if actEdit.HelpKeyword = '' then
    tmpName := (lsvMain.Items.Item[ItemIndex] as TListViewItem).Text
  else
    tmpName := actEdit.HelpKeyword;
  TDialogServiceAsync.InputQuery('Редактирование базы данных', ['Новое название'], [tmpName],
    procedure(const AResult: TModalResult; const AValues: array of string)
    var
      tmpSourse: string;
      tmpTarget: string;
    begin
      if AResult = mrOk then
      begin
        actEdit.HelpKeyword := AValues[0];
        if not TPath.HasValidFileNameChars(AValues[0], false) then
        begin
          ShowMessage('Недопустимое имя файла');
          actEdit.Execute; // перезапустить ввод
        end
        else
        begin
          tmpSourse := (lsvMain.Items.Item[ItemIndex] as TListViewItem).Data[DATA_FILENAME].AsString;
          tmpTarget := TPath.Combine(TPath.GetDirectoryName(tmpSourse), AValues[0] + BASE_EXT);
          if tmpSourse <> tmpTarget then
          begin
            TFile.Move(tmpSourse, tmpTarget);
            actUpdateList.Execute;
          end;
        end;
      end;
      actEdit.HelpKeyword := '';
    end);
end;Code language: Delphi (delphi)

actCopy

Копирование похоже на переименование, но файл базы данных копируется функцией TFile.Copy. В процессе копирования может выясниться, что выбранный файл назначения уже существует. Здесь нас выручит окно диалога, которое отображается процедурой TDialogService.MessageDialog().

procedure TfraDBaseList.actCopyExecute(Sender: TObject);
var
  tmpName: string;
begin
  if ItemIndex = -1 then
    damUIData.ShowMessageToast('Выберите запись', ShortToast)
  else
  begin
    if actCopy.HelpKeyword = '' then
      tmpName := (lsvMain.Items.Item[ItemIndex] as TListViewItem).Text
    else
      tmpName := actCopy.HelpKeyword;
    TDialogServiceAsync.InputQuery('Копирование базы данных', ['Имя копии'], [tmpName],
      procedure(const AResult: TModalResult; const AValues: array of string)
      var
        tmpSourse: string;
        tmpTarget: string;
      begin
        if AResult = mrOk then
        begin
          actCopy.HelpKeyword := AValues[0];
          if (not TPath.HasValidFileNameChars(AValues[0], false)) or (AValues[0] = '') then
          begin
            ShowMessage('Недопустимое имя файла');
            actCopy.Execute; // повторяем ввод
            exit;
          end;
          tmpSourse := (lsvMain.Items.Item[ItemIndex] as TListViewItem).Data[DATA_FILENAME].AsString;
          tmpTarget := TPath.Combine(TPath.GetDirectoryName(tmpSourse), AValues[0] + BASE_EXT);
          if tmpSourse = tmpTarget then
          begin
            ShowMessage('Имя должно отличаться');
            actCopy.Execute; // повторяем ввод
            exit;
          end;
          if not TFile.Exists(tmpTarget) then
          begin
            TFile.Copy(tmpSourse, tmpTarget, True);
            actUpdateList.Execute;
          end
          else
          begin
            TDialogService.MessageDialog('Такой файл уже существует. Перезаписать?', System.UITypes.TMsgDlgType.mtConfirmation,
              [System.UITypes.TMsgDlgBtn.mbYes, System.UITypes.TMsgDlgBtn.mbNo], System.UITypes.TMsgDlgBtn.mbYes, 0,
              procedure(const AResult: TModalResult)
              begin
                if AResult = mrYES then
                begin
                  TFile.Copy(tmpSourse, tmpTarget, True);
                  actUpdateList.Execute;
                end
                else
                  actCopy.Execute; // повторяем ввод
              end);
          end;
        end;
        actCopy.HelpKeyword := '';
      end);
  end;
end;Code language: Delphi (delphi)

actDelete

Используется базовый механизм удаления, то есть обработчик события actDelete.onExcecute находится в родительском классе и выгладит так:

procedure TfraViewFrame.actDeleteExecute(Sender: TObject);
begin
  inherited;
  // проверить, выбрана ли запись
  if ItemIndex <> -1 then
    TDialogService.MessageDialog(fDeleteMessage, System.UITypes.TMsgDlgType.mtConfirmation, [System.UITypes.TMsgDlgBtn.mbYes, System.UITypes.TMsgDlgBtn.mbNo],
      System.UITypes.TMsgDlgBtn.mbYes, 0,
      procedure(const AResult: TModalResult)
      begin
        if AResult = mrYES then
          DeleteRecord;
      end)
  else
    damUIData.ShowMessageToast('Выберите запись', ShortToast);
end;Code language: Delphi (delphi)

Сначала проверяется свойство фрейма ItemIndex, а в случае, если запись выбрана, вызывается виртуальный метод DeleteRecord.

function TfraDBaseList.DeleteRecord: boolean;
var
  tmpFileName: string;
begin
  tmpFileName := (lsvMain.Items.Item[ItemIndex] as TListViewItem).Data[DATA_FILENAME].AsString;
  TFile.Delete(tmpFileName);
  inherited;
end;Code language: PHP (php)

Внимательный читатель может спросить: почему свойство ItemIndex находится в классе TfraViewFrame, у которого даже нет компонента отображения данных? Отвечаю – ему здесь самое место, так как заранее неизвестно, какой именно компонент отображения данных будет у наследников: список (TListView) или дерево (TTreeView). А для получения информации о номере выбранного элемента послужит виртуальная функция GetItemIndex(), которую необходимо перекрыть в классах с компонентами отображения данных. Подробнее об устройстве классов TfraViewFrame, TfraListFrame и TfraTreeFrame я расскажу при рассмотрении конструктора виртуальной структуры БД. Пока же отмечу, что удаление элемента из списка осуществляется в классе TfraListFrame.

function TfraListFrame.DeleteRecord: boolean;
begin
  lsvMain.Items.Delete( lsvMain.ItemIndex );
end;Code language: Delphi (delphi)

actViewDetail

Выбранный файл помещается в параметр PAR_DB_FILE_NAME и фрейм закрывается (actBack.Execute).

procedure TfraDBaseList.actViewDetailExecute(Sender: TObject);
// используется для выбора базы данных
begin
  inherited;
  if ItemIndex = -1 then
    damUIData.ShowMessageToast('Выберите запись', ShortToast)
  else
  begin
    damUIData.Param[PAR_DB_FILE_NAME] := (lsvMain.Items.Item[ItemIndex] as TListViewItem).Data[DATA_FILENAME].AsString;
    actBack.Execute;
  end;
end;Code language: Delphi (delphi)

За кадром остался обновленный механизм управления видимостью фреймов, но об этом расскажу в следующий раз.

Ссылки

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

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