Продолжаем колдовать над проектом. В качестве СУБД в 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
![](https://k245.ru/wp-content/uploads/2024/06/image-14.png)
На фрейме находится список доступных баз данных. По факту это список файлов в специально предназначенной для этого папке. Добавлена реализация для действий, связанных с кнопками на нижней панели инструментов.
- 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)
За кадром остался обновленный механизм управления видимостью фреймов, но об этом расскажу в следующий раз.
![](https://k245.ru/wp-content/uploads/2024/06/DBaseList.jpeg)
Ссылки
- ПрогХаус. System.IOUtils – удобные утилиты Delphi для работы с файлами и директориями
- ПрогХаус. Использование стандартных дженериков Delphi для работы с наборами данных
- TDelphiBlog Обобщённое программирование(generics) в Delphi 2009 для Win32.
- Хабр. Применяем дженерики в RAD Studio Delphi. Создаем библиотеку сортировки списков однотипных объектов