Pull to refresh

Разработка собственной Файловой модели (вместо QDirModel и QFileSystemModel)

Reading time 8 min
Views 8.5K
В этой статье я расскажу о создании собственной файл модели в Qt. Сразу скажу что модель создавалась под конкретную задачу и не планировалось для широкого использования, так что в ней может и не быть того что вам хочется. Еще хочется добавить что опыт программирования на С++/Qt у меня не столь велик, поэтому вполне готов к комментариям типа: «Ваш код г%вно».

Теперь о том, а зачем собственно...


Известно что в Qt 4 есть две встроенных модели работы с файлами:
QDirModel и
QFileSystemModel.

Однако, эти модели очень медленно работают с большим количеством файлов.
QDirModel так вообще безобразно, у второй дела получше, но даже если сравнивать с виндовым проводником, то все равно отвратительно. Как показало исследование профайлером, при каждом запросе к содержимому папки идет очень долгое ожидание мутекса.

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

Работа с файловой системой


Собственно для работы с файловой системой был принято решение использовать boost, а именно библиотеку boost filesystem.

Функция получения содержимого папки выглядит так:
  1. QList<FileItem *> *getFileList(FileItem *parent)
  2. {
  3.   QList<FileItem *> *result = new QList<FileItem *>();
  4.   //FileItem — класс файлового элемента, опишу его пожже.
  5.   QString path = parent->getFilePath();
  6.   QTextCodec *localeCodec = QTextCodec::codecForLocale();
  7.  
  8.   fs::path full_path(fs::initial_path<fs::path>());
  9.   full_path = fs::system_complete(fs::path(localeCodec->fromUnicode(path).append('\0').data())); // Получаем полный путь к директории в представлении boost'a
  10.   try // На случай запрета на чтение
  11.   {
  12.     if (fs::exists(full_path) && fs::is_directory(full_path)) // Проверяем на существование и действительно ли это директория, а то мало ли что
  13.     {
  14.       fs::directory_iterator end_iter;
  15.  
  16.       for (fs::directory_iterator dir_itr(full_path); dir_itr != end_iter; ++dir_itr)
  17.       {
  18.         FileItem *fileInfo = 0;
  19.         try // На случай запрета на чтение
  20.         {
  21.           fileInfo = new FileItem(localeCodec->toUnicode(dir_itr->path().string().c_str()),
  22.                       fs::is_directory(dir_itr->status()),
  23.                       parent,
  24.                       (!fs::is_directory(dir_itr->status())? fs::file_size(dir_itr->path()): 0),
  25.                       QDateTime::fromTime_t(last_write_time(dir_itr->path()))); // Создаем новый элемент дерева
  26.  
  27.  
  28.           result->append(fileInfo); // и добавляем его в результат
  29.         }
  30.         catch(const std::exception &ex)
  31.         {
  32.           delete fileInfo; // Если что-то пошло не так надоб удалить
  33.         }
  34.       }
  35.     }
  36.   }
  37.   catch (const std::exception &ex)
  38.   {
  39.   }
  40.   return result;
  41. }
* This source code was highlighted with Source Code Highlighter.

Функция достаточно простая, можно вполне обойтись и без boost'a, но тогда вам придется заморачиваться с раздельным кодом для *nix и Windows.

Класс FileItem


Теперь поговорим об элементах дерева…

Это его объявление (вырезаны маловажные куски):
  1. class FileItem
  2. {
  3. public:
  4.   FileItem(QString filePath, bool fileIsDir, FileItem *parent = 0, int size = 0, QDateTime date = QDateTime::currentDateTime());
  5.  
  6.   void setFilePath(QString what);
  7.   void setFileSize(int what) { size = what; }
  8.   void setFileDateTime(QDateTime what) { date = what; }
  9.   void setIsDir(bool what) { fileIsDir = what; }
  10.   void setChildren(QList<FileItem *>* what);
  11.  
  12.   QString getFilePath() const { return filePath; }
  13.   QString getFileName() const;
  14.   QString getFileSize() const; // В «человекопонятном» представлении
  15.   int getFileSizeInBytes() const { return size; } // В байтах
  16.   QDateTime getFileDateTime() const { return date; }
  17.   bool isDir() const { return fileIsDir; }
  18.   QList<FileItem *> *getChildren() { return children; }
  19.   void addChild(FileItem *item);
  20.   int childCount() const { return children->count(); }
  21.   int row() const; // Возвращает свой номер в списке родителя
  22.   FileItem *parent() { return itemParent; }
  23.   void setFetched(bool what) { fetched = what; }
  24.   bool getFetched() const { return fetched; }
  25.  
  26. private:
  27.   void setParent(FileItem *parent) { itemParent = parent; } // На случай, если при создании родитель не известен
  28.  
  29.   QString filePath;
  30.   QString fileName;
  31.   int size;
  32.   QDateTime date;
  33.   bool fileIsDir;
  34.   FileItem *itemParent;
  35.   QList<FileItem *> *children;
  36.   bool fetched; // А мы уже загружали детей?
  37.  
  38. };
* This source code was highlighted with Source Code Highlighter.

