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