QModelIndex as parent? - c++

In Qt, QModelIndex is used to represent an index to my understanding. Officially:
This class is used as an index into item models derived from
QAbstractItemModel. The index is used by item views, delegates, and
selection models to locate an item in the model.
But I see it being used to represent a parent object. For instance, if I want to get an index in a QFileSystemModel object, I need a row, column and a parent:
QModelIndex QFileSystemModel::index(int row, int column, const QModelIndex &parent = QModelIndex()) const
I am trying to get a QModelIndex object, but to do that, I need another QModelIndex object? I am merely trying to iterate over the model. I don't have a separate parent object. How do I just create an index from row/column number? I don't understand the role of QModelIndex as a "parent". Shouldn't the model itself know what the parent object is? We passed a pointer to the constructor when creating the model.
Here's a bit of code showing the problem:
#include "MainWindow.hpp"
#include "ui_MainWindow.h"
#include <QFileSystemModel>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
auto* model = new QFileSystemModel{ui->listView};
ui->listView->setModel(model);
ui->listView->setRootIndex(model->setRootPath("C:\\Program Files"));
connect(ui->pushButton, &QPushButton::clicked, [this] {
auto* model = static_cast<QFileSystemModel*>(ui->listView->model());
int row_count = model->rowCount();
for (int i = 0; i != row_count; ++i) {
qDebug() << model->fileName(model->index(i, 0)) << '\n';
}
});
}
Here I have a QListView object (*listView) and a QFileSystemModel object (*model). I would like to iterate over the model and do something, like print the names of the files. The output is
C:
No matter which directory the rootpath is. I assume that is because I did't pass anything as the parent.

You're just accessing the children of the root of the QFileSystemModel when you default the parent node to QModelIndex() in the call model->index(i, 0).
If you also want to list the children of those items, we'll want to iterate them, too:
#include <QApplication>
#include <QDebug>
#include <QFileSystemModel>
void list_files(const QFileSystemModel *model, QModelIndex ix = {},
QString indent = {})
{
auto const row_count = model->rowCount(ix);
for (int i = 0; i < row_count; ++i) {
auto const child = model->index(i, 0, ix);
qDebug() << qPrintable(indent) << model->fileName(child);
list_files(model, child, indent + " ");
}
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QFileSystemModel model;
model.setRootPath(".");
list_files(&model);
}
See how we pass the child index as the new parent when we recurse into list_files()?
Note that the model is likely incomplete at this stage, as it implements lazy reading - so don't expect to see all your files with this simple program.

Related

How to connect two QComboBox so that the second shows only certain items?