Из реализации важен только код контруктора:
  1. FileItem::FileItem(QString filePath, bool fileIsDir, FileItem *parent, int size, QDateTime date)
  2. {
  3.   this->filePath = filePath;
  4.   if (filePath.isEmpty())
  5.     fileName = "";
  6.   else
  7.   {
  8. #ifdef Q_OS_WIN // Не забываем что в Windows есть диски
  9.     if (filePath.size() == 3 && filePath.at(1) == QLatin1Char(':'))
  10.     {
  11.       fileName = filePath;
  12.       fileName.chop(1);
  13.     }
  14.     else
  15.     {
  16.       QStringList fileParts = filePath.split("/");
  17.       fileName = fileParts.last();
  18.     }
  19. #else // а в *nix их нету
  20.     QStringList fileParts = filePath.split("/");
  21.     fileName = fileParts.last();
  22. #endif
  23.   }
  24.   this->fileIsDir = fileIsDir;
  25.   this->size = size;
  26.   this->date = date;
  27.   this->itemParent = parent;
  28.   this->children = new QList<FileItem *>();
  29.   if (fileIsDir && !filePath.isEmpty())
  30.   {
  31.     fetched = false; // Детей мы еще пока не загружали.
  32.     addChild(new FileItem(«dummy», false, this)); // Жуткая гадость, но без этого виджет не будет лепить вам плюсики к папочкам,
  33.     // потому что будет считать, что они пусты.
  34.     // Кстати, минус этого подхода в том что мы не знаем, непуста ли папка на самом деле
  35.     // и лепим плюсики ко всему подряд,
  36.     // к сожалению пинать каждую директорию-ребенка на предмет «не пустоты» слишком затратная затея.
  37.   }
  38. }
* This source code was highlighted with Source Code Highlighter.

FileModel


А теперь, собственно, сама модель. Как и любая другая «хитрая» пользовательская модель она наследуется от QAbsractItemModel:
  1. class FileItemModel: public QAbstractItemModel
  2. {
  3. public:
  4.   enum Columns // Четыре очевидных колонки требуются нам
  5.   {
  6.     NameColumn = 0,
  7.     SizeColumn,
  8.     TypeColumn,
  9.     DateColumn
  10.   };
  11.  
  12.   FileItemModel();
  13.   ~FileItemModel();
  14.  
  15.   QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const// Важная виртуальная функция, ее определение обязательно
  16.                                         // отвечает за вывод всех информации в дереве
  17.   Qt::ItemFlags flags(const QModelIndex &index) const; // (виртуальная, обязательная)
  18.   QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; // Отвечает за заголовки столбцов (виртуальная, обязательная)
  19.   QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; // Возвращает индекс элемента (виртуальная, обязательная)
  20.   QModelIndex parent(const QModelIndex &index) const; // Возвращает индекс родителя (виртуальная, обязательная)
  21.   int rowCount(const QModelIndex &parent = QModelIndex()) const; // (виртуальная, обязательная)
  22.   int columnCount(const QModelIndex &parent = QModelIndex()) const; // (виртуальная, обязательная)
  23.   FileItem *item(QModelIndex index) const { return static_cast<FileItem *>(index.internalPointer()); } // Возвращает элемент по индексу
  24.   FileItem *getRootItem() { return rootItem; }
  25.   bool hasChildren(const QModelIndex &index) const { return index.isValid()? item(index)->childCount(): true; }
  26.   bool canFetchMore(const QModelIndex &index) const { return index.isValid()? !item(index)->getFetched(): true; }
  27.   void fetchMore(const QModelIndex &index); // Выполняется когда нажали плюсик(или минусик) (виртуальная, обязательная)
  28.   void refresh() { emit layoutChanged(); }
  29.  
  30. private:
  31.   FileItem *rootItem;
  32.   static const int ColumnCount = 4; // Колонки всегда 4
  33.   QFileIconProvider iconProvider; // Стандартные иконки дисков, файлов и папок
  34.  
  35. protected:
  36.   void readDir(FileItem *item); // Загружает детей
  37.  
  38. };
* This source code was highlighted with Source Code Highlighter.

Из реализации важно отметить следующие методы:
  1. FileItemModel::FileItemModel()
  2. {
  3.   lastSortColumn = 0;
  4.   lastSortOrder = Qt::AscendingOrder;
  5. #ifdef Q_OS_WIN // Как много нам открытий чудных...
  6.   rootItem = new FileItem("", true); // А все потому, что в windows нет корня как такового
  7.   QFileInfoList drives = QDir::drives(); // Зато есть диски
  8.   for (QFileInfoList::iterator driveIt = drives.begin(); driveIt != drives.end(); ++driveIt) // Здесь то мы их и создаем
  9.   {
  10.     FileItem *drive = new FileItem((*driveIt).absolutePath(), true);
  11.     rootItem->addChild(drive);
  12.   }
  13. #else
  14.   QString path = QDir::rootPath();
  15.   rootItem = new FileItem(path, true);
  16.   readDir(rootItem); //читаем содержимое корня сразу же
  17. #endif
  18. }
  19.  
  20. void FileItemModel::readDir(FileItem *item)
  21. {
  22.   item->setChildren(getFileList(item)); // Получаем детей и сразу связывем их с предком
  23.   // Здесь опущен вызов сортировки
  24. }
  25.  
  26. void FileItemModel::fetchMore(const QModelIndex &index)
  27. {
  28.   if (index.isValid() && !item(index)->getFetched()) // Если еще ни разу детей не загружали то загружаем.
  29.   {
  30.     readDir(item(index));
  31.     item(index)->setFetched(true);
  32.     refresh();
  33.   }
  34. }
* This source code was highlighted with Source Code Highlighter.


Заключение


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

P. S.


Повторяюсь, эта статья не содержит в себе готового к использованию решения, она лишь описывает трудности которые возникнут при разработке собственной файловой модели и способы их решения, также я не могу выложить полный код класса, в связи с лицензией.
Tags:
Hubs:
+13
Comments 8
Comments Comments 8

Articles