QTreeView and QFileSystemModel for showing only USB drives

2.1k views Asked by At

I'm trying to write a dialog that allows the user to select from among any mounted USB drive found on the system. In Windows, I can definitely get this information manually, using calls to both GetLogicalDriveStrings and GetDriveType, so I could create a simple list that way. But the user also needs to be able to navigate down into any one of the USB drives to choose the correct folder in which to write a file. I have taken a look at QFileSystemModel, but I don't see how to restrict (filter) it to only showing mounted USB drives, and their child folders/files. Does anyone have an idea of how this should best be done using the Qt framework?


Updated - 12/3/24:

docsteer, thanks for the suggestion. Sounds like the right way to go with this. I implemented the suggested change, and I'm having a crash happen most times that I run the application. It shows C:, and waits some time, then crashes with an error code of 255. I assume there is something that I didn't correctly hook up here, but haven't been able to figure that out as yet. Those times that it doesn't crash, I am still seeing the full list of available drives on the system, including the two USBs that I have plugged in, rather than just seeing the USBs. If I change line 42 in filesystemmodeldialog.cpp so that I pass "dir" in instead of "usbModel", there is no crash. Can you or anyone see anything here that could cause the crash, and any reason why the USBDriveFilterProxyModel that I created, which correctly chooses the two USBs from all mounted drives, is not working to filter the data in the view? I have given all files from my small test application, including the header generated from the .ui file, so that if anyone wants to run it to see what's happening, they can.

main.cpp:

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

mainwindow.cpp:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class FileSystemModelDialog;
class QFileSystemModel;

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    void detectUsb();

private:
    Ui::MainWindow *ui;
    FileSystemModelDialog *treeView;
    QFileSystemModel *fileSystemModel;
};

#endif // MAINWINDOW_H

mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "filesystemmodeldialog.h"

#include <QFileSystemModel>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    treeView = new FileSystemModelDialog(this);
    setCentralWidget(treeView);
    fileSystemModel = new QFileSystemModel;
}

MainWindow::~MainWindow()
{
    delete ui;
}

filesystemmodeldialog.h:

#ifndef FILESYSTEMMODELDIALOG_H
#define FILESYSTEMMODELDIALOG_H

#include "ui_filesystemmodelwidget.h"

#include <QWidget>
#include <QFileSystemModel>
#include <QItemDelegate>

class USBDriveFilterProxyModel;

class IconItemDelegate : public QItemDelegate
{
public:
    IconItemDelegate();
    QSize sizeHint ( const QStyleOptionViewItem & option, const QModelIndex & index ) const;
};

class IconFileSystemModel : public QFileSystemModel
{
    Q_OBJECT
public:
    virtual QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const;

};

class FileSystemModelDialog : public QWidget
{
    Q_OBJECT

public:
    explicit FileSystemModelDialog(QWidget *parent);
    ~FileSystemModelDialog();

private:
    Ui::FileSystemModelWidget *ui;
    IconFileSystemModel *dir;
    USBDriveFilterProxyModel *usbModel;

Q_SIGNALS:
    void signalFileSelected(QString);
};

#endif // FILESYSTEMMODELDIALOG_H

filesystemmodeldialog.cpp:

#include "filesystemmodeldialog.h"
#include "usbdrivefilter.h"

IconItemDelegate::IconItemDelegate(){}

QSize IconItemDelegate::sizeHint ( const QStyleOptionViewItem & /*option*/, const QModelIndex & index ) const
{
    const QFileSystemModel *model = reinterpret_cast<const QFileSystemModel *>(index.model());
    QFileInfo info = model->fileInfo(index);
    if(info.isDir())
    {
        return QSize(40,40);
    }
    else
    {
        return QSize(64,64);
    }
}

QVariant IconFileSystemModel::data ( const QModelIndex & index, int role ) const
{
    // will do more, but for now, just paints to view
    return QFileSystemModel::data(index, role);
}

