I have the QVector cars that I want to filter basing on the car's registration number. I want to create a new filtered vector. I don't think that this is ok because i'm iterating 2 vectors, copying from the first one to the second one. Am I doing this right?
void MainWindow::on_actionBy_registration_number_triggered()
{
myDialog = new MyDialog(this);
myDialog->exec();
QString toSearchString = myDialog->getRegistrationNumber();
QVector<Vehicle> founded;
QVectorIterator<Vehicle> iterator(cars);
while(iterator.hasNext()){
Vehicle car = iterator.next();
QString num = car.getRegistration().getRegistrationNumber();
if(num.contains(toSearchString,Qt::CaseInsensitive)){
founded.append(car);
}
}
model = new QStandardItemModel(founded.size(),5,this);
//create header
createHeader(model);
int j = 0; //row
QVectorIterator<Vehicle> iter(founded);
while(iter.hasNext()){
Vehicle output = iter.next();
//set car
QString makeAndModel = output.getGeneralData().getMake() + output.getGeneralData().getModel();
QStandardItem *mAndM = new QStandardItem(QString(makeAndModel));
model->setItem(j,0,mAndM);
//set type
QStandardItem *type = new QStandardItem(QString(output.getGeneralData().getType()));
model->setItem(j,1,type);
//set mileage
QString mileageString = QString::number(output.getGeneralData().getMileage());
QStandardItem *mileage = new QStandardItem(QString(mileageString));
model->setItem(j,2,mileage);
//set year
QString yearString = QString::number(output.getGeneralData().getYear());
QStandardItem *year = new QStandardItem(QString(yearString));
model->setItem(j,3,year);
//set registration
QString regString = VehicleHelper::convertBoolToString(output.getRegistration().isRegistered());
QStandardItem *regDate = new QStandardItem(QString(regString));
model->setItem(j,4,regDate);
j++;
}
ui->tableView->setModel(model);
ui->tableView->setEnabled(false);
}
This can be done neatly using a proxy filter model. Below is a self-contained example that runs on both Qt 4 and 5.
// https://github.com/KubaO/stackoverflown/tree/master/questions/filter-18964377
#include <QtGui>
#if QT_VERSION_MAJOR > 4
#include <QtWidgets>
#endif
class Vehicle {
QString m_make, m_model, m_registrationNumber;
public:
Vehicle(const QString & make, const QString & model, const QString & registrationNumber) :
m_make{make}, m_model{model}, m_registrationNumber{registrationNumber} {}
QString make() const { return m_make; }
QString model() const { return m_model; }
QString registrationNumber() const { return m_registrationNumber; }
bool isRegistered() const { return !m_registrationNumber.isEmpty(); }
};
class VehicleModel : public QAbstractTableModel {
QList<Vehicle> m_data;
public:
VehicleModel(QObject * parent = {}) : QAbstractTableModel{parent} {}
int rowCount(const QModelIndex &) const override { return m_data.count(); }
int columnCount(const QModelIndex &) const override { return 3; }
QVariant data(const QModelIndex &index, int role) const override {
if (role != Qt::DisplayRole && role != Qt::EditRole) return {};
const auto & vehicle = m_data[index.row()];
switch (index.column()) {
case 0: return vehicle.make();
case 1: return vehicle.model();
case 2: return vehicle.registrationNumber();
default: return {};
};
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (orientation != Qt::Horizontal || role != Qt::DisplayRole) return {};
switch (section) {
case 0: return "Make";
case 1: return "Model";
case 2: return "Reg.#";
default: return {};
}
}
void append(const Vehicle & vehicle) {
beginInsertRows({}, m_data.count(), m_data.count());
m_data.append(vehicle);
endInsertRows();
}
};
class Widget : public QWidget {
QGridLayout m_layout{this};
QTableView m_view;
QPushButton m_button{"Filter"};
VehicleModel m_model;
QSortFilterProxyModel m_proxy;
QInputDialog m_dialog;
public:
Widget() {
m_layout.addWidget(&m_view, 0, 0, 1, 1);
m_layout.addWidget(&m_button, 1, 0, 1, 1);
connect(&m_button, SIGNAL(clicked()), &m_dialog, SLOT(open()));
m_model.append({"Volvo", "240", "SQL8941"});
m_model.append({"Volvo", "850", {}});
m_model.append({"Volvo", "940", "QRZ1321"});
m_model.append({"Volvo", "960", "QRZ1628"});
m_proxy.setSourceModel(&m_model);
m_proxy.setFilterKeyColumn(2);
m_view.setModel(&m_proxy);
m_dialog.setLabelText("Enter registration number fragment to filter on. Leave empty to clear filter.");
m_dialog.setInputMode(QInputDialog::TextInput);
connect(&m_dialog, SIGNAL(textValueSelected(QString)),
&m_proxy, SLOT(setFilterFixedString(QString)));
}
};
int main(int argc, char *argv[])
{
QApplication a{argc, argv};
Widget w;
w.show();
return a.exec();
}
Related
I have a QComboBox that contains QTableView like below. When I select a row, QComboBox title shows only "Alex" but i want "Alex - Alex address". How can i do it?
Thanks.
I suggested:
Have you tried signal QComboBox::currentIndexChanged(int index) in combination with QComboBox::setEditText()? I'm not quite sure whether this works as well if editable is false but might be worth to check out.
and OP asked for an answer as this seemed to be valuable.
So, here we go:
// standard C++ header:
#include <string>
#include <vector>
// Qt header:
#include <QtWidgets>
// table data entry
struct Entry {
int i;
std::string name;
std::string address;
};
// custom table model
class TableModel: public QAbstractTableModel {
private:
std::vector<Entry> _entries;
public:
explicit TableModel(QObject *pQParent = nullptr):
QAbstractTableModel(pQParent)
{ }
template <typename ITER>
TableModel(ITER first, ITER last, QObject *pQParent = nullptr) :
TableModel(pQParent)
{
for (; first != last; ++first) _entries.push_back(*first);
}
virtual ~TableModel() = default;
TableModel(const TableModel&) = delete;
TableModel& operator=(const TableModel&) = delete;
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
return (int)_entries.size();
}
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
return 3;
}
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
const size_t i = (size_t)index.row();
if (i >= _entries.size()) return QVariant();
if (role == Qt::DisplayRole) {
switch (index.column()) {
case 0: return _entries[i].i;
case 1: return QString::fromStdString(_entries[i].name);
case 2: return QString::fromStdString(_entries[i].address);
}
}
return QVariant();
}
};
// sample data
const Entry entries[] = {
{ 1, "Alex", "Alex address" },
{ 5, "Ben", "Ben address" },
{ 6, "Mary", "Mary address" },
{ 2, "Max", "Max address" },
{ 4, "Nicole", "Nicole address" },
{ 3, "Tim", "Tim address" }
};
// main application
int main(int argc, char **argv)
{
qDebug() << "Qt Version:" << QT_VERSION_STR;
QApplication app(argc, argv);
// setup GUI
TableModel qTblModel(std::begin(entries), std::end(entries));
QComboBox qCBox;
qCBox.setWindowTitle("Test QComboBox with Table Model");
QTableView qTblView(&qCBox);
qTblView.setModel(&qTblModel);
qTblView.horizontalHeader()->hide();
qTblView.verticalHeader()->hide();
qTblView.resizeColumnsToContents();
qTblView.setSelectionBehavior(QTableView::SelectRows);
qCBox.setView(&qTblView);
qCBox.setModel(&qTblModel);
qCBox.setEditable(true); // allow access to line edit
qCBox.lineEdit()->setReadOnly(true); // prevent editing in line edit
qCBox.show();
// install signal handlers
QObject::connect(&qCBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
[&](int i) {
if ((size_t)i < std::size(entries)) {
qCBox.setEditText(
QString::fromStdString(entries[i].name + " | " + entries[i].address));
}
});
qCBox.setCurrentIndex(-1);
// runtime loop
return app.exec();
}
Output:
It's in fact necessary to set QComboBox::editable to true to make QComboBox::setEditText() working.
To prevent accidental user-editing, I set in turn the embedded QLineEdit to readOnly.
assuming you have a table, you can set the att "Selection behaviour" to select Rows
I am currently working on a ticker client, that polls data from a web api, hands it to a ListModel, which is then used to display the data in a qml ListView.
class TickerClient : public QObject
{
Q_OBJECT
public:
explicit TickerClient(QObject *parent = nullptr);
QList<QVariantMap> items() const;
protected:
void registerErrorHandlers(QNetworkReply *reply);
signals:
void statusChanged(QNetworkAccessManager::NetworkAccessibility acc);
void dataChanged();
void preItemRefresh();
void postItemRefresh();
public slots:
void fetch(int start = 0, int limit = 100);
protected slots:
void onReceive(QNetworkReply *reply);
protected:
QSharedPointer<QNetworkAccessManager> mNetMan;
QList<QVariantMap> mItems;
};
class TickerModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(TickerClient *client READ client WRITE setClient)
public:
explicit TickerModel(QObject *parent = nullptr);
enum {
IdRole = Qt::UserRole + 1,
NameRole,
SymbolRole,
...
};
// Basic functionality:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
TickerClient *client() const;
void setClient(TickerClient *client);
private:
TickerClient *mClient;
};
The ticker client does not only fetch, but also handle the fetched data and expose it to the surrounding ListModel as a list of QVariantMaps.
void TickerClient::onReceive(QNetworkReply *reply)
{
if (!reply)
return;
if(reply->error()) {
qCritical() << "Error: "
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toString();
return;
}
// Read all data as json document.
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll());
if(!jsonDoc.isArray()) {
qCritical() << "Error: Expected array";
return;
}
emit preItemRefresh();
mItems.clear();
QJsonArray currencies = jsonDoc.array();
for(int i = 0; i < currencies.count(); i++) {
QJsonObject currency = currencies[i].toObject();
mItems.append(currency.toVariantMap());
}
emit postItemRefresh();
reply->deleteLater();
}
Both, the TickerClient and the TickerModel are exposed to qml:
qmlRegisterType<TickerModel>("Ticker", 1, 0, "TickerModel");
qmlRegisterUncreatableType<TickerClient>("Ticker", 1, 0, "TickerClient",
QStringLiteral("MarketCapProvider should not be created in QML"));
TickerClient tickerClient;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("tickerClient", &tickerClient);
The exposed client is then handed to the model and refreshed every 5 seconds:
ListView {
id: page
Timer {
interval: 5000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
var pos = scrollBar.position
tickerClient.fetch()
scrollBar.position = pos
}
}
ScrollBar.vertical: ScrollBar {
id: scrollBar
}
model: TickerModel {
client: tickerClient
}
delegate: RowLayout {
width: parent.width
spacing: 10
Label {
text: "#" + model.rank
padding: 5
}
Label {
text: model.name
padding: 5
}
}
}
However, the fetching of the data doesn't seem to work well as the wrong data is returned. So if I ask for model.name, I might end of with model.rank. The following code is used to fetch the entry for a given pair of index and role.
QVariant TickerModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !mClient)
return QVariant();
auto it = roleNames().find(role);
if(it != roleNames().end())
return mClient->items().at(index.row())[it.value()];
else
qDebug() << "Didn't match a role";
return QVariant();
}
QHash<int, QByteArray> TickerModel::roleNames() const
{
static const QHash<int, QByteArray> names{
{ IdRole, "id" },
{ NameRole, "name" },
...
};
return names;
}
TickerClient *TickerModel::client() const
{
return mClient;
}
void TickerModel::setClient(TickerClient *client)
{
beginResetModel();
if(mClient) {
mClient->disconnect(this);
}
mClient = client;
if(mClient) {
connect(mClient, &TickerClient::preItemRefresh, this, [=]() {
beginResetModel();
});
connect(mClient, &TickerClient::postItemRefresh, this, [=]() {
endResetModel();
});
}
endResetModel();
}
What, am I doing wrong and how can I extend this solution for more complex Json objects?
You are clearing mItems and then appending new items to it in onReceive method. You are not showing if model is listening to postItemRefresh signal. Anyway, you should call beginResetModel and endResetModel.
E.g. add following slot to your TickerModel and connect it to postItemRefresh signal to get your model synched witht the new data:
void TickerModel::reset()
{
beginResetModel();
endResetModel();
}
Edit:
You should add prints or go through with the debugger with what parameters your model data() is called when things get screwed. Also, make sure beginResetModel and endResetModel are called when items are updated.
Is your rowCount returning mClient->items().count() ?
To handle more complex json objects, instead of:
QJsonArray currencies = jsonDoc.array();
for(int i = 0; i < currencies.count(); i++) {
QJsonObject currency = currencies[i].toObject();
mItems.append(currency.toVariantMap());
}
You don't handle QVariantMap but parse more complex json objects to your own class which provides getters for data:
QJsonArray currencies = jsonDoc.array();
for(int i = 0; i < currencies.count(); i++) {
QJsonObject currency = currencies[i].toObject();
ComplexCurrency c;
c.read(currency);
mItems.append(c);
}
Your class:
class ComplexCurrency
{
public:
int id() const;
QString name() const;
bool enabled() const;
QList<QVariantMap> items();
void read(const QJsonObject &json);
private:
int m_id;
QString m_name;
bool m_enabled;
QList<QVariantMap> m_items;
...
};
void ComplexCurrency::read(const QJsonObject &json)
{
m_id = json["id"].toInt();
m_name = json["name"].toString();
m_enabled = json["enabled"].toBool();
// parse items array here
}
And then in model data():
QVariant TickerModel::data(const QModelIndex &index, int role) const
{
if ((index.row() < 0 || index.row() >= mClient->items().count()) || !mClient)
return QVariant();
const ComplexCurrency &c = mClient->items().at(index.row());
if (role == NameRole)
return c.name();
else if (role == SomethingRole)
return c.something();
return QVariant();
}
I'm new to Qt. I'm trying to create custom model for tree view with support of rows deletion. I've implemented it according to examples http://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html and http://doc.qt.io/qt-5/qtwidgets-itemviews-editabletreemodel-example.html. Also, I've made context menu with option to remove row after pressing with right mouse button on the row.
Now, I have hardly reproducible error (there is no exact pattern, but it is easy to obtain). When I start to delete rows from model randomly, sometimes my program crashes, sometimes I receive following messages to output:
QAbstractItemModel::endRemoveRows: Invalid index ( 1 , 0 ) in model QAbstractItemModel(0x55555580db10)
When program crashes, I almost always in fuction
QModelIndex TreeModel::parent(const QModelIndex &child) const
which is inherited from
QModelIndex QAbstractItemModel::parent(const QModelIndex &child) const
Stack of function calls shows that this function is called from
void QAbstractItemModel::beginRemoveRows(const QModelIndex &parent, int first, int last)
which I call in overrided
bool TreeModel::removeRows(int row, int count, const QModelIndex &parent)
When I compared adresses of child.indernalPointer() (where I store pointer to internal tree Nodes, representing my model) with already deleted Nodes, It became clear, that by some reason beginRemoveRows() using already invalid indices.
There is question with very similar error: QModelIndex becomes invalid when removing rows, howerer I can't understand why and where I use invalid indices.
So, I place the minimal example with this behavior (I've put a lot of effort to minimize it to this size and make the code clear, sorry for it is nevertheless long).
tree.pro
QT += core gui widgets
TARGET = tree
TEMPLATE = app
SOURCES += main.cpp widget.cpp treemodel.cpp
HEADERS += widget.h treemodel.h
treemodel.h
#ifndef TREEMODEL_H
#define TREEMODEL_H
#include <QAbstractItemModel>
class TreeModel : public QAbstractItemModel
{
public:
TreeModel();
~TreeModel();
QModelIndex index(int row, int column, const QModelIndex &parent) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool removeRows(int row, int count, const QModelIndex &parent) override;
private:
class Impl;
Impl* impl = nullptr;
};
#endif // TREEMODEL_H
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = 0);
~Widget();
private slots:
void projectTreeMenuRequested(const QPoint& point);
void eraseItem();
private:
class Impl;
Impl* impl;
};
#endif // WIDGET_H
main.cpp
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
treemodel.cpp
#include "treemodel.h"
#include <cassert>
#include <string>
#include <list>
#include <memory>
namespace {
struct Node {
Node(const std::string& name)
: text(name)
{
}
~Node() {
}
Node& append(const std::string& name) {
child.emplace_back(name);
Node& n = child.back();
n.parent = this;
return n;
}
size_t getChildNum() const {
return child.size();
}
bool hasParent() const {
return parent != nullptr;
}
Node& getParent() {
assert(hasParent());
return *parent;
}
size_t getIndexInParent() const {
if (parent) {
size_t index = 0;
Childs::iterator it = parent->child.begin();
while (it != parent->child.end()) {
if (&*it == this) {
return index;
}
++it;
++index;
}
}
return 0;
}
Node& getChild(size_t i) {
assert(i < child.size());
Childs::iterator it = child.begin();
std::advance(it, i);
return *it;
}
void setText(std::string name) {
this->text = std::move(name);
}
std::string getText() const {
return text;
}
void remove() {
assert(hasParent());
Node& p = getParent();
for (Childs::iterator it = p.child.begin(); it != p.child.end(); ++it) {
if (&*it == this) {
p.child.erase(it);
return;
}
}
assert(0); // Child for remove not found
}
bool removeChilds(size_t start, size_t end) {
if (start < end && end <= child.size()) {
Childs::iterator it1 = child.begin();
assert(it1 != child.end());
std::advance(it1, start);
assert(it1 != child.end());
Childs::iterator it2 = it1;
std::advance(it2, end - start);
child.erase(it1, it2);
return true;
} else {
return false;
}
}
static const int Columns = 1;
private:
using Childs = std::list<Node>;
std::string text;
Node* parent = nullptr;
Childs child;
};
} // namespace
struct TreeModel::Impl {
Impl()
: root("Root")
{
fill(root);
}
void fill(Node& from, std::string str = "", int depth = 0) {
if (depth == 10) return;
for (int j = 0; j != 5; ++j) {
std::string name = str + std::to_string(j);
fill(from.append(name), name, depth+1);
}
}
Node root;
};
TreeModel::TreeModel()
: impl(new Impl)
{
}
TreeModel::~TreeModel()
{
delete impl;
}
QModelIndex
TreeModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent)) {
return QModelIndex();
} else {
Node* node = nullptr;
if (!parent.isValid()) {
node = &impl->root;
} else {
node = static_cast<Node*>(parent.internalPointer());
}
return createIndex(row, column, &node->getChild(row));
}
}
QModelIndex TreeModel::parent(const QModelIndex &child) const
{
if (!child.isValid()) {
return QModelIndex();
}
Node* node = static_cast<Node*>(child.internalPointer());
if (!node->hasParent()) {
return QModelIndex();
}
return createIndex(node->getIndexInParent(),
child.column(),
&node->getParent());
}
int TreeModel::rowCount(const QModelIndex &parent) const
{
Node* p = nullptr;
if (parent.isValid()) {
p = static_cast<Node*>(parent.internalPointer());
} else {
p = &impl->root;
}
return p->getChildNum();
}
int TreeModel::columnCount(const QModelIndex &) const
{
return Node::Columns;
}
QVariant TreeModel::data(const QModelIndex &index, int role) const
{
if (index.isValid()) {
Node* node = static_cast<Node*>(index.internalPointer());
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
return QString::fromUtf8(node->getText().data(),
node->getText().size());
break;
}
}
return QVariant();
}
bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (role != Qt::EditRole)
return false;
Node* node = nullptr;
if (index.isValid()) {
node = static_cast<Node*>(index.internalPointer());
} else {
node = &impl->root;
}
node->setText(value.toString().toStdString());
emit dataChanged(index, index);
return true;
}
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return 0;
return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}
bool TreeModel::removeRows(int row, int count, const QModelIndex &parent)
{
Node* node = nullptr;
QModelIndex correctParent;
if (parent.isValid()) {
node = static_cast<Node*>(parent.internalPointer());
correctParent = parent;
} else {
node = &impl->root;
correctParent = QModelIndex();
}
beginRemoveRows(correctParent, row, row + count - 1); // [row, row + count - 1]
bool success = node->removeChilds(row, row + count); // [row, row + count)
endRemoveRows();
return success;
}
widget.cpp
#include "widget.h"
#include <QVBoxLayout>
#include <QTreeView>
#include <QPoint>
#include <QMenu>
#include "treemodel.h"
struct Widget::Impl {
QVBoxLayout* layout;
QTreeView* treeView;
TreeModel* treeModel;
};
Widget::Widget(QWidget *parent)
: QWidget(parent)
, impl(new Impl)
{
impl->layout = new QVBoxLayout(this);
impl->treeView = new QTreeView;
impl->treeModel = new TreeModel;
impl->layout->addWidget(impl->treeView);
impl->treeView->setModel(impl->treeModel);
impl->treeView->setSelectionMode(QAbstractItemView::ExtendedSelection);
impl->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(impl->treeView, SIGNAL(customContextMenuRequested(const QPoint&)),
this, SLOT(projectTreeMenuRequested(const QPoint&)));
}
Widget::~Widget()
{
delete impl->treeModel;
delete impl;
}
void Widget::projectTreeMenuRequested(const QPoint &point)
{
QPoint globalPos = impl->treeView->mapToGlobal(point);
QMenu myMenu;
myMenu.addAction("Erase", this, SLOT(eraseItem()));
myMenu.exec(globalPos);
}
void Widget::eraseItem()
{
for (QModelIndex index : impl->treeView->selectionModel()->selectedIndexes()) {
impl->treeModel->removeRow(index.row(), index.parent());
}
}
EDIT
I think about two ways to solve the problem. The first is direct approach when somebody point me to the incorrect use of Qt API. The second approach is if somebody write independent implementation of this functionality (tree with infinity nesting and ability to remove) and I will try to figure out what I am doing wrong compared to another implementation.
EDIT 2
After thorough analysis of QStandardItemModel I come to conclusion, that it is important to store in internalPointer of indices parent of actual Node, but in my example I use internalPointer to store Node itself. So it seems correct for Qt internal implementation to call parent() on indices of already deleted elements, assuming that the information in indernalPointer is not related to element and remains correct. (Please, correct me if I am wrong.)
Confirmed, that after rewriting implementation to store pointers to parent node in internal nodes, this bug was eliminated. Correction of other bugs is provided in accepted answer.
You are using for(index: selectedIndexes()) in Widget::eraseItem(), but after removing something, indexes changes, so your index in for becomes invalid. Also, it is a bad practice, to change container while you iterating through it.
In removeChilds() try changing the conditional from end <= child.size() to end < child.size()
I'm new to Qt, so I'm not sure what to use.
I thought I would have a QStringList to give to my QStringListModel and display it in a ListView.
Now, however, I need to divide QStringList in the values of 2 types. So, I need to have string + some typeId, not just one string, but QStringList is for one-dimensional list only.
Anyone could give an advice on what is the best way to try to implement this?
The solution is to use QAbstractListModel subclass as a Qt Quick model. An example of base class for a models (I use it for convenience):
// abstractobjectlistmodel.h
#pragma once
#include <QtCore>
struct AbstractObjectListModel
: public QAbstractListModel
{
explicit AbstractObjectListModel(QObject * const parent = Q_NULLPTR)
: QAbstractListModel{parent}
{ ; }
int rowCount(QModelIndex const & parent = {}) const Q_DECL_OVERRIDE Q_DECL_FINAL
{
Q_UNUSED(parent);
return items.count();
}
QVariant data(QModelIndex const & index, const int role = Qt::DisplayRole) const Q_DECL_OVERRIDE Q_DECL_FINAL
{
if (!index.isValid()) {
return {};
}
switch (role) {
case Qt::UserRole : {
return QVariant::fromValue(items[index.row()].data());
}
default : {
return {};
}
}
}
QHash< int, QByteArray > roleNames() const Q_DECL_OVERRIDE Q_DECL_FINAL
{
auto roleNames = QAbstractListModel::roleNames();
roleNames.insert(Qt::UserRole, "modelData");
return roleNames;
}
Q_INVOKABLE
virtual
QObject * get(int row) const
{
if (row < 0) {
return {};
}
if (row >= rowCount()) {
return {};
}
return items[row];
}
void remove(int row, int count = 1)
{
Q_ASSERT(count > 0);
Q_ASSERT(row >= 0);
Q_ASSERT(row + count <= rowCount());
beginRemoveRows({}, row, row + count - 1);
while (0 < count) {
items.takeAt(row)->deleteLater();
--count;
}
endRemoveRows();
}
void clear()
{
if (!items.isEmpty()) {
remove(0, rowCount());
}
}
protected :
~AbstractObjectListModel() Q_DECL_OVERRIDE Q_DECL_EQ_DEFAULT; // derived classes should not meant to be manipulated polymorphically
QList< QPointer< QObject > > items;
void insert(int row, QObject * const item)
{
item->setParent(this);
beginInsertRows({}, row, row);
items.insert(row, item);
endInsertRows();
}
void append(QObject * const item)
{
insert(rowCount(), item);
}
};
But one need to override get to access items' Q_PROPERTY properties (in addition to dynamic ones):
// type of element
class Project
: public QObject
{
Q_OBJECT
Q_PROPERTY(QString name MEMBER name NOTIFY nameChanged)
Q_PROPERTY(QString path MEMBER path NOTIFY pathChanged)
public :
Project(QString name, QString path,
QObject * const parent = Q_NULLPTR)
: QObject{parent}
, name{name}
, path{path}
{ ; }
Q_SIGNALS :
void nameChanged(QString name);
void pathChanged(QString path);
private :
QString name;
QString path;
};
// custom model
class ProjectsListModel
: public AbstractObjectListModel
{
Q_OBJECT
public :
explicit ProjectsListModel(QObject * const parent = Q_NULLPTR)
: AbstractObjectListModel{parent}
{ ; }
void appendProject(QString name, QString path)
{
AbstractObjectListModel::append(::new Project{name, path});
}
Q_INVOKABLE
Project *
get(int row) const Q_DECL_OVERRIDE
{
return qobject_cast< Project * >(AbstractObjectListModel::get(row));
}
};
Before use one need to register concrete model with qmlRegisterType< ProjectsListModel >();. Properties of Project class are avaliable in delegate and highlight by means of members of modelData.
Another example:
struct TimeZoneModel Q_DECL_FINAL
: public QAbstractListModel
{
Q_OBJECT
public :
explicit TimeZoneModel(QObject * const parent = Q_NULLPTR)
: QAbstractListModel{parent}
{ ; }
int rowCount(QModelIndex const & parent = {}) const Q_DECL_OVERRIDE
{
Q_UNUSED(parent);
return timeZoneIds.count();
}
QVariant data(QModelIndex const & index, const int role = Qt::DisplayRole) const Q_DECL_OVERRIDE
{
if (!index.isValid() || (role > Qt::UserRole + 4)) {
return {};
}
QTimeZone timeZone{timeZoneIds[index.row()]};
if (!timeZone.isValid()) {
return {};
}
return roleData(timeZone, role);
}
QHash< int, QByteArray > roleNames() const Q_DECL_OVERRIDE
{
auto roleNames = QAbstractListModel::roleNames();
int i = Qt::UserRole;
for (const auto role : {"modelData", "id", "comment", "name", "country"}) {
roleNames.insert(i++, role);
}
return roleNames;
}
Q_INVOKABLE
QByteArray get(int row) const
{
if (row < 0) {
return {};
}
if (row >= rowCount()) {
return {};
}
return timeZoneIds[row];
}
private :
QVariant roleData(QTimeZone const & timeZone, int role = Qt::UserRole) const
{
switch (role) {
case Qt::UserRole : {
QVariantMap modelData;
const auto names = roleNames();
while (++role < Qt::UserRole + 5) {
modelData.insert(QString::fromUtf8(names[role]), roleData(timeZone, role));
}
return modelData;
}
case Qt::UserRole + 1: {
return QString::fromUtf8(timeZone.id());
}
case Qt::UserRole + 2 : {
return timeZone.comment();
}
case Qt::UserRole + 3 : {
return timeZone.displayName(QTimeZone::StandardTime);
}
case Qt::UserRole + 4 : {
return QLocale::countryToString(timeZone.country());
}
default : {
return {};
}
}
}
const QByteArrayList timeZoneIds = QTimeZone::availableTimeZoneIds();
};
In addition to access via modelData's fields (modelData.id, modelData.comment etc) all the symbols are accessible directly (i.e. id, comment etc) in delegate and highlight contexts of the ListView.
Model TimeZoneModel is const and can be injected into global scope directly without any performance drawbacks:
QQmlApplicationEngine engine;
TimeZoneModel timeZoneModel;
Q_SET_OBJECT_NAME(timeZoneModel);
qmlRegisterType< TimeZoneModel >();
const auto rootContext = engine.rootContext();
rootContext->setContextProperty(timeZoneModel.objectName(), &timeZoneModel);
If you need dictionary that contains QString and any other type I suggest you to use
QMap<QString, YourType> myMap;
Here you have some example of usage:
QMap<int, QString> myMap;
myMap.insert(1,"A");
myMap.insert(2,"B");
myMap[3] = "C";
foreach(int i, myMap.keys()) qDebug() << myMap[i];
Here is my program:
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <QTreeView>
class MySortFilterProxyModel : public QSortFilterProxyModel
{
public:
MySortFilterProxyModel();
void updateFilter(int filterType);
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const;
bool lessThan(const QModelIndex &left,const QModelIndex &right) const;
private:
int _filterType;
};
MySortFilterProxyModel::MySortFilterProxyModel()
: _filterType(0)
{
}
bool MySortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
QStandardItemModel* source = static_cast<QStandardItemModel*>(sourceModel());
QModelIndex modelIndex = source->index(sourceRow, 0, sourceParent);
QStandardItem* item = source->itemFromIndex(modelIndex);
QVariant v = item->data(Qt::UserRole);
int itemType = v.toInt();
if(itemType == _filterType)
return true;
return false;
}
bool MySortFilterProxyModel::lessThan(const QModelIndex &left,const QModelIndex &right) const
{
QVariant leftData = sourceModel()->data(left);
QVariant rightData = sourceModel()->data(right);
if(leftData.type() == QVariant::String && rightData.type() == QVariant::String)
{
QString leftString = leftData.toString();
QString rightString = rightData.toString();
return QString::localeAwareCompare(leftString, rightString) < 0;
}
return false;
}
void MySortFilterProxyModel::updateFilter(int filterType)
{
_filterType = filterType;
// how can i trigger filteracceptRows here ??
}
int main(int argc, char** argv)
{
QApplication qtApp(argc, argv);
MySortFilterProxyModel mySortFilterProxyModel;
QStandardItemModel standardModel;
QTreeView treeView;
mySortFilterProxyModel.setSourceModel(&standardModel);
treeView.setModel(&standardModel);
treeView.setSortingEnabled(true);
treeView.show();
return qtApp.exec();
}
Everytime i AppendRow to standardModel sort and filter works.
How can i trigger filtering without appending or removing something to standardModel?
I want to filter rows on QTreeView through right click but i couldn't find a way to triggger filterAcceptRows on my void MySortFilterProxyModel::updateFilter(int filterType) function.
Having multiple instances of MySortFilterProxyModel class for every possible filterType value and switching them according to filterType may work but is there a better solution?
calling invalidate() on updateFilter worked for me.
void MySortFilterProxyModel::updateFilter(int filterType)
{
_filterType = filterType;
invalidate();
}