Суббота, 19 декабря 2020 11:19

Пишем свой Проводник на основе QListView. - Работа с моделями в Qt5 для отображения данных в виджетах.

Россия
Оцените материал
(0 голосов)

Сегодня мы расширим использование модели в QListView.  Мы напишем свою версию Проводника Windows. Конечно, она не будет обладать всем функционалом, но, для демонстрации возможностей виджета, этого будет достаточно.

Источником данных для модели может быть любой источник данных, на этот раз этим источником будет файловая система ПК.

Создание модели

Создадим новый проект Qt Widgets и назовем его QListViewExplorer.

Добавим на форму QListView

Добавим класс для нашей модели – QListViewExplorerModel

Заголовок:

#ifndef QLISTVIEWEXPLORERMODEL_H
#define QLISTVIEWEXPLORERMODEL_H

#include <QDir>
#include <QModelIndex>

class QListViewExplorerModel: public QAbstractListModel
{
public:
    QListViewExplorerModel(QObject *parent=nullptr);
    int rowCount(const QModelIndex &) const;
    QVariant data(const QModelIndex &index, int role) const;

    void getFolderList(QString folderPath, QFileInfoList *dirList);

private:
    QFileInfoList *aDirList;

};

#endif // QLISTVIEWEXPLORERMODEL_H

Реализация:

 

#include "qlistviewexplorermodel.h"

QListViewExplorerModel::QListViewExplorerModel(QObject *parent)
    :QAbstractListModel(parent)
{

}

void QListViewExplorerModel::getFolderList(QString folderPath, QFileInfoList *dirList)
{
    QDir dir = QDir(folderPath);

    *dirList = dir.entryInfoList();

    this->aDirList = dirList;
}

int QListViewExplorerModel::rowCount(const QModelIndex &) const
{
    return this->aDirList->count();
}

QVariant QListViewExplorerModel::data( const QModelIndex &index, int role ) const
{

    QVariant value;

        switch ( role )
        {
            case Qt::DisplayRole: //string
            {
                value = this->aDirList->at(index.row()).fileName();
            }
            break;

            case Qt::UserRole: //data
            {
                value = this->aDirList->at(index.row()).fileName();
            }
            break;

            default:
                break;
        }

    return value;
}

Добавим код в конструктор формы MainWindow::MainWindow(QWidget *parent)

    this->aDirList = new QFileInfoList(); 

    this->model = new QListViewExplorerModel(); 

    this->model->getFolderList(".",this->aDirList); 

    this->ui->listView->setModel(model);

Добавим поля в заголовок:

private:
    Ui::MainWindow *ui;

    QFileInfoList *aDirList;

    QListViewExplorerModel *model;

Запустим

2020-12-17_16-26-45.png

Мы и получили список файлов папки, из которой производится запуск нашей программы.

Рассмотрим код модели:

В методе getFolderList() мы создаем экземпляр класса QDir. Данный класс позволяет получать доступ к файлам и папкам файловой системы, а также виртуально перемещаться по ним.

После этого мы получаем список файлов и папок, указанной папки и через указатель присваиваем значение этого списка полю класса, объявленному в нашей главной форме.

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

Метод data() в нашем случае для всех ролей возвращает имя файла или папки, которые мы выбрали. В данный момент это несущественно.

Сортируем список папок и файлов

У нас есть список файлов и папок, но проблема в том, что содержимое списка отсортировано непонятно как - неясно где папка, а где файл.

Применим сортировку к списку. В методе модели getFolderList() изменим строку

    *dirList = dir.entryInfoList( QDir::NoFilter, QDir::DirsFirst);