FileSystemModelDialog::FileSystemModelDialog(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::FileSystemModelWidget)
{
    ui->setupUi(this);

    dir = new IconFileSystemModel();
    dir->setRootPath("\\");
    dir->setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);

    usbModel = new USBDriveFilterProxyModel(this);
    usbModel->setSourceModel(dir);
    usbModel->setDynamicSortFilter(true);

    IconItemDelegate *iconItemDelegate = new IconItemDelegate();

    ui->treeView->setModel(usbModel);
    // hide unneeded hierachy info columns
    ui->treeView->hideColumn(1);
    ui->treeView->hideColumn(2);
    ui->treeView->hideColumn(3);
    ui->treeView->setItemDelegate(iconItemDelegate);
}

FileSystemModelDialog::~FileSystemModelDialog()
{
    delete dir;
}

usbdrivefilter.h:

#ifndef USBDRIVEFILTER_H
#define USBDRIVEFILTER_H

#include <QSortFilterProxyModel>
#include <QModelIndex>
#include <QString>

class USBDriveFilterProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT
public:
    explicit USBDriveFilterProxyModel(QObject *parent = 0);

protected:
    virtual bool filterAcceptsRow(
        int source_row, const QModelIndex &source_parent) const;

private:
    void getMountedRemovables();

private:
    QList<QString> removables;

};

#endif // USBDRIVEFILTER_H

usbdrivefilter.cpp:

#include "usbdrivefilter.h"

#include <windows.h>

USBDriveFilterProxyModel::USBDriveFilterProxyModel(QObject *parent) :
    QSortFilterProxyModel(parent)
{
    getMountedRemovables();
    // will eventually also register for changes to mounted removables
    // but need to get passed my current issue of not displaying only USBs.
}

bool USBDriveFilterProxyModel::filterAcceptsRow(int sourceRow,
    const QModelIndex &sourceParent) const
{
    QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);

    // Since drive string can have more than just "<DriveLetter>:", need
    // to check each entry in the usb list for whether it is contained in
    // the current drive string.

    for (int i = 0; i < removables.size(); i++)
    {
        if (sourceModel()->data(index0).toString().contains(removables[i]))
        {
            return true;
        }
    }
    return false;
}

void USBDriveFilterProxyModel::getMountedRemovables()
{
    DWORD test = GetLogicalDrives();

    DWORD mask = 1;
    UINT type = 0;
    WCHAR wdrive[] = L"C:\\"; // use as a drive letter template
    for (int i = 0; i < 32; i++)
    {
        if (test & mask)
        {
            wdrive[0] = (char)('A' + i); // change letter in template
            type = GetDriveType(wdrive);
            switch (type) {
            case DRIVE_REMOVABLE:
            {
                QString qdrive = QString((char)('A' + i)) + ":";
                removables.append(qdrive);
                break;
            }
            default: break;
            }
        }
        mask = mask << 1;
    }
}

ui_filesystemmodelwidget.h:

#ifndef UI_FILESYSTEMMODELWIDGET_H
#define UI_FILESYSTEMMODELWIDGET_H

#include <QtWidgets/QApplication>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QTreeView>

QT_BEGIN_NAMESPACE

class Ui_FileSystemModelWidget
{
public:
    QTreeView *treeView;

