В этой статье я расскажу о создании собственной файл модели в Qt. Сразу скажу что модель создавалась под конкретную задачу и не планировалось для широкого использования, так что в ней может и не быть того что вам хочется. Еще хочется добавить что опыт программирования на С++/Qt у меня не столь велик, поэтому вполне готов к комментариям типа: «Ваш код г%вно».
Известно что в Qt 4 есть две встроенных модели работы с файлами:
QDirModel и
QFileSystemModel.
Однако, эти модели очень медленно работают с большим количеством файлов.
QDirModel так вообще безобразно, у второй дела получше, но даже если сравнивать с виндовым проводником, то все равно отвратительно. Как показало исследование профайлером, при каждом запросе к содержимому папки идет очень долгое ожидание мутекса.
Поэтому было принято решение написать собственную файл модельс блекджеком и шлюхами избавленную от всего лишнего и максимально быстро работающую.
Собственно для работы с файловой системой был принято решение использовать boost, а именно библиотеку boost filesystem.
Функция получения содержимого папки выглядит так:
Функция достаточно простая, можно вполне обойтись и без boost'a, но тогда вам придется заморачиваться с раздельным кодом для *nix и Windows.
Теперь поговорим об элементах дерева…
Это его объявление (вырезаны маловажные куски):
Из реализации важен только код контруктора:
А теперь, собственно, сама модель. Как и любая другая «хитрая» пользовательская модель она наследуется от QAbsractItemModel:
Из реализации важно отметить следующие методы:
Из минусов класса, к сожалению в нем нет особой магии чтобы модель автоматически обновлялась при изменениях в файловой системе. Но точно такая же проблема есть и у QDirModel.
На самом деле мне это просто не потребовалось, кому требуется такой функционал, могут посмотреть в сторону QFileSystemWatcher
Повторяюсь, эта статья не содержит в себе готового к использованию решения, она лишь описывает трудности которые возникнут при разработке собственной файловой модели и способы их решения, также я не могу выложить полный код класса, в связи с лицензией.
Теперь о том, а зачем собственно...
Известно что в Qt 4 есть две встроенных модели работы с файлами:
QDirModel и
QFileSystemModel.
Однако, эти модели очень медленно работают с большим количеством файлов.
QDirModel так вообще безобразно, у второй дела получше, но даже если сравнивать с виндовым проводником, то все равно отвратительно. Как показало исследование профайлером, при каждом запросе к содержимому папки идет очень долгое ожидание мутекса.
Поэтому было принято решение написать собственную файл модель
Работа с файловой системой
Собственно для работы с файловой системой был принято решение использовать boost, а именно библиотеку boost filesystem.
Функция получения содержимого папки выглядит так:
* This source code was highlighted with Source Code Highlighter.
- QList<FileItem *> *getFileList(FileItem *parent)
- {
- QList<FileItem *> *result = new QList<FileItem *>();
- //FileItem — класс файлового элемента, опишу его пожже.
- QString path = parent->getFilePath();
- QTextCodec *localeCodec = QTextCodec::codecForLocale();
- fs::path full_path(fs::initial_path<fs::path>());
- full_path = fs::system_complete(fs::path(localeCodec->fromUnicode(path).append('\0').data())); // Получаем полный путь к директории в представлении boost'a
- try // На случай запрета на чтение
- {
- if (fs::exists(full_path) && fs::is_directory(full_path)) // Проверяем на существование и действительно ли это директория, а то мало ли что
- {
- fs::directory_iterator end_iter;
- for (fs::directory_iterator dir_itr(full_path); dir_itr != end_iter; ++dir_itr)
- {
- FileItem *fileInfo = 0;
- try // На случай запрета на чтение
- {
- fileInfo = new FileItem(localeCodec->toUnicode(dir_itr->path().string().c_str()),
- fs::is_directory(dir_itr->status()),
- parent,
- (!fs::is_directory(dir_itr->status())? fs::file_size(dir_itr->path()): 0),
- QDateTime::fromTime_t(last_write_time(dir_itr->path()))); // Создаем новый элемент дерева
- result->append(fileInfo); // и добавляем его в результат
- }
- catch(const std::exception &ex)
- {
- delete fileInfo; // Если что-то пошло не так надоб удалить
- }
- }
- }
- }
- catch (const std::exception &ex)
- {
- }
- return result;
- }
Функция достаточно простая, можно вполне обойтись и без boost'a, но тогда вам придется заморачиваться с раздельным кодом для *nix и Windows.
Класс FileItem
Теперь поговорим об элементах дерева…
Это его объявление (вырезаны маловажные куски):
* This source code was highlighted with Source Code Highlighter.
- class FileItem
- {
- public:
- FileItem(QString filePath, bool fileIsDir, FileItem *parent = 0, int size = 0, QDateTime date = QDateTime::currentDateTime());
- void setFilePath(QString what);
- void setFileSize(int what) { size = what; }
- void setFileDateTime(QDateTime what) { date = what; }
- void setIsDir(bool what) { fileIsDir = what; }
- void setChildren(QList<FileItem *>* what);
- QString getFilePath() const { return filePath; }
- QString getFileName() const;
- QString getFileSize() const; // В «человекопонятном» представлении
- int getFileSizeInBytes() const { return size; } // В байтах
- QDateTime getFileDateTime() const { return date; }
- bool isDir() const { return fileIsDir; }
- QList<FileItem *> *getChildren() { return children; }
- void addChild(FileItem *item);
- int childCount() const { return children->count(); }
- int row() const; // Возвращает свой номер в списке родителя
- FileItem *parent() { return itemParent; }
- void setFetched(bool what) { fetched = what; }
- bool getFetched() const { return fetched; }
- private:
- void setParent(FileItem *parent) { itemParent = parent; } // На случай, если при создании родитель не известен
- QString filePath;
- QString fileName;
- int size;
- QDateTime date;
- bool fileIsDir;
- FileItem *itemParent;
- QList<FileItem *> *children;
- bool fetched; // А мы уже загружали детей?
- };
Из реализации важен только код контруктора:
* This source code was highlighted with Source Code Highlighter.
- FileItem::FileItem(QString filePath, bool fileIsDir, FileItem *parent, int size, QDateTime date)
- {
- this->filePath = filePath;
- if (filePath.isEmpty())
- fileName = "";
- else
- {
- #ifdef Q_OS_WIN // Не забываем что в Windows есть диски
- if (filePath.size() == 3 && filePath.at(1) == QLatin1Char(':'))
- {
- fileName = filePath;
- fileName.chop(1);
- }
- else
- {
- QStringList fileParts = filePath.split("/");
- fileName = fileParts.last();
- }
- #else // а в *nix их нету
- QStringList fileParts = filePath.split("/");
- fileName = fileParts.last();
- #endif
- }
- this->fileIsDir = fileIsDir;
- this->size = size;
- this->date = date;
- this->itemParent = parent;
- this->children = new QList<FileItem *>();
- if (fileIsDir && !filePath.isEmpty())
- {
- fetched = false; // Детей мы еще пока не загружали.
- addChild(new FileItem(«dummy», false, this)); // Жуткая гадость, но без этого виджет не будет лепить вам плюсики к папочкам,
- // потому что будет считать, что они пусты.
- // Кстати, минус этого подхода в том что мы не знаем, непуста ли папка на самом деле
- // и лепим плюсики ко всему подряд,
- // к сожалению пинать каждую директорию-ребенка на предмет «не пустоты» слишком затратная затея.
- }
- }
FileModel
А теперь, собственно, сама модель. Как и любая другая «хитрая» пользовательская модель она наследуется от QAbsractItemModel:
* This source code was highlighted with Source Code Highlighter.
- class FileItemModel: public QAbstractItemModel
- {
- public:
- enum Columns // Четыре очевидных колонки требуются нам
- {
- NameColumn = 0,
- SizeColumn,
- TypeColumn,
- DateColumn
- };
- FileItemModel();
- ~FileItemModel();
- QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; // Важная виртуальная функция, ее определение обязательно
- // отвечает за вывод всех информации в дереве
- Qt::ItemFlags flags(const QModelIndex &index) const; // (виртуальная, обязательная)
- QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; // Отвечает за заголовки столбцов (виртуальная, обязательная)
- QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; // Возвращает индекс элемента (виртуальная, обязательная)
- QModelIndex parent(const QModelIndex &index) const; // Возвращает индекс родителя (виртуальная, обязательная)
- int rowCount(const QModelIndex &parent = QModelIndex()) const; // (виртуальная, обязательная)
- int columnCount(const QModelIndex &parent = QModelIndex()) const; // (виртуальная, обязательная)
- FileItem *item(QModelIndex index) const { return static_cast<FileItem *>(index.internalPointer()); } // Возвращает элемент по индексу
- FileItem *getRootItem() { return rootItem; }
- bool hasChildren(const QModelIndex &index) const { return index.isValid()? item(index)->childCount(): true; }
- bool canFetchMore(const QModelIndex &index) const { return index.isValid()? !item(index)->getFetched(): true; }
- void fetchMore(const QModelIndex &index); // Выполняется когда нажали плюсик(или минусик) (виртуальная, обязательная)
- void refresh() { emit layoutChanged(); }
- private:
- FileItem *rootItem;
- static const int ColumnCount = 4; // Колонки всегда 4
- QFileIconProvider iconProvider; // Стандартные иконки дисков, файлов и папок
- protected:
- void readDir(FileItem *item); // Загружает детей
- };
Из реализации важно отметить следующие методы:
* This source code was highlighted with Source Code Highlighter.
- FileItemModel::FileItemModel()
- {
- lastSortColumn = 0;
- lastSortOrder = Qt::AscendingOrder;
- #ifdef Q_OS_WIN // Как много нам открытий чудных...
- rootItem = new FileItem("", true); // А все потому, что в windows нет корня как такового
- QFileInfoList drives = QDir::drives(); // Зато есть диски
- for (QFileInfoList::iterator driveIt = drives.begin(); driveIt != drives.end(); ++driveIt) // Здесь то мы их и создаем
- {
- FileItem *drive = new FileItem((*driveIt).absolutePath(), true);
- rootItem->addChild(drive);
- }
- #else
- QString path = QDir::rootPath();
- rootItem = new FileItem(path, true);
- readDir(rootItem); //читаем содержимое корня сразу же
- #endif
- }
- void FileItemModel::readDir(FileItem *item)
- {
- item->setChildren(getFileList(item)); // Получаем детей и сразу связывем их с предком
- // Здесь опущен вызов сортировки
- }
- void FileItemModel::fetchMore(const QModelIndex &index)
- {
- if (index.isValid() && !item(index)->getFetched()) // Если еще ни разу детей не загружали то загружаем.
- {
- readDir(item(index));
- item(index)->setFetched(true);
- refresh();
- }
- }
Заключение
Из минусов класса, к сожалению в нем нет особой магии чтобы модель автоматически обновлялась при изменениях в файловой системе. Но точно такая же проблема есть и у QDirModel.
На самом деле мне это просто не потребовалось, кому требуется такой функционал, могут посмотреть в сторону QFileSystemWatcher
P. S.
Повторяюсь, эта статья не содержит в себе готового к использованию решения, она лишь описывает трудности которые возникнут при разработке собственной файловой модели и способы их решения, также я не могу выложить полный код класса, в связи с лицензией.