QDir::NoFilter – указывает, что нам не нужна фильтрация (подробнее в документации Qt5 - https://doc.qt.io/qt-5/qdir.html#Filter-enum)

QDir::DirsFirst – задаёт метод сортировки – папки первыми, потом файлы (подробнее в документации Qt5 - https://doc.qt.io/qt-5/qdir.html#SortFlag-enum).

Запустим

2020-12-17_17-03-15.png

Уже лучше, но всё равно непонятно, где папка, а где файл. Давайте добавим иконки для каждой строки.

Для начала нам понадобятся сами иконки, скачать вы их можете с Яндекс.Диска - https://yadi.sk/d/_N4v7pySleb07A или с Github.

Создадим в корне проекта папку img и скопируем в нее файлы с иконками.

Создадим файл ресурса resource.qrc со следующим содержимым:

<RCC>
    <qresource prefix="/">
        <file>img/file.png</file>
        <file>img/folder.png</file>
        <file>img/zip.png</file>
        <file>img/folderup.png</file>
        <file>img/iconlist.png</file>
        <file>img/list.png</file>
    </qresource>
</RCC>

Добавим его к проекту.

Изменим метод модели data():

QVariant QListViewExplorerModel::data( const QModelIndex &index, int role ) const
{
    QVariant value;

        switch ( role )
        {
            case Qt::DisplayRole: //string
            {
                value = this->aDirList->at(index.row()).fileName();
            }
            break;

            case Qt::DecorationRole: //icon
            {
                if (this->aDirList->at(index.row()).isDir()) {
                    QPixmap icon = QPixmap(":/img/folder.png");
                    QPixmap tmp = icon.scaled(30, 30, Qt::KeepAspectRatio);
                    value = tmp;
                    break;
                }

                if (this->aDirList->at(index.row()).isFile()) {
                    QPixmap icon = QPixmap(":/img/file.png");
                    QPixmap tmp = icon.scaled(30, 30, Qt::KeepAspectRatio);
                    value = tmp;
                    break;
                }

                value = this->aDirList->at(index.row()).fileName();
            }
            break;

            case Qt::UserRole: //data
            {
                value = this->aDirList->at(index.row()).fileName();
            }
            break;

            default:
                break;
        }

    return value;
}

Запустим:

2020-12-17_17-52-25.png 

Здесь все просто, для роли Qt::DecorationRole мы возвращаем экземпляр класса QPixmap, в зависимости от типа элемента мы используем разные иконки и каждую, дополнительно, масштабируем.

Обратите внимание, мы используем временную переменную, для того, чтобы создать саму ионку. Если вы попробуете написать вот так:

value = QPixmap(":/img/file.png").scaled(30, 30, Qt::KeepAspectRatio);

То всё будет работать, но вы можете получить множество ошибок:

QPixmap::scaled: Pixmap is a null pixmap

У меня на одном ПК они появлялись, а на другом нет, неясно с чем это связано, поэтому я и использую временную переменную.

Фильтруем элементы списка

Осталось убрать папку с именем «.»

Для этого мы можем использовать фильтры метода entryInfoList()

    *dirList = dir.entryInfoList(QDir::NoDot | QDir::Files | QDir::Dirs, QDir::DirsFirst);

В данном случае мы использовали фильтр:

QDir::NoDot | QDir::Files | QDir::Dirs

Что означает: не показывать пункт с одной точкой, показывать файлы и папки.

Вы можете избавиться и от пункта с двумя точками, для этого достаточно добавить к фильтру еще одно значение:

QDir::NoDot | QDir::Files | QDir::Dirs | QDir::NoDotDot

Или переписать так:

QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs

Что даст тот же результат.

Добавляем иконку для zip архивов

Давайте добавим еще кое-что, у всех файлов независимо от расширения, одна и та же иконка, давайте сделаем для отдельную zip-файлов.

Для таких файлов у нас уже есть иконка, осталось добавить код. Модернизируем метод data() нашей модели:

QVariant QListViewExplorerModel::data( const QModelIndex &index, int role ) const
{

    QVariant value;

        switch ( role )
        {
            case Qt::DisplayRole: //string
            {
                value = this->aDirList->at(index.row()).fileName();
            }
            break;

            case Qt::DecorationRole: //icon
            {
                if (this->aDirList->at(index.row()).isDir()) {
                    QPixmap icon = QPixmap(":/img/folder.png");
                    QPixmap tmp = icon.scaled(30, 30, Qt::KeepAspectRatio);
                    value = tmp;
                    break;
                }

                if (this->aDirList->at(index.row()).isFile()) {
                    QString fileExt = this->aDirList->at(index.row()).completeSuffix();

                    if (fileExt == "zip") {

                        QPixmap icon = QPixmap(":/img/zip.png");
                        QPixmap tmp = icon.scaled(30, 30, Qt::KeepAspectRatio);
                        value = tmp;
                        break;

                    }

                    QPixmap icon = QPixmap(":/img/file.png");
                    QPixmap tmp = icon.scaled(30, 30, Qt::KeepAspectRatio);
                    value = tmp;
                    break;
                }

                value = this->aDirList->at(index.row()).fileName();
            }
            break;

            case Qt::UserRole: //data
            {
                value = this->aDirList->at(index.row()).fileName();
            }
            break;

            default:
                break;
        }

    return value;
}

Теперь вам нужно создать архив в папке, где осуществляется сборка проекта, у меня это папка на уровень выше, чем папка с проектом и называется она:

build-QListViewExplorer-Desktop_Qt_MinGW_w64_64bit_MSYS2-Debug

Запускаем:

2020-12-18_12-52-46.png

У архива своя иконка.

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

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

Открываем файлы, как в Проводнике Windows

Попробуем реализовать схожий функционал проводника – запуск/открытие файла при двойном щелчке.

Добавим слот для нашего QListView:

void MainWindow::on_listView_doubleClicked(const QModelIndex &index)
{
    QDesktopServices::openUrl(QUrl(this->aDirList->at(index.row()).absoluteFilePath()));
}

При двойном щелчке на архиве запускается 7-Zip, как и ожидалось.

Добавляем адресную строку

Добавим адресную строку сверху от нашего QListView, чтобы пользователь мог видеть текущий каталог.

На форму добавим компонент LineEdit и разместим его выше QListView.

Добавим в конец конструктора формы строку:

this->ui->lineEdit->setText(QDir::currentPath());

Запустим:

2020-12-18_14-01-56.png

Путь появился в строке.

Меняем текущую папки при двойном щелчке

Давайте добавим функционал перехода в папку, при двойном щелчке на нее.

Изменим метод слота on_listView_doubleClicked()

void MainWindow::on_listView_doubleClicked(const QModelIndex &index)
{
    if (this->aDirList->at(index.row()).isDir())
    {
        QString tmp = this->aDirList->at(index.row()).absoluteFilePath();
        model->getFolderList(this->aDirList->at(index.row()).absoluteFilePath(),this->aDirList);
        this->ui->lineEdit->setText(tmp);
    } else {
        QDesktopServices::openUrl(QUrl(this->aDirList->at(index.row()).absoluteFilePath()));
    }
}

Внесем изменения в метод модели getFolderList() 

void QListViewExplorerModel::getFolderList(QString folderPath, QFileInfoList *dirList)
{
    QDir dir = QDir(folderPath);

    *dirList = dir.entryInfoList(QDir::NoDot | QDir::Files | QDir::Dirs, QDir::DirsFirst);

    this->beginResetModel();
        this->aDirList = dirList;
    this->endResetModel();
}

Запустим.

Теперь вы можете сменить текущую папку, с помощью двойного щелчка по пункту «..» или имени папки.

Рассмотрим получившийся код: 

В слоте по двойному щечку мы проверяем является ли выбранный элемент папкой, если является, то мы вызываем метод модели и заново загружаем элементов список элементов с помощью getFolderList(), но уже для новой папки. Не забываем обновить имя текущей папки в строке адреса.

В методе модели getFolderList(), после того как мы получили список элементов указанной папки мы используем методы beginResetModel() и endResetModel() чтобы уведомить модель о том, что произошел сброс модели, так как данные у нас целиком поменялись.

Переходим на уровень выше с помощью кнопки

Добавим кнопку, чтобы перейти в папку на уровень выше, чем текущая.

На форму добавим кнопку QPushButton.

Назначим ей иконку folderup.png  из ресурса

Удалим в редакторе текст для кнопки, чтобы она использовала иконку.

Добавим к кнопке слот на одиночный щелчок: 

void MainWindow::on_pushButton_clicked()
{
    currentFolder.cdUp();

    this->ui->lineEdit->setText(currentFolder.absolutePath());
    model->getFolderList(currentFolder.absolutePath(),this->aDirList);
}

Изменим конструктор формы:

заменим

    this->model->getFolderList(".",this->aDirList);

на

    this->currentFolder = QDir(QDir::currentPath());

    this->model->getFolderList(this->currentFolder.absolutePath(),this->aDirList);

Добавим в заголовок формы приватное поле

private:
	QDir currentFolder;

Оно нам понадобится, чтобы хранить информацию о текущей папке.

Запускаем. Теперь по щелчку по этой кнопке, мы можем перейти на каталог выше.

Переключаемся между списком и иконками

Доработаем нашу программу еще немного – добавим две кнопки для переключения между отображением списком и иконками.

Добавим две кнопки на форму

Назначим одной иконку из ресурса list.png, а другой iconlist.png

2020-12-18_17-49-12.png

Добавим слоты для кнопок

Для перехода в режим списка:

void MainWindow::on_pushButton_3_clicked()
{
    this->ui->listView->setViewMode(QListView::ListMode);
}

Для перехода в режим иконок:

void MainWindow::on_pushButton_2_clicked()
{
    this->ui->listView->setViewMode(QListView::IconMode);
}

Запускаем:

 2020-12-18_17-51-07.png

При переходе в режим иконок всё съезжает и выглядит очень некрасиво. Доработает методы:

void MainWindow::on_pushButton_3_clicked()
{
    this->ui->listView->setUniformItemSizes(false);
    this->ui->listView->setViewMode(QListView::ListMode);

}

void MainWindow::on_pushButton_2_clicked()
{
    this->ui->listView->setUniformItemSizes(true);
    this->ui->listView->setViewMode(QListView::IconMode);
}

Запускаем:

2020-12-18_17-52-50.png

Уже лучше.

Управляем отображением иконок в окне:

Добавим в конструктор окна строку 

this->ui->listView->setWrapping(true);

Теперь у нас папки отображаются в несколько колонок.

2020-12-19_14-42-46.png

Заменим эту строку на

this->ui->listView->setWordWrap(true);

2020-12-19_14-47-13.png

Имена файлов будут переноситься на новую строку, чтобы вместиться по ширине.

Заключение

Сегодня мы начали создание собственного файлового менеджера, с файлами и папками…

Был создан новый проект, на форму добавлен QListView

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

Настроили сортировку элементов списка таким образом, чтобы папки шли перед файлами.

Добавили иконки в ресурсы проекта.

Доработали модель таким образом, чтобы папки и файлы имели разные иконки.

Реализовали механизм, который при двойном щелчке на файл, открывает так же, как это делает Windows Explorer.

Добавили отдельную иконку для zip-файлов

Добавили кнопку для перехода на папку выше уровнем.

Добавили две кнопки для смены режима отображения между обычным списком и иконками.

Прочитано 623 раз Последнее изменение Среда, 13 января 2021 12:13