    void setupUi(QWidget *FileSystemModelWidget)
    {
        if (FileSystemModelWidget->objectName().isEmpty())
            FileSystemModelWidget->setObjectName(QStringLiteral("FileSystemModelWidget"));
        FileSystemModelWidget->resize(670, 755);
        treeView = new QTreeView(FileSystemModelWidget);
        treeView->setGeometry(QRect(0, 0, 670, 531));
        treeView->setObjectName(QStringLiteral("treeView"));
        treeView->setAutoFillBackground(true);
        treeView->setStyleSheet(QLatin1String(" QScrollBar:vertical {\n"
"      width: 61px;\n"
"   background-color: rgb(227, 227, 227);\n"
"  }\n"
"  QScrollBar::handle:vertical {\n"
"      min-height: 50px;\n"
"  }\n"
"\n"
"QTreeView, QListView {\n"
"   alternate-background-color: rgb(226, 226, 226);\n"
"   font-size: 16px;\n"
"     show-decoration-selected: 1;\n"
" }\n"
"\n"
" QTreeView::item, QListView::item {\n"
" height: 22px;\n"
"      border: 1px solid transparent;\n"
"     border-top-color: transparent;\n"
"     border-bottom-color: transparent;\n"
" }\n"
"\n"
" QTreeView::item:selected, QListView::item::selected {\n"
"     border: 1px solid #567dbc;\n"
"   background-color: rgb(85, 170, 255);\n"
" }\n"
"\n"
"\n"
" QTreeView::branch:has-siblings:!adjoins-item {\n"
"     border-image: url(:/new/prefix1/images/vline.png) 0;\n"
" }\n"
"\n"
" QTreeView::branch:has-siblings:adjoins-item {\n"
"     border-image: url(:/new/prefix1/images/branch-more.png) 0;\n"
" }\n"
"\n"
" QTreeView::branch:!has-children:!has-siblings:adjoins-item {\n"
"     border-image: url"
                        "(:/new/prefix1/images/branch-end.png) 0;\n"
" }\n"
"\n"
" QTreeView::branch:has-children:!has-siblings:closed,\n"
" QTreeView::branch:closed:has-children:has-siblings {\n"
"         border-image: none;\n"
"         image: url(:/new/prefix1/images/branch-closed.png);\n"
" }\n"
"\n"
" QTreeView::branch:open:has-children:!has-siblings,\n"
" QTreeView::branch:open:has-children:has-siblings  {\n"
"         border-image: none;\n"
"         image: url(:/new/prefix1/images/branch-open.png);\n"
" }\n"
""));
        treeView->setFrameShape(QFrame::Box);
        treeView->setFrameShadow(QFrame::Plain);
        treeView->setHorizontalScrollMode(QAbstractItemView::ScrollPerItem);
        treeView->setExpandsOnDoubleClick(true);
        treeView->header()->setVisible(false);
        treeView->header()->setStretchLastSection(true);

        retranslateUi(FileSystemModelWidget);

        QMetaObject::connectSlotsByName(FileSystemModelWidget);
    } // setupUi

    void retranslateUi(QWidget *FileSystemModelWidget)
    {
        FileSystemModelWidget->setWindowTitle(QApplication::translate("FileSystemModelWidget", "Form", 0));
    } // retranslateUi

};

namespace Ui {
    class FileSystemModelWidget: public Ui_FileSystemModelWidget {};
} // namespace Ui

QT_END_NAMESPACE

#endif // UI_FILESYSTEMMODELWIDGET_H

Updated - 12/4/24:

So I found that the crash takes place in IconItemDelegate::sizeHint(), in the filesystemusbmodeldialog.cpp file. The run gets to line 9:

QFileInfo info = model->fileInfo(index);

and stepping over at that point gives an access violation. I assume that this is because I replaced the IconFileSystemUsbModel object with the USBDriveFilterProxyModel as the QTreeView model in the FileSystemUsbModelDialog constructor. I am assuming therefore that casting the index.model() in IconItemDelegate::sizeHint() is an incorrect cast, and that I now need to get hold of the original source model before calling fileInfo(). So I changed the sizeHint() overload to the following:

QSize IconItemUsbDelegate::sizeHint ( const QStyleOptionViewItem & /*option*/, const QModelIndex & index ) const
{
    const USBDriveFilterProxyModel *model = reinterpret_cast<const USBDriveFilterProxyModel *>(index.model());
    const QFileSystemModel *fsmodel = reinterpret_cast<const QFileSystemModel *>(model->sourceModel());
    QFileInfo info = fsmodel->fileInfo(index);
    if(info.isDir())
    {
        return QSize(40,40);
    }
    else
    {
        return QSize(64,64);
    }
}

But that didn't work. Then I found a link that seemed to say I needed to call setRootIndex() on my view, now that the proxy is in place of the model, so I added that in my FileSystemUsbModelDialog constructor which now looks like this:

FileSystemUsbModelDialog::FileSystemUsbModelDialog(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::FileSystemUsbModelWidget)
{
    ui->setupUi(this);

    dir = new IconFileSystemUsbModel();
    dir->setRootPath("\\");
    dir->setFilter(QDir::AllDirs | QDir::NoDotAndDotDot);

    usbModel = new USBDriveFilterProxyModel(this);
    usbModel->setSourceModel(dir);
    usbModel->setDynamicSortFilter(false);

    IconItemUsbDelegate *iconItemDelegate = new IconItemUsbDelegate();

    ui->treeView->setModel(usbModel);
    ui->treeView->setRootIndex(usbModel->mapFromSource(dir->setRootPath("\\")));
    // hide unneeded hierachy info columns
    ui->treeView->hideColumn(1);
    ui->treeView->hideColumn(2);
    ui->treeView->hideColumn(3);
    ui->treeView->setItemDelegate(iconItemDelegate);
}

This didn't work. I went back to my IconItemUsbDelegate::sizeHint() and changed it back, thinking that maybe setting root on the view was all I really needed to do, and no luck.

Any thoughts?

2

There are 2 answers

0
bmahf On BEST ANSWER

So docsteer got me on the right track, as I stated in my updates, but as I also stated, I got a crash when going into my item delegate's sizeHint() method. Per someone else's suggestion, I put some debug statements in to find out what the index showed as follows:

qDebug() << index.isValid();
qDebug() << "text = " <<  index.data();
qDebug() << "Row = " << index.row()  << "Column = " << index.column();

and I found that the content of the index was specific to what I would expect the proxy model to contain, rather than the file system model. Looking more closely, I realized that I had been passing the index associated with the proxy model into the fileInfo() method for a cast to the file system model. I changed it as shown below so that I first cast the index's model to a pointer of the proxy model type, then I get the source (file system) model pointer from that and call its fileInfo() method. But now I first map the index to the source index and I pass the result of that mapping into the file system model's fileInfo() method, which now works like a charm:

const USBDriveFilterProxyModel *model = reinterpret_cast<const USBDriveFilterProxyModel *>(index.model());
const QFileSystemModel *fsmodel = reinterpret_cast<const QFileSystemModel *>(model->sourceModel());
QFileInfo info = fsmodel->fileInfo(index);
if(info.isDir())
{
    return QSize(40,40);
}
else
{
    return QSize(64,64);
}

Thanks for the help.

0
docsteer On

I would suggest doing this with a QSortFilterProxyModel. An example might look like

.header

class USBDriveFilter : public QSortFilterProxyModel
{
    Q_OBJECT;
public:
    USBDriveFilter(QObject *parent = 0);
protected:
    // Reimplemented from QSortFilterProxyModel
    virtual bool filterAcceptsRow ( int source_row, const QModelIndex & source_parent ) const;
};

.cpp

bool USBDriveFilter::filterAcceptsRow(int sourceRow,
         const QModelIndex &sourceParent) const
{
    QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
    // This is a naive example and just doesn't accept the drive if the name
    // of the root node contains C: - you should extend it to check the letter
    // against your known list of USB drives derived from the windows API
    return (!sourceModel()->data(index0).toString().contains("C:"));
}

To use this you would do something like

QFileSystemModel *m = new QFileSystemModel(this);
USBDriveFilter *filter = new USBDriveFilter(this);
filter->setSourceModel(m);
// Now use filter as your model to pass into your tree view.