I have two QComboBoxes to connect with each other.
In particular, I'd like that if a scaling is made in the first QComboBox, this is not also shown in the second QComboBox and vice versa..
This is my code:
auto lingua_originaleT = new QComboBox();
lingua_originaleT->addItems({"Italiano", "Inglese", "Francese", "Spagnolo", "Portoghese", "Tedesco", "Cinese"});
auto lingua_targetT = new QComboBox();
lingua_targetT->addItems({"Italiano", "Inglese", "Francese", "Spagnolo", "Portoghese", "Tedesco", "Cinese"});
The result should be like this:
The same language should not appear in the second drop-down menu as well
One possible solution is to use QSortFilterProxyModel to do the filtering:
#include <QApplication>
#include <QComboBox>
#include <QHBoxLayout>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QWidget>
class FilterProxyModel: public QSortFilterProxyModel{
public:
using QSortFilterProxyModel::QSortFilterProxyModel;
protected:
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const{
if(filterRegExp().isEmpty())
return true;
return !QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget widget;
widget.resize(640, 480);
widget.show();
QStandardItemModel *model = new QStandardItemModel(&widget);
const QStringList values{"Italiano", "Inglese", "Francese", "Spagnolo", "Portoghese", "Tedesco", "Cinese"};
for(const QString & value: values){
model->appendRow(new QStandardItem(value));
}
QComboBox *lingua_originaleT = new QComboBox;
FilterProxyModel *proxy_originalT = new FilterProxyModel(&widget);
proxy_originalT->setSourceModel(model);
lingua_originaleT->setModel(proxy_originalT);
QComboBox *lingua_targetT = new QComboBox;
FilterProxyModel *proxy_targetT = new FilterProxyModel(&widget);
proxy_targetT->setSourceModel(model);
lingua_targetT->setModel(proxy_targetT);
QHBoxLayout *lay = new QHBoxLayout(&widget);
lay->addWidget(lingua_originaleT);
lay->addWidget(lingua_targetT);
QObject::connect(lingua_originaleT, &QComboBox::currentTextChanged, proxy_targetT, &FilterProxyModel::setFilterFixedString);
QObject::connect(lingua_targetT, &QComboBox::currentTextChanged, proxy_originalT, &FilterProxyModel::setFilterFixedString);
lingua_originaleT->setCurrentIndex(0);
lingua_targetT->setCurrentIndex(1);
return a.exec();
}
Internally, QCombobox uses QStandardItemModel - unless you provided a custom one using setModel().
That means you can do things like:
// Just some setup
auto combo = new QComboBox(this);
combo->addItems({ "Item0", "Item1", "Item2" });
// Here is the interesting bit
auto model = qobject_cast<QStandardItemModel*>(combo->model());
auto item = model->item(0); // <-- index in the combobox
item->setEnabled(false); // <-- You can't select it anymore
if(combo->currentIndex() == 0) // Choose another one if it's already selected
combo->setCurrentIndex(1);
// From now, Item 0 will be visible in the dropdown but not selectable by the user.
I leave you find a way to get the 2 boxes disabling each other items (And importantly, enabling them back once if the selection change). This is a matter of listening the index changed signals of each boxes and and updating the other model accordingly.
I solved and the solution works perfectly!
Here's how I did it:
lingua_originaleT = new QComboBox();
lingua_targetT = new QComboBox();
QStringList traduzioneLangList = {"Italiano", "Inglese", "Francese", "Spagnolo", "Portoghese", "Tedesco", "Cinese"};
lingua_originaleT->clear();
lingua_originaleT->addItems(traduzioneLangList);
lingua_originaleT->setCurrentIndex(-1);
lingua_targetT->clear();
lingua_targetT->addItems(traduzioneLangList);
lingua_targetT->setCurrentIndex(-1);
connect(lingua_originaleT, &QComboBox::currentTextChanged, lingua_targetT, [=](const QString &selection) {
auto tarLangList = traduzioneLangList;
lingua_targetT->blockSignals(true);
tarLangList.removeOne(selection);
lingua_targetT->clear();
lingua_targetT->addItems(traduzioneLangList);
lingua_targetT->blockSignals(false);
});
connect(lingua_targetT, &QComboBox::currentTextChanged, lingua_originaleT, [=](const QString &selection) {
auto tarLangList = traduzioneLangList;
lingua_originaleT->blockSignals(true);
tarLangList.removeOne(selection);
lingua_originaleT->clear();
lingua_originaleT->addItems(traduzioneLangList);
lingua_originaleT->blockSignals(false);
});
lingua_originaleT->setCurrentIndex(0);
Many thanks to #lifof on reddit!

Filtering files of specific folder using QFileSystemModel and QSortFilterProxyModel subclass

I am trying to filter files of QFileSystemModel using QSortFilterProxyModel. The problem is, I want to only show the contents of a specific folder while filtering. Normally, if I wanted to only show the contents of a specific folder using QFileSystemModel, I would do something like this:
view = new QTreeView(this);
fSystemModel = new QFileSystemModel(this);
view->setModel(fSystemModel);
fSystemModel->setRootPath("C:/Qt");
QModelIndex idx = fSystemModel->index("C:/Qt");
view->setRootIndex(idx);
But when I use the QSortFilterProxyModel, the index has to be the QSortFilterProxyModel's. Since I could not find much information in Qt Documentation regarding this issue, I looked around and found this thread. Using this as a base, I created the following:
MainWindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
layout = new QVBoxLayout();
ui->centralWidget->setLayout(layout);
view = new QTreeView(this);
fSystemModel = new QFileSystemModel(this);
filter = new FilterModel();
filter->setSourceModel(fSystemModel);
layout->addWidget(view);
view->setModel(filter);
fSystemModel->setRootPath("C:/Qt");
QModelIndex idx = fSystemModel->index("C:/Qt");
QModelIndex filterIdx = filter->mapFromSource(idx);
qDebug() << filterIdx.isValid();
view->setRootIndex(filterIdx);
}
FilterModel.cpp (QSortFilterProxyModel subclass)
#include "filtermodel.h"
bool FilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
QModelIndex zIndex = sourceModel()->index(source_row, 0, source_parent);
QFileSystemModel* fileModel = qobject_cast<QFileSystemModel*>(sourceModel());
return fileModel->fileName(zIndex).contains("C"); //This line will have custom
//filtering behaviour in the future,
//instead of the name searching one.
}
However, when I run the program, it does not use the specified root index. Moreover, when I use qDebug() to see if the filterIdx is valid, it prints false. What am I doing wrong?
Try to see the result of the next line
qDebug() << idx << " " << fSystemModel->fileName(idx) << " " << filterIdx.isValid();
You can notice that fSystemModel->fileName(idx) is "Qt" (not full path "C:/Qt"). So it doesn't contain "C" from your filter (FilterModel::filterAcceptsRow).

