Using Qt 6.2.4, Ubuntu environment, I derived from QComboBox
to set a QTreeView
as its view.
It contains a tree (folders and files) with multiple parents and children, several levels of folders is possible.
All is working fine and behaves as I want.
Now, when one item is selected I'd like to use up and down arrow keys to select the previous or next item of the same parent.
I tried several things, I get the right sibling but I am not able to update the comboBox view accordingly (the comboBox always shows the item selected before, nothing is updated).
main.cpp:
#include <QApplication>
#include "tree-combobox.h"
int main(int argc, char **argv)
{
QApplication app (argc, argv);
TreeComboBox combo;
combo.setGeometry(0, 0, 400, 50);
combo.ShowFileList("/my/path/", "*");
combo.show();
return app.exec();
}
tree-combobox.h:
#include <QComboBox>
#include <QTreeView>
#include <QFileSystemModel>
#include <QMouseEvent>
#include <QAbstractItemView>
class TreeComboBox : public QComboBox
{
public:
TreeComboBox(QWidget* parent = 0) : QComboBox(parent)
{
QTreeView* tree = new QTreeView(this); // tree view for combobox
setView(tree); // assign it to combobox
}
void ShowFileList(QString path, QString filesFilter) // fill combobox with folders and files, specify path and wildcard(s)
{
// block signals
this->blockSignals(true); // no signals emitted while stuffing the widget
//// create files model
QFileSystemModel *fileModel = new QFileSystemModel(this); // file system model to use
// set options to file model
fileModel->setReadOnly(true); // set it read-only
fileModel->setFilter(QDir::AllDirs | QDir::AllEntries |QDir::NoDotAndDotDot); // all folders, all files, no file beginning with a dot
fileModel->setOption(QFileSystemModel::DontUseCustomDirectoryIcons); // don't use icons from the files
fileModel->setOption(QFileSystemModel::DontWatchForChanges); // the widget won't track changes on disk
// set file filter for files model
QStringList filter; // for files wildcard
filter << filesFilter;
fileModel->setNameFilters(filter);
// set root for files model
fileModel->setRootPath("");
//// create view
// tree view
QTreeView *view = new QTreeView;
this->setView(view); // assign tree view to combobox
// files model
this->setModel(fileModel); // assign files model to combobox
// remove columns in tree view, to keep only filenames
QModelIndex index = fileModel->index(path);
for (int i = 1; i < fileModel->columnCount(); ++i) // all columns but first one
view->hideColumn(i);
// tree view options
view->setAnimated(true); // animated
view->setSortingEnabled(true); // sorting enabled by clicking on header
view->sortByColumn(0, Qt::AscendingOrder); // sort values
view->expand(index); // expand the view from the given path
view->scrollTo(index); // set view from given path
view->setRootIndex(index); // set root index to given path
// allow signals again
this->blockSignals(false);
}
QString GetFile() // get selected value from list
// currentItemChanged() is emitted each time an item is clicked, even a parent item
// this function returns an empty QString if the clicked item is not valid (i.e. a folder)
{
QModelIndex index = view()->currentIndex(); // current value index from QTree
QString path = model()->data(index, QFileSystemModel::FilePathRole).toString(); // get full path value
QFileInfo info(path); // to test this value
if (info.isFile()) // if the value is really a file
return path; // ... return its full path
else // value is a folder
return QString(); // ... so return nothing
}
private:
virtual void hidePopup() // control popup hiding behaviour
// for a combobox, each time an item is clicked the popup disappears... but not for a folder this time !
{
if (!view()->underMouse()) { // is mouse over QTreeView ?
QComboBox::hidePopup(); // if not collapse the comboBox
return;
}
QModelIndex index = view()->currentIndex(); // get current index of selected item
if (!model()->hasChildren(index)) // if it doesn't have children (so it is not a folder)
QComboBox::hidePopup(); // collapse the comboBox
}
virtual void keyPressEvent(QKeyEvent *keyboardEvent) // keyboard event
{
if (this->hasFocus()) { // widget has to be active to accept keyboard keys
if (keyboardEvent->key() == Qt::Key_Up) { // up
//view()->setCurrentIndex(view()->currentIndex().sibling(view()->currentIndex().row() - 1, 0));
//view()->selectionModel()->select(view()->currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Rows);
QModelIndex index = view()->currentIndex();
int n = index.row() - 1;
QModelIndex sibling = index.siblingAtRow(n);
if (sibling.isValid()) {
view()->setCurrentIndex(sibling);
view()->scrollTo(sibling);
//view()->selectionModel()->setCurrentIndex(sibling, QItemSelectionModel::ClearAndSelect);
//view()->selectionModel()->select(sibling, QItemSelectionModel::Select | QItemSelectionModel::Rows);
//tree->selectionModel()->select(tree->currentIndex(), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
//this->setCurrentIndex(sibling.row());
}
keyboardEvent->accept(); // accept keyboard event
}
else if (keyboardEvent->key() == Qt::Key_Down) { // down
view()->setCurrentIndex(view()->currentIndex().sibling(view()->currentIndex().row() - 1, 0));
keyboardEvent->accept();
}
}
}
};
What's working so far in keyPressEvent()
: the sibling
index (QModelIndex
) is the right one.
What I tried: the lines commented out with //
.
Desired result: the comboBox selects and shows the previous or next item in the list, for the same parent (no need to go up or down to another parent).
Both the tree view and combobox need to be updated on key press, in order for the items to be visibly updated on the combobox.
And since it appears a rootModelIndex
needs to be set, and the initial root getting lost on subfolders, I added a QModelIndex
as a member to store it in.
Modified class with explanatory comments:
class TreeComboBox : public QComboBox
{
public:
//add a new memeber to save the root index
QModelIndex rootIndex;
TreeComboBox(QWidget* parent = 0) : QComboBox(parent)
{
QTreeView* tree = new QTreeView(this);
setView(tree);
}
void ShowFileList(QString path, QString filesFilter)
{
this->blockSignals(true);
QFileSystemModel *fileModel = new QFileSystemModel(this);
fileModel->setReadOnly(true);
fileModel->setFilter(QDir::AllDirs | QDir::AllEntries |QDir::NoDotAndDotDot);
fileModel->setOption(QFileSystemModel::DontUseCustomDirectoryIcons);
fileModel->setOption(QFileSystemModel::DontWatchForChanges);
QStringList filter;
filter << filesFilter;
fileModel->setNameFilters(filter);
fileModel->setRootPath("");
QTreeView *view = new QTreeView;
this->setView(view);
this->setModel(fileModel);
QModelIndex index = fileModel->index(path);
for (int i = 1; i < fileModel->columnCount(); ++i)
view->hideColumn(i);
view->setAnimated(true);
view->setSortingEnabled(true);
view->sortByColumn(0, Qt::AscendingOrder);
view->expand(index);
view->scrollTo(index);
view->setRootIndex(index);
//save the root index
rootIndex=index;
//then set to your comboBox
setRootModelIndex(index);
this->blockSignals(false);
}
QString GetFile()
{
QModelIndex index = view()->currentIndex();
QString path = model()->data(index, QFileSystemModel::FilePathRole).toString();
QFileInfo info(path);
if (info.isFile())
return path;
else
return QString();
}
private:
virtual void hidePopup()
{
if (!view()->underMouse()) {
QComboBox::hidePopup();
return;
}
QModelIndex index = view()->currentIndex();
if (!model()->hasChildren(index))
QComboBox::hidePopup();
}
virtual void keyPressEvent(QKeyEvent *keyboardEvent)
{
if (this->hasFocus())
{
//I just used this to avoid having to select an item by clicking on it
//you can remove it if it's of no use to you
if(!view()->currentIndex().isValid())
{
view()->setCurrentIndex(view()->indexAt(QPoint(0,0)));
}
if (keyboardEvent->key() == Qt::Key_Up)
{
QModelIndex index = view()->currentIndex();
int n = index.row() - 1;
QModelIndex sibling = index.siblingAtRow(n);
if (sibling.isValid())
{
//update view's current index
view()->setCurrentIndex(sibling);
//update combobox
setRootModelIndex(sibling.parent());
setCurrentIndex(sibling.row());
//this is where you save the root index from being lost
if(rootModelIndex()!=rootIndex)
setRootModelIndex(rootIndex);
}
keyboardEvent->accept();
}
else
if (keyboardEvent->key() == Qt::Key_Down)
{
QModelIndex index = view()->currentIndex();
int n = index.row() + 1;
QModelIndex sibling = index.siblingAtRow(n);
if(sibling.isValid())
{
//update view's current index
view()->setCurrentIndex(sibling);
//update combobox
setRootModelIndex(sibling.parent());
setCurrentIndex(sibling.row());
if(rootModelIndex()!=rootIndex)
setRootModelIndex(rootIndex);
}
keyboardEvent->accept();
}
}
}
};
Result:
For more: