В предыдущей статье (#1) я обещал, что продолжу разработку файлового менеджера и добавлю новый функционал:
2) Копирование/перемещение файлов и папок
3) Более дружелюбный интерфейс приложения
Видео в конце поста.
Upd (20.04.14). Проверил код на Delphi XE6 и добавил исходники для новой версии IDE.
Начал я с последнего пункта.
Решил сделать пару иконок, добавить их к пунктам в листбоксе, в зависимости от типа (папка или файл). Так визуально становилось легче определять папки, для дальнейшего перемещения.
Сразу надо оговориться, что я никогда не рисовал иконок и совсем не разбирался с иконками в Android’е. Время пришло! Начал читать в интернете статьи и рекомендации от гугла (раз Metrics and Grids, два: Iconography, три: Devices and Displays), в итоге запутался…
Суть вопроса в том, что для разных экранов (с разным dpi) нужно несколько вариантов одной картинки. Но мне нужно было загружать подходящую картинку в «ListBoxItem», а загрузить туда сразу все варианты нельзя, только одну картинку. Поэтому я не мог решить какой вариант картинки использовать.
Пришлось обратиться за консультацией к Профессионалу (Ярославу Бровину). Ярослав всё подробно объяснил и привёл оригинальное решение для данного вопроса. Для всех кому интересно описание и решение, посетите форум: Как использовать иконки разного качества для экранов с разным DPI?
Ярослав, если вы читаете это сообщение, ещё раз, Спасибо вам!
В проекте я использовал иконки, базовый размер 48x48:
Для кнопок:
- MDPI - 1X - 32x32
- HDPI - 1,5X - 48x48
- XHDPI - 2X - 64x64
- XXHDPI – 3X - 96x96
Для листбокса:
- MDPI - 1X - 48x48
- HDPI - 1,5X - 64x64
- XHDPI - 2X - 96x96
Все используемые иконки, в том числе сделанные мной (строго не судите, я вообще рисовать не умею :) залил на гугл драйв (в конце статьи), вдруг кому-то пригодится…
Также теперь подключается стиль, специально сделанный только для «ListBox»(listboxitemnodetail), уменьшающий расстояние между картинкой и текстом в «ListBoxItem».
Вообще для удобства пользователя сделал много чего, теперь вроде стало удобнее пользоваться приложением.
Создание файлов и папок
Принцип: жмём кнопку «Создать файл» или «Создать папку», появляется окно для ввода имени, вводим имя, жмём кнопку «Создать» и файл/папка появляются в списке.
Проблема возникла с созданием окна для ввода имени. Я хотел сделать отдельную форму и вызывать её как модальное окно, но когда всё сделал, обнаружил, что на Android’е вместо прозрачной формы появляется чёрная. Начал экспериментировать, если выставить «FormStyle := fsPopup», то форма появляется прозрачная, но неправильно позиционируется (эта задача решаема) и если на такой форме есть «TEdit», то вы не сможете ни чего ввести, т.к. клавиатура просто не появится. Ярослав был оповещён о подобных проблемах и сказал, что баг уже известен и будет исправлен, также он посоветовал использовать «TCustomPopupForm». В то время я уже делал другой вариант (как временный), который описал в предыдущей статье. Внимание! В данной версии файлового менеджера не используется «TCustomPopupForm» (Почитать можно тут Всплывающие формы в XE5).
Далее техническая часть вопроса. Введённое имя нужно проверять на запрещённые символы. Написал функцию проверки, в коде это «function CheckName». Описывать проверки на существование файла/папки, обработку нажатия кнопок «Enter» и «HardwareBack» я не буду.
Для создания файлов и папок используем методы «TFile.Create» и «TDirectory.CreateDirectory»
Копирование/перемещение файлов и папок
Нам понадобится 3 кнопки «Копировать», «Вставить», «Вырезать». В коде главной кнопкой будет, кнопка «Вставить».
Принцип: Выбираем пункт (отмечаем галочкой) в «ListBox», жмём кнопку «Копировать/Вырезать», перемещаемся в другую папку, жмём кнопку «Вставить», готово.
Для копирования используем методы «TDirectory.Copy» и «TFile.Copy». Для перемещения «TDirectory.Move» и «TFile.Move». Ловим исключения – выдаём сообщения :).
Переписал процедуру удаления файлов и папок, теперь работает быстрее.
Структура проекта:
Код приложения (Delphi XE5 Update 2):
{
******************************************************************************
FileManager2 for Android
Autor: Andrey Yefimov (Contact: http://delphifmandroid.blogspot.ru/)
Use only non-commercial purposes. Necessarily indicating the authorship.
Используйте только в некоммерческих целях. Обязательно с указанием авторства.
******************************************************************************
}
unit Unit1;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
FMX.ListBox, FMX.Layouts, FMX.StdCtrls, FMX.Objects, FMX.Edit, FMX.Effects;
type
TForm1 = class(TForm)
ListBox1: TListBox;
Label1: TLabel;
ToolBar1: TToolBar;
SpeedButton1: TSpeedButton;
SpeedButton2: TSpeedButton;
SpeedButton3: TSpeedButton;
ImageBook: TStyleBook;
SpeedButton4: TSpeedButton;
Image1: TImage;
StyleBook1: TStyleBook;
Image2: TImage;
Dialog: TRectangle;
ToolBar2: TToolBar;
Layout1: TLayout;
DialogEdit: TEdit;
DialogTitle: TLabel;
SpeedButton5: TSpeedButton;
SpeedButton6: TSpeedButton;
ShadowEffect1: TShadowEffect;
DialogFon: TRectangle;
DialogError: TLabel;
SpeedButton7: TSpeedButton;
Image3: TImage;
SpeedButton8: TSpeedButton;
ToolBar3: TToolBar;
Image4: TImage;
SpeedButton9: TSpeedButton;
Image5: TImage;
procedure FormCreate(Sender: TObject);
procedure ListBox1ItemClick(const Sender: TCustomListBox;
const Item: TListBoxItem);
procedure SpeedButton1Click(Sender: TObject);
procedure FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
Shift: TShiftState);
procedure ListBox1ChangeCheck(Sender: TObject);
procedure SpeedButton2Click(Sender: TObject);
procedure SpeedButton3Click(Sender: TObject);
procedure SpeedButton5Click(Sender: TObject);
procedure SpeedButton6Click(Sender: TObject);
procedure SpeedButton4Click(Sender: TObject);
procedure DialogEditChangeTracking(Sender: TObject);
procedure SpeedButton8Click(Sender: TObject);
procedure SpeedButton7Click(Sender: TObject);
procedure SpeedButton9Click(Sender: TObject);
private
procedure AddListItem(list: array of string; itype: string);
procedure TotalWork(path_tr: string; clear: boolean);
procedure OnOffButton(del, add, copy, cut: boolean);
{ Private declarations }
public
function GetImage(const AImageName: string): TBitmap;
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
uses
System.IOUtils, System.Generics.Collections, Generics.Defaults,
FMX.Helpers.Android, Androidapi.JNI.JavaTypes,
Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Webkit;
var
path: string; // Здесь будем хранить путь
ItemsCheck: array of array of string; // Массив с помеченными пунктами
function CompareLowerStr(const Left, Right: string): Integer;
begin
Result := CompareStr(AnsiLowerCase(Left), AnsiLowerCase(Right));
end;
{Функция получения картинки из ImageBook'а(Автор: Ярослав Бровин)}
function TForm1.GetImage(const AImageName: string): TBitmap;
var
StyleObject: TFmxObject;
Image: TImage;
begin
StyleObject := ImageBook.Style.FindStyleResource(AImageName);
if (StyleObject <> nil) and (StyleObject is TImage) then
begin
Image := StyleObject as TImage;
// Здесь мы получим картинку нужного dpi (Scale)
Result := Image.Bitmap;
end
else
Result := nil;
end;
{Процедура для вставки массивов в ListBox}
procedure TForm1.AddListItem(list: array of string; itype: string);
var
c: integer;
LItem: TListBoxItem;
BitmapFolder, BitmapFile: TBitmap;
begin
BitmapFolder := GetImage('folder');
BitmapFile := GetImage('file');
ListBox1.BeginUpdate;
for c := 0 to Length(list) - 1 do
begin
LItem := TListBoxItem.Create(ListBox1);
if itype = 'folder' then
begin
if BitmapFolder <> nil then
begin
LItem.ItemData.Bitmap.Assign(BitmapFolder);
end;
end
else
begin
if BitmapFile <> nil then begin
LItem.ItemData.Bitmap.Assign(BitmapFile);
end;
end;
LItem.ItemData.Text := ExtractFileName(list[c]);
LItem.ItemData.Detail := list[c]; // Помещаем полный путь в Detail
LItem.TagString := itype;
ListBox1.AddObject(LItem);
end;
ListBox1.EndUpdate;
end;
{Функция для проверки введённого имени}
function CheckName(NewName: string): Boolean;
const
InvalidChars: Array[0..9] of Char = ('\', '/', ':', '*', '?', '"', '<', '>', '|', '~');
var
LengthName, i, j: integer;
begin
Result := False;
LengthName := Length(NewName);
if LengthName <> 0 then
begin
for i := 0 to LengthName - 1 do
begin
for j := 0 to 9 do
begin
if NewName[i] = InvalidChars[j] then
begin
Result := True;
end;
end;
end;
end;
end;
{Отслеживаем ввод символов и проверяем их}
procedure TForm1.DialogEditChangeTracking(Sender: TObject);
begin
if CheckName(DialogEdit.Text) then
DialogError.Text := 'Недопускаются \ / : * ? " < > | ~'
else
DialogError.Text := '';
end;
{Загружаем список файлов в корне устройства}
procedure TForm1.FormCreate(Sender: TObject);
begin
// Корень устройства
path := '/';
TotalWork(path, False);
end;
{Обрабатываем кнопки Hardware Back, Enter}
procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
Shift: TShiftState);
begin
if Dialog.Visible = False then
begin
if (Key = vkHardwareBack) AND (path <> '/') then
begin
SpeedButton1Click(Self); // Вызываем кнопку Назад
Key := 0;
end;
end
else
begin
if Key = vkHardwareBack then
begin
SpeedButton5Click(Self); // Вызываем кнопку Отмена
Key := 0;
end;
if Key = 13 then
begin
SpeedButton6Click(Self); // Вызываем кнопку Создать
end;
end;
end;
{Пункт пометили галкой}
procedure TForm1.ListBox1ChangeCheck(Sender: TObject);
var
i: integer;
begin
SetLength(ItemsCheck, 0, 0);
for i := 0 to ListBox1.Count-1 do
begin
if ListBox1.ListItems[i].IsChecked then
begin
SetLength(ItemsCheck, i + 1, 3);
ItemsCheck[i][0] := ListBox1.ListItems[i].Text; // Имя
ItemsCheck[i][1] := ListBox1.ListItems[i].TagString; // Тип
ItemsCheck[i][2] := ListBox1.ListItems[i].ItemData.Detail; // Путь
end;
end;
if Length(ItemsCheck) <> 0 then begin
OnOffButton(True, False, True, True);
end
else begin
OnOffButton(False, False, False, False);
end;
end;
{Клик по Item'у, вперёд}
procedure TForm1.ListBox1ItemClick(const Sender: TCustomListBox;
const Item: TListBoxItem);
var
FileName, ExtFile: string;
mime: JMimeTypeMap;
ExtToMime: JString;
Intent: JIntent;
begin
if Length(ItemsCheck) <> 0 then begin
OnOffButton(False, True, False, False);
end
else begin
OnOffButton(False, False, False, False);
end;
if Item.TagString = 'folder' then
begin
// Сохраняем выбранный путь
path := Item.ItemData.Detail;
if TDirectory.Exists(path) then
begin
TotalWork(path, True);
end
else
begin
ListBox1.Items.Delete(Item.Index);
ShowMessage('Папка не найдена');
end;
end
else if Item.TagString = 'file' then
begin
// Получаем путь до файла
FileName := Item.ItemData.Detail;
try
//Определяем расширение файла и его mime тип
ExtFile := AnsiLowerCase(StringReplace(TPath.GetExtension(FileName), '.', '',[]));
mime := TJMimeTypeMap.JavaClass.getSingleton();
ExtToMime := mime.getMimeTypeFromExtension(StringToJString(ExtFile));
//Запрашиваем открытие файла
Intent := TJIntent.Create;
Intent.setAction(TJIntent.JavaClass.ACTION_VIEW);
Intent.setDataAndType(StrToJURI('file:' + FileName), ExtToMime);
SharedActivity.startActivity(Intent);
except
ShowMessage('Невозможно открыть файл!');
end;
end;
end;
{Управляем кнопками}
procedure TForm1.OnOffButton(del, add, copy, cut: boolean);
begin
SpeedButton2.Enabled := del; // кнопка Удаления
SpeedButton7.Enabled := add; // кнопка Вставить
SpeedButton8.Enabled := copy; // кнопка Копировать
SpeedButton9.Enabled := cut; // кнопка Вырезать
end;
{Кнопка назад}
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
// Определяем предыдущую папку
path := ExtractFileDir(path);
if path = '/' then path := '/'
else path := path;
TotalWork(path, True);
if Length(ItemsCheck) <> 0 then begin
OnOffButton(False, True, False, False);
end
else begin
OnOffButton(False, False, False, False);
end;
end;
{Кнопка удалить}
procedure TForm1.SpeedButton2Click(Sender: TObject);
var
i: integer; // Устанавливаем счётчик
LItemPath: string;
msg: integer;
begin
msg := MessageDlg('Удалить выбранные файлы?', System.UITypes.TMsgDlgType.mtConfirmation,
[
System.UITypes.TMsgDlgBtn.mbYes,
System.UITypes.TMsgDlgBtn.mbNo
], 0);
if msg = mrYes then
begin
for i := 0 to Length(ItemsCheck) - 1 do
begin
if ItemsCheck[i][0] <> '' then
begin
// Получаем путь из массива
LItemPath := ItemsCheck[i][2];
if ItemsCheck[i][1] = 'folder' then begin
if TDirectory.Exists(LItemPath) then
begin
TDirectory.Delete(LItemPath, True); // Удаляем папку и подпапки
end;
end
else if ItemsCheck[i][1] = 'file' then begin
if TFile.Exists(LItemPath) then
begin
TFile.Delete(LItemPath); // Удаляем файл
end;
end;
end;
end;
TotalWork(path, True); // Обновляем список
ListBox1ChangeCheck(Self); // Чистим массив и т.д.
end;
end;
{Кнопка для создания файла - выводим окно для ввода имени файла}
procedure TForm1.SpeedButton3Click(Sender: TObject);
begin
DialogTitle.Text := 'Введите имя файла';
DialogEdit.TagString := 'CreateFile';
DialogFon.Visible := True;
Dialog.Visible := True;
end;
{Кнопка для создания папки - выводим окно для ввода имени папки}
procedure TForm1.SpeedButton4Click(Sender: TObject);
begin
DialogTitle.Text := 'Введите имя папки';
DialogEdit.TagString := 'CreateFolder';
DialogFon.Visible := True;
Dialog.Visible := True;
end;
{Кастомное окно - кнопка Отмена - "закрываем" окно}
procedure TForm1.SpeedButton5Click(Sender: TObject);
begin
DialogEdit.Text := '';
DialogError.Text := '';
Dialog.Visible := False;
DialogFon.Visible := False;
end;
{Кастомное окно - кнопка Создать - создаём папку или файл}
procedure TForm1.SpeedButton6Click(Sender: TObject);
var
newpath: string;
newfile: TFileStream;
begin
if (Length(DialogEdit.Text) = 0) OR (DialogEdit.Text = ' ') then begin
DialogError.Text := 'Введите имя!';
Exit;
end;
newpath := path + PathDelim + DialogEdit.Text;
if DialogEdit.TagString = 'CreateFile' then
begin
if TFile.Exists(newpath) then begin
ShowMessage('Файл существует!');
end
else begin
newfile := TFile.Create(newpath); // Создаём файл(поток)
newfile.Free; // Очищаем поток
SpeedButton5Click(Self); // Закрываем окно
TotalWork(path, True); // Обновляем список
end;
end
else if DialogEdit.TagString = 'CreateFolder' then
begin
if TDirectory.Exists(newpath) then begin
ShowMessage('Папка существует!');
end
else begin
TDirectory.CreateDirectory(newpath); // Создаём папку
SpeedButton5Click(Self); // Закрываем окно
TotalWork(path, True); // Обновляем список
end;
end;
end;
{кнопка Вставить}
procedure TForm1.SpeedButton7Click(Sender: TObject);
var
i: integer; // Устанавливаем счётчик
LItemPath: string; // Старый путь до файла или папки
begin
for i := 0 to Length(ItemsCheck) - 1 do
begin
if ItemsCheck[i][0]<>'' then
begin
// Получаем путь из массива
LItemPath := ItemsCheck[i][2];
if ItemsCheck[i][1] = 'folder' then begin
try
if SpeedButton9.Tag <> 1 then begin
TDirectory.Copy(LItemPath, path + PathDelim + ItemsCheck[i][0]); // Копируем папку
end
else begin
TDirectory.Move(LItemPath, path + PathDelim + ItemsCheck[i][0]); // Перемещаем папку
end;
except
ShowMessage('Копирование папки ' + ItemsCheck[i][0] + ' не возможно!');
end;
end
else if ItemsCheck[i][1] = 'file' then begin
try
if SpeedButton9.Tag <> 1 then begin
TFile.Copy(LItemPath, path + PathDelim + ItemsCheck[i][0], True); // Копируем файл
end
else begin
TFile.Move(LItemPath, path + PathDelim + ItemsCheck[i][0]); // Перемещаем файл
end;
except
ShowMessage('Копирование файла ' + ItemsCheck[i][0] + ' не возможно!');
end;
end;
end;
end;
SpeedButton9.Tag := 0;
TotalWork(path, True); // Обновляем список
ListBox1ChangeCheck(Self); // Чистим массив и т.д.
end;
{кнопка Копировать}
procedure TForm1.SpeedButton8Click(Sender: TObject);
begin
if Length(ItemsCheck) <> 0 then
begin
ShowMessage('Скопировали в буфер, перейдите в другую папку для вставки объектов');
end;
end;
{кнопка Вырезать/перместить}
procedure TForm1.SpeedButton9Click(Sender: TObject);
begin
if Length(ItemsCheck) <> 0 then
begin
SpeedButton9.Tag := 1;
ShowMessage('Вырезали в буфер, перейдите в другую папку для вставки объектов');
end;
end;
{Основа для обработки перемещений по папкам}
procedure TForm1.TotalWork(path_tr: string; clear: boolean);
var
folders, files: TStringDynArray;
begin
// Выводим текущий путь
Label1.Text := path_tr;
//*****Папки*****
// Ищем папки
folders := TDirectory.GetDirectories(path_tr);
// Сортируем папки
TArray.Sort(folders, TComparer.Construct(CompareLowerStr));
if clear then begin
// Очищаем Листбокс
ListBox1.Clear;
end;
// Заполняем Листбокс списком отсортированных папок
AddListItem(folders, 'folder');
//***************
//*****Файлы*****
// Ищем файлы
files := TDirectory.GetFiles(path_tr);
// Сортируем файлы
TArray.Sort(files, TComparer.Construct(CompareLowerStr));
// Дополняем Листбокс списком отсортированных файлов
AddListItem(files, 'file');
//***************
end;
end.
Скриншоты:
Когда посвящаешь себя полностью делу, которое нравится, начинаешь параллельно думать, как и что можно улучшить/сделать лучше.
Вот у меня так и произошло, пока дописывал основной функционал (Копирование/перемещение) приложения, в голове параллельно появлялись новые мысли по улучшению приложения, поэтому пришлось создать текстовый документ, в который записывалась каждая пришедшая мысль. В итоге, когда дописал основной функционал, в текстовом документе было уже 12 строчек, в этой версии я реализовал 4 мысли, некоторые идеи пока даже не знаю как реализовать, но с ними приложение стало бы ещё немного дружелюбнее/удобнее. Возможно, будет ещё одна часть, зависит от вашего мнения/лайков.
Вот у меня так и произошло, пока дописывал основной функционал (Копирование/перемещение) приложения, в голове параллельно появлялись новые мысли по улучшению приложения, поэтому пришлось создать текстовый документ, в который записывалась каждая пришедшая мысль. В итоге, когда дописал основной функционал, в текстовом документе было уже 12 строчек, в этой версии я реализовал 4 мысли, некоторые идеи пока даже не знаю как реализовать, но с ними приложение стало бы ещё немного дружелюбнее/удобнее. Возможно, будет ещё одна часть, зависит от вашего мнения/лайков.
Исходный код (Delphi XE5 Update 2): Скачать с Google Drive
Исходный код (Delphi XE6): Скачать с Google Drive
APK файл для установки на ваш девайс: Скачать с Google Drive
Иконки: Скачать с Google Drive
На этом всё. Спасибо за внимание.
Надеюсь, вам понравилась/пригодится данная статья, приложение, иконки и т.д.
p.s.1 Пока добавлял пост заметил, что количество строк выросло с 258 до 551, можно уменьшить и сохранить при этом читабельность кода, но в моих целях этого пока не было.


