I am working on a window called "Configuration Manager" that allows a user to :
- see all files on a folder with specific extension in a
ListView
component, - filter the displayed items according a text the user enters in a
TextEdit
component, - sort the displayed items by the last modification date,
- select only one item to load the related file,
- select one or multiple to delete the related files,
- enter a name to save data in a new file,
The listview displays filtered/sorted items. When the user click on one item of the listview, I change the color of this item. If he clicks another item, the color of the previous item come back to the default one and the new item has its color change.
The data is stored in the backend in a class inheriting from QAsbtractListModel
and I am using another class inheriting from QSortFilterProxyModel
to show only element filtered and sorted.
The listview can show up to 100 elements, so I add a scrollbar to be able to select any delegate displayed on it.
If i select a first item, the color is updating normally. If I click on another item next to the first one, the change of color of these 2 items (the previous and new one) is okay also. I have a problem if a select an item on top, scroll on the bottom of the listview and select another item. In this way, the previous item is not updated and is still with the selected color. On the backend, with some qDebug, I can confirm the previous item is not updated because the method setData is not called like it is when I select an item (to update color on GUI and a boolean on the backend for this element on the model).
I am not talking here of the multiple selection with CTRL or SHIFT key of the keyboard but the problem is the same. Only some items are updated.
I think I am missing some properties of the listview because it seems not all delegate are in the cache (so not updated when scrolling and select a new item).
I add the code below, I can’t do shorter for now. Thank you in advance for any help.
Main files
- main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include <QQmlContext>
#include <QQmlApplicationEngine>
#include "FilenameListModel.h"
#include "FilterProxyModel.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
FilenameListModel model;
FilterProxyModel filterModel;
filterModel.setSourceModel(&model);
engine.rootContext()->setContextProperty("_FilteredRunwayConfigurationFilesModel", &filterModel);
engine.load(QUrl("qrc:/main.qml"));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
- main.qml
import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.5
import QtQml.Models 2.12
import QtQuick.Layouts 1.12
ApplicationWindow
{
id: root
property bool isSaving: false
minimumWidth: 600
minimumHeight: 400
visible: true
modality: Qt.ApplicationModal
Component.onCompleted:
{
// Update the backend with current saving mode
_FilteredRunwayConfigurationFilesModel.isSavingMode = isSaving
}
DelegateModel
{
id: delegateModel
model: _FilteredRunwayConfigurationFilesModel
delegate: Rectangle
{
id: item_delegate
property bool checked: false
width: ListView.view.width - 10
height: name.implicitHeight
color: model.isChecked? "lightgreen" : "lightblue"
radius: 10
onCheckedChanged:
{
listViewFilter.checkChanged()
}
Connections
{
target: listViewFilter
// Called after the slot onCheckOne of the listViewFilter component
onCheckOne:
{
model.isChecked = (idx === index)
checked = model.isChecked
}
onCheckMul:
{
if (idx > listViewFilter.mulBegin)
{
model.isChecked = (index >= listViewFilter.mulBegin && index <= idx)
checked = model.isChecked
}
else
{
model.isChecked = (index <= listViewFilter.mulBegin && index >= idx)
checked = model.isChecked
}
}
}
Connections
{
target: textFilterString
// Called after this signal has been catched by the textFilterString component
onTextChanged:
{
// Reset all status
model.isChecked = false
checked = model.isChecked
}
}
Rectangle
{
id: leftRectangle
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: rightRectangle.left
}
color: "transparent"
Text
{
id: name
anchors.fill: parent
anchors.margins: 5
verticalAlignment: Qt.AlignVCenter
horizontalAlignment: Qt.AlignHCenter
text: "%1".arg(model.name)
}
}
Rectangle
{
id: rightRectangle
anchors
{
top: parent.top
bottom: parent.bottom
right: parent.right
}
color: "transparent"
width: parent.width * 0.3
Text
{
id: date
anchors.fill: parent
anchors.margins: 5
verticalAlignment: Qt.AlignVCenter
horizontalAlignment: Qt.AlignHCenter
text:
{
var text = "%1".arg(model.date)
return text
}
}
}
MouseArea
{
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked:
{
if (!isSaving)
{
switch(mouse.modifiers)
{
case Qt.ControlModifier:
model.isChecked = !model.isChecked;
checked = model.isChecked
break
case Qt.ShiftModifier:
listViewFilter.checkMul(index)
break
default:
listViewFilter.checkOne(index)
listViewFilter.mulBegin = index
break
}
}
else
{
textFilterString.text = model.name
}
}
}
}
}
ColumnLayout
{
anchors.fill: parent
anchors.margins: 5
TextField
{
id: textFilterString
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
placeholderText: isSaving ? "Write the filename with the '.atols' extension" : "Write the begin of the filename"
onTextChanged:
{
_FilteredRunwayConfigurationFilesModel.filenameBeginning = text;
listViewFilter.checkChanged()
}
}
RowLayout
{
Layout.fillWidth: true
Layout.preferredHeight: textFilterString.height
Layout.alignment: Qt.AlignHCenter
Button
{
id: buttonLoad
Layout.preferredWidth: 100
Layout.preferredHeight: parent.height
text: "LOAD"
enabled: false
visible: !isSaving
onClicked:
{
_FilteredRunwayConfigurationFilesModel.loadConfigurationFile(listViewFilter.selectedIndex)
}
}
Button
{
id: buttonDelete
Layout.preferredWidth: 100
Layout.preferredHeight: parent.height
text: "DELETE"
enabled: false
visible: !isSaving
onClicked:
{
_FilteredRunwayConfigurationFilesModel.deleteConfigurationFile(listViewFilter.selectedIndex)
}
}
Button
{
id: buttonSave
Layout.preferredWidth: 100
Layout.preferredHeight: parent.height
text: "SAVE"
enabled: textFilterString.text
visible: isSaving
onClicked:
{
// TODO
}
}
}
ListView
{
id: listViewFilter
property int mulBegin: 0
property var selectedIndex: []
signal checkOne(int idx)
signal checkMul(int idx)
// Called first when executing from a delegate item
onCheckOne:
{
listViewFilter.mulBegin = idx
}
function checkChanged()
{
listViewFilter.selectedIndex = []
// Check how many items are selected in the list
var nSelected = 0
for (var i = 0; i < listViewFilter.count; i++)
{
if (listViewFilter.contentItem.children[i] && listViewFilter.contentItem.children[i].checked)
{
nSelected++
listViewFilter.selectedIndex.push(i)
}
}
// Enable or disable buttons to load or delete the selected items
var loadEnable = false
var deleteEnable = false
switch(nSelected)
{
case 1: // One item selected => Load and Delete available
loadEnable = true
deleteEnable = true
break
case 0: // No items selected => Nothing available
loadEnable = false
deleteEnable = false
break
default: // Multiple items selected => Only Delete available
loadEnable = false
deleteEnable = true
break
}
buttonLoad.enabled = loadEnable
buttonDelete.enabled = deleteEnable
}
Layout.fillWidth: true
Layout.fillHeight: true
model: delegateModel
ScrollBar.vertical: ScrollBar
{
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
policy: listViewFilter.contentHeight > listViewFilter.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff
active: true
}
}
TextField
{
Layout.fillWidth: true
Layout.preferredHeight: textFilterString.height
text: "No files with this filter"
verticalAlignment: Qt.AlignTop
horizontalAlignment: Qt.AlignHCenter
visible: listViewFilter.count === 0
}
}
}
Base model
- FilenameListModel.h
#ifndef FILENAMELISTMODEL_H
#define FILENAMELISTMODEL_H
#include <QAbstractListModel>
#include <QDate>
class FilenameListModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit FilenameListModel(QObject *parent = nullptr);
public:
enum Roles
{
NameRole = Qt::UserRole,
DateRole,
CheckedRole
};
int rowCount(const QModelIndex& parent) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override;
QHash<int, QByteArray> roleNames() const override;
private:
struct Entry
{
QString name;
QDate date;
bool isChecked;
};
QVector<Entry> m_entries;
};
#endif // FILENAMELISTMODEL_H
- FilenameListModel.cpp
#include "FilenameListModel.h"
#include <QDebug>
FilenameListModel::FilenameListModel(QObject *parent)
: QAbstractListModel (parent)
{
m_entries = {
Entry{"Tata", QDate(2022, 5, 14)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)},
Entry{"Tititata", QDate(2020, 3, 12)},
Entry{"Toto", QDate(2018, 1, 10)},
Entry{"Tata", QDate(2019, 2, 11)},
Entry{"Toto", QDate(2021, 4, 13)},
Entry{"Titi", QDate(2023, 6, 15)}
};
}
int FilenameListModel::rowCount( const QModelIndex& parent) const
{
if (parent.isValid())
return 0;
return m_entries.count();
}
QVariant FilenameListModel::data(const QModelIndex &index, int role) const
{
if ( !index.isValid() )
return QVariant();
const Entry &entry = m_entries.at(index.row());
switch(role)
{
case NameRole:
return entry.name;
case DateRole:
return entry.date.toString("yyyy-MM-dd - hh::mm::ss");
case CheckedRole:
{
return entry.isChecked;
}
default:
return QVariant();
}
}
bool FilenameListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
// Method called when QML change a parameter of the model
if (!hasIndex(index.row(), index.column(), index.parent()) || !value.isValid())
return false;
if (m_entries[index.row()].isChecked != value.toBool())
qDebug() << "backend:" << index.row() << "/" << value.toBool();
m_entries[index.row()].isChecked = value.toBool();
emit dataChanged(index, index, { role });
return true;
}
QHash<int, QByteArray> FilenameListModel::roleNames() const
{
static QHash<int, QByteArray> mapping {
{NameRole, "name"},
{DateRole, "date"},
{CheckedRole, "isChecked"}
};
return mapping;
}
Proxy model
- FilterProxyModel.h
#ifndef FILTERPROXYMODEL_H
#define FILTERPROXYMODEL_H
#include <QSortFilterProxyModel>
class FilterProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
Q_PROPERTY(QString filenameBeginning WRITE setFilenameBeginning READ getFilenameBeginnning NOTIFY filenameBeginningChanged)
Q_PROPERTY(bool isSavingMode WRITE setIsSavingMode NOTIFY isSavingModeChanged)
public:
explicit FilterProxyModel(QObject *parent = nullptr);
void setFilenameBeginning(QString newStr);
QString getFilenameBeginnning();
void setIsSavingMode(bool newMode);
public slots:
Q_INVOKABLE void deleteConfigurationFile(QVector<int> indexToDelete);
Q_INVOKABLE void loadConfigurationFile(QVector<int> indexToLoad);
signals:
void filenameBeginningChanged();
void isSavingModeChanged();
protected:
bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override;
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
QString m_filenameBeginning;
bool m_isSavingMode;
};
#endif // FILTERPROXYMODEL_H
- FilterProxyModel.cpp
#include "FilterProxyModel.h"
#include "FilenameListModel.h"
#include <QDebug>
FilterProxyModel::FilterProxyModel(QObject *parent)
: QSortFilterProxyModel (parent)
{
m_isSavingMode = false;
sort(0, Qt::SortOrder::DescendingOrder);
}
bool FilterProxyModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const
{
const QDate leftDate = sourceLeft.data(FilenameListModel::DateRole).toDate();
const QDate rightDate = sourceRight.data(FilenameListModel::DateRole).toDate();
return leftDate < rightDate;
}
bool FilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
// In saving mode, keep all files on the list
if (m_isSavingMode)
return true;
// Otherwise, filter the rows
const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
const QString name = index.data(FilenameListModel::NameRole).toString();
return strncmp(name.toLower().toStdString().c_str(), m_filenameBeginning.toLower().toStdString().c_str(), m_filenameBeginning.size()) == 0;
}
void FilterProxyModel::setFilenameBeginning(QString newStr)
{
m_filenameBeginning = newStr;
emit filenameBeginningChanged();
invalidateFilter();
}
QString FilterProxyModel::getFilenameBeginnning()
{
return m_filenameBeginning;
}
void FilterProxyModel::deleteConfigurationFile(QVector<int> indexToDelete)
{
for (int i = 0; i < indexToDelete.size(); i++)
{
qDebug() << this->data(this->index(indexToDelete[i], 0), FilenameListModel::Roles::NameRole);
}
}
void FilterProxyModel::loadConfigurationFile(QVector<int> indexToLoad)
{
// The input should be with only one element on it
qDebug() << this->data(this->index(indexToLoad[0], 0), FilenameListModel::Roles::DateRole);
}
void FilterProxyModel::setIsSavingMode(bool newMode)
{
m_isSavingMode = newMode;
}
Thanks to @JarMan advice, I simplify the QML part in the way there is no more algorithmic part on the delegate of the listview. Now the backend update itself the role
CheckedRole
of each item on the model I want. And the scroll is not anymore a problem.I put here the modification of the code:
Main files
main.cpp (No change)
main.qml (Simplify delegate and put logic on the listview)
Base model
FilenameListModel.h (No change)
FilenameListModel.cpp (No change)
Proxy model