How to Get Absolute Path of Currently Selected Item in QTreeWidget on mouse Clicked

I have a simple QTreeWidget pointing to the root directory:
#include <QTreeWidget>
#include <QStringList>
#include <QApplication>
int main(int argc, char **argv)
{
QApplication application(argc, argv);
QStringList fileNames{"TEST/branch", "trunk"};
QTreeWidget treeWidget;
treeWidget.setColumnCount(1);
for (const auto& filename : fileNames)
{
QTreeWidgetItem *parentTreeItem = new QTreeWidgetItem(&treeWidget);
parentTreeItem->setText(0, filename.split('/').first());
QStringList filenameParts = filename.split('/').mid(1);
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
}
}
treeWidget.show();
return application.exec();
}
Output:
The item I have selected above is /TEST/branches. How can I get the absolute path of the currently selected item?
Well, I don't think there is a built in function does that but you can write a function yourself like
QString treeItemToFullPath(QTreeWidgetItem* treeItem)
{
QString fullPath= treeItem->text(0);
while (treeItem->parent() != NULL)
{
fullPath= treeItem->parent()->text(0) + "/" + fullPath;
treeItem = treeItem->parent();
}
return fullPath;
}
edit:
Input treeItem is the selected tree item that you want to show its path. if you are sure at least one item is selected, you can get it by
treeWidget.selectedItems().first();
Another mehtod is using tooltips. You can add tip for each item, while you are adding them to tree, but you can do this after you add them in their final place.
change this
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
}
as this
for(const auto& filenamePart : filenameParts)
{
QTreeWidgetItem *treeItem = new QTreeWidgetItem();
treeItem->setText(0, filenamePart);
parentTreeItem->addChild(treeItem);
parentTreeItem = treeItem;
treeItem->setToolTip(0, treeItemToFullPath(treeItem));
}
this way you will see the full path whenever you hover the mouse on the item.
To get notified of the current item change, one can use QTreeWidget::currentItemChanged or QItemSelectionModel::currentChanged.
There are two main approaches to obtaining the full path:
Iterate up the tree from the selected item and reconstruct the path. This keeps the data model normalized - without redundant information.
Store full path to each item.
If the tree is large, storing the model normalized will use less memory. Given that selection of the items is presumably rare because it's done on explicit user input, the cost of iterating the tree to extract the full path is minuscule. Humans aren't all that fast when it comes to mashing the keys or the mouse button.
The example demonstrates both approaches:
// https://github.com/KubaO/stackoverflown/tree/master/questions/tree-path-41037995
#include <QtWidgets>
QTreeWidgetItem *get(QTreeWidgetItem *parent, const QString &text) {
for (int i = 0; i < parent->childCount(); ++i) {
auto child = parent->child(i);
if (child->text(0) == text)
return child;
}
return new QTreeWidgetItem(parent, {text});
}
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QStringList filenames{"TEST/branch", "TEST/foo", "trunk"};
QWidget window;
QVBoxLayout layout(&window);
QTreeWidget treeWidget;
QLabel label1, label2;
for (const auto &filename : filenames) {
QString path;
auto item = treeWidget.invisibleRootItem();
for (auto const &chunk : filename.split('/')) {
item = get(item, chunk);
path.append(QStringLiteral("/%1").arg(chunk));
item->setData(0, Qt::UserRole, path);
}
}
QObject::connect(&treeWidget, &QTreeWidget::currentItemChanged, [&](const QTreeWidgetItem *item){
QString path;
for (; item; item = item->parent())
path.prepend(QStringLiteral("/%1").arg(item->text(0)));
label1.setText(path);
});
QObject::connect(&treeWidget, &QTreeWidget::currentItemChanged, [&](const QTreeWidgetItem *item){
label2.setText(item->data(0, Qt::UserRole).toString());
});
layout.addWidget(&treeWidget);
layout.addWidget(&label1);
layout.addWidget(&label2);
window.show();
return app.exec();
}

How to add a string and an int to a QTableView when a button is clicked

How can I add a QSting and an int to a tableView at same time when a button is clicked?
What I want is to have the first column with names and the second column with numbers. Both items need to be added at the same row when a button is clicked.
Col 1--- Col 2---
First Name 1
Second Name 2
Third Name 3
Here is what I have which adds two strings, how can I convert the second cell to be an int?
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
model = new QStandardItemModel();
model->setRowCount(0);
ui->tableView->setModel(model);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
QStandardItem *userName = new QStandardItem(ui->lineEdit_Name->text());
QStandardItem *userNumber = new QStandardItem(ui->lineEdit_Number->text());
QList<QStandardItem*> row;
row <<userName << userNumber;
model->appendRow(row);
}
Thanks a lot
Does it really have to be an integer or can it just look like an integer?
Conventionally in tables, text is displayed left-aligned and numbers are displayed right-aligned. You can get this effect by setting the alignment for your second column:
QStandardItem *userNumber = new QStandardItem(ui->lineEdit_Number->text());
userNumber->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
If you need to use the data as an integer somewhere else in your program, you can convert the QString to an integer when you need it.
Ok, i will gather all in this answer:
First:
every data in a QStandardItemModel is a QVariant, so you can query the data either
to the model with
virtual QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const
to the QStandardItem with:
virtual QVariant data ( int role = Qt::UserRole + 1 ) const
Both will return a QVariant. You can get the int with
int toInt(bool * ok = 0) const
Note that the bool is optional, but will return true if the conversion was posible.
Also, you can check if it can be converted to an int:
bool QVariant::canConvert(int targetTypeId) const
yourVariant.canConvert( QMetaType::Int ) should return true.
To get what you want. I would use takeRow method from QStandardItemModel:
QList<QStandardItem *> takeRow(int row)
That its, the reverse to what you doing to append the row. So you know that you can ask the second element of the returned QList for the Int value.

What is wrong with this Qt code?

I was reading MVC tutorial and wanted to try out the code, but for some reason (which I'm not able to figure out) it is not working.
This code is suppose to show contents of current directory in QListWidget.
#include <QApplication>
#include <QFileSystemModel>
#include <QModelIndex>
#include <QListWidget>
#include <QListView>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QFileSystemModel *model = new QFileSystemModel;
QString dir = QDir::currentPath();
model->setRootPath(dir);
QModelIndex parentIndex = model->index(dir);
int numRows = model->rowCount(parentIndex);
QListWidget *list = new QListWidget;
QListWidgetItem *newItem = new QListWidgetItem;
for(int row = 0; row < numRows; ++row) {
QModelIndex index = model->index(row, 0, parentIndex);
QString text = model->data(index, Qt::DisplayRole).toString();
newItem->setText(text);
list->insertItem(row, newItem);
}
list->show();
return a.exec();
}
There are 2 problems.
The first described by Frank Osterfeld's answer. Move:
QListWidgetItem *newItem = new QListWidgetItem;
into your loop.
The second has to do with QFileSystemModel's threading model. from the docs for QFileSystemModel:
Unlike the QDirModel, QFileSystemModel uses a separate thread to populate itself so it will not cause the main thread to hang as the file system is being queried. Calls to rowCount() will return 0 until the model populates a directory.
and
Note: QFileSystemModel requires an instance of a GUI application.
I don't think QFileSystemModel() will work properly until after the Qt event loop is running (which is started by a.exec() in your example).
In your case, model->rowCount(parentIndex) returns 0, even though there are items in the directory (at least that's what it's doing on my test).
Replacing QFileSystemModel with QDirModel (and removing the model->setRootPath(dir) call, which QDirModel` doesn't support) populates the list.
You must create a new item for each row. Move
QListWidgetItem *newItem = new QListWidgetItem;
into the for loop.