qt - QSortFilterProxyModel:从源模型添加行时有时会崩溃
问题描述
我正在编写一个 Qt 应用程序,其中包含一个表格视图,该视图显示不同人执行的任务。每人一栏,每天一排。视图初始化为从今天减去 10 天到今天 +15 天的 26 行。应用程序概述当垂直滚动条处于最小或最大并且用户做出向上/向下移动的操作时,顶部/底部会添加 10 个新行(即天)。问题是有时它会因为一些内部 QPersistentModelIndex 问题而崩溃,但只是因为我在源模型和视图之间添加了一个 QSortFilterProxyModel 以仅显示某人有任务要做的日子。
函数调用传播如下:
- 视图(类型为 DashboardTableView)发出信号“wantToGoMoreUp”或“wantToGoMoreDown”
- -> DashboardWidget::slotAskDataUp 或 slotAskDataDown
- -> DashboardTableModel::addDaysOnTop 或 addDaysOnBottom
我试过了:
- 使用自定义模型而不是 QSortFilterProxyModel(通过重新实现“filterAcceptsRow”)
- 在行添加/删除周围添加“emit layoutAboutToBeChanged()”和“emit layoutChanged()”......但没有成功,它仍然崩溃。
有时崩溃发生在多次操作之后(我无法理解导致崩溃的事件的连续性)。可能的操作是:
- 当然滚动(鼠标滚轮,鼠标点击滚动条按钮或向上/向下翻页键)
- 跳转到给定日期(右键单击+在日历弹出窗口中选择一个日期)
- 使用工具栏按钮启用/禁用过滤
我在我的 GitHub 帐户上做了一个完整的工作示例: https ://github.com/3noix/ForDebug 我目前正在使用 Qt 5.11.0 和 MinGW 5.3.0 32 位。
所以你知道我做错了什么吗?提前谢谢。
//DashboardWidget.h
#include <QWidget>
#include <QVector>
#include "../Task.h"
class QVBoxLayout;
class DashboardTableView;
class DashboardTableModel;
class DashboardFilterModel;
class QSortFilterProxyModel;
class DashboardTableDelegate;
class DashboardWidget : public QWidget
{
Q_OBJECT
public:
explicit DashboardWidget(QWidget *parent = nullptr);
DashboardWidget(const DashboardWidget &other) = delete;
DashboardWidget(DashboardWidget &&other) = delete;
DashboardWidget& operator=(const DashboardWidget &other) = delete;
DashboardWidget& operator=(DashboardWidget &&other) = delete;
virtual ~DashboardWidget() = default;
void toggleDaysFiltering();
void updateTasks(const QVector<Task> &tasks, const QDateTime &refreshDateTime);
public slots:
void slotAskDataUp();
void slotAskDataDown();
void slotShowDate(const QDate &date);
private slots:
void slotNewTimeRange(const QDate &from, const QDate &to);
private:
QVBoxLayout *m_layout;
DashboardTableView *m_view;
DashboardTableModel *m_model;
#ifdef CUSTOM_FILTER_MODEL
DashboardFilterModel *m_filter;
#else
QSortFilterProxyModel *m_filter;
#endif
DashboardTableDelegate *m_delegate;
};
//DashboardWidget.cpp
#include "DashboardWidget.h"
#include "DashboardTableView.h"
#include "DashboardTableModel.h"
#include "DashboardFilterModel.h"
#include <QSortFilterProxyModel>
#include "DashboardTableDelegate.h"
#include "DashboardGeometry.h"
#include "../DataInterface.h"
#include <QVBoxLayout>
///////////////////////////////////////////////////////////////////////////////
// RESUME :
//
// CONSTRUCTEUR
//
// TOGGLE DAYS FILTERING
// SLOT ASK DATA UP
// SLOT ASK DATA DOWN
// SLOT NEW TIME RANGE
// SLOT SHOW DATE
// UPDATE TASKS
///////////////////////////////////////////////////////////////////////////////
// CONSTRUCTEUR ///////////////////////////////////////////////////////////////
DashboardWidget::DashboardWidget(QWidget *parent) : QWidget{parent}
{
m_layout = new QVBoxLayout{this};
this->setLayout(m_layout);
m_view = new DashboardTableView{this};
m_model = new DashboardTableModel{this};
m_delegate = new DashboardTableDelegate{this};
#ifdef CUSTOM_FILTER_MODEL
m_filter = new DashboardFilterModel{this};
#else
m_filter = new QSortFilterProxyModel{this};
m_filter->setFilterKeyColumn(0);
m_filter->setFilterRole(Qt::UserRole+1);
m_filter->setFilterRegExp(QRegExp{});
#endif
m_filter->setSourceModel(m_model);
m_view->setModel(m_filter);
m_view->setItemDelegate(m_delegate);
m_view->setupView();
m_layout->addWidget(m_view);
m_layout->setContentsMargins(0,0,0,0);
QObject::connect(m_view, SIGNAL(wantToGoMoreUp()), this, SLOT(slotAskDataUp()));
QObject::connect(m_view, SIGNAL(wantToGoMoreDown()), this, SLOT(slotAskDataDown()));
QObject::connect(m_view, SIGNAL(dateClicked(QDate)), this, SLOT(slotShowDate(QDate)));
QObject::connect(m_model, SIGNAL(newTimeRange(QDate,QDate)), this, SLOT(slotNewTimeRange(QDate,QDate)));
}
// TOGGLE DAYS FILTERING //////////////////////////////////////////////////////
void DashboardWidget::toggleDaysFiltering()
{
#ifdef CUSTOM_FILTER_MODEL
bool b = m_filter->filtersDays();
m_filter->setFilterOnDays(!b);
#else
if (m_filter->filterRegExp().isEmpty()) {m_filter->setFilterRegExp("Keep");}
else {m_filter->setFilterRegExp(QRegExp{});}
#endif
}
// SLOT ASK DATA UP ///////////////////////////////////////////////////////////
void DashboardWidget::slotAskDataUp()
{
int nbDays = 10;
QDate from = m_model->from();
m_model->addDaysOnTop(nbDays);
//if (m_model->daysVisible() > 100) {m_model->removeDaysOnBottom(nbDays);}
this->slotNewTimeRange(from.addDays(-nbDays),from);
}
// SLOT ASK DATA DOWN /////////////////////////////////////////////////////////
void DashboardWidget::slotAskDataDown()
{
int nbDays = 10;
QDate to = m_model->to();
m_model->addDaysOnBottom(nbDays);
//if (m_model->daysVisible() > 100) {m_model->removeDaysOnTop(nbDays);}
this->slotNewTimeRange(to,to.addDays(nbDays));
}
// SLOT SHOW DATE /////////////////////////////////////////////////////////////
void DashboardWidget::slotShowDate(const QDate &date)
{
int row = m_model->showDate(date);
QModelIndex indexSource = m_model->index(row,0);
QModelIndex indexView = m_filter->mapFromSource(indexSource);
m_view->scrollTo(indexView,QAbstractItemView::PositionAtTop);
}
// SLOT NEW TIME RANGE ////////////////////////////////////////////////////////
void DashboardWidget::slotNewTimeRange(const QDate &from, const QDate &to)
{
// list the tasks in this interval
QString errorMessage;
QVector<Task> tasks = DataInterface::instance().getTasks(from,to,&errorMessage);
if (errorMessage != "") {return;}
// update the table
this->updateTasks(tasks,QDateTime{});
}
// UPDATE TASKS ///////////////////////////////////////////////////////////////
void DashboardWidget::updateTasks(const QVector<Task> &tasks, const QDateTime &refreshDateTime)
{
m_model->updateTasks(tasks);
if (refreshDateTime.isValid()) {m_view->setUpdateTime(refreshDateTime);}
}
//DashboardTableModel.h
#include <QAbstractTableModel>
#include "../User.h"
#include "../Task.h"
#include <QDate>
class DashboardTableModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit DashboardTableModel(QObject *parent = nullptr);
DashboardTableModel(const DashboardTableModel &other) = delete;
DashboardTableModel(DashboardTableModel &&other) = delete;
DashboardTableModel& operator=(const DashboardTableModel &other) = delete;
DashboardTableModel& operator=(DashboardTableModel &&other) = delete;
virtual ~DashboardTableModel() = default;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override final;
Qt::ItemFlags flags(const QModelIndex &index) const override final;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override final;
int columnCount(const QModelIndex &parent = QModelIndex{}) const override final;
int rowCount(const QModelIndex &parent = QModelIndex{}) const override final;
void updateTasks(const QVector<Task> &tasks);
int dateToRow(const QDate &date);
QDate from() const;
QDate to() const;
int daysVisible() const;
int showDate(const QDate &date);
void addDaysOnTop(int nbDays = 20);
void addDaysOnBottom(int nbDays = 20);
bool removeDaysOnTop(int nbDays = 20);
bool removeDaysOnBottom(int nbDays = 20);
void clear();
signals:
void newTimeRange(const QDate &from, const QDate &to);
private:
void initLines(const QDate &from, const QDate &to);
using Location = std::pair<QDate,int>;
struct DataLine
{
QDate date;
QVector<TaskGroup> data;
};
static int searchTask(const TaskGroup &g, int taskId);
int userIdToColumn(int userId) const;
QVector<User> m_users;
QList<DataLine> m_data;
QMap<int,Location> m_index; // key=taskId
};
//DashboardTableModel.cpp
#include "DashboardTableModel.h"
#include <QBrush>
const QStringList daysOfWeek{"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};
const QColor greenToday{200,255,200};
const QColor grayWeekEnd{240,240,240};
const int initNbDaysBefore = -10;
const int initNbDaysAfter = 15;
const bool emitLayoutChange = false;
///////////////////////////////////////////////////////////////////////////////
// RESUME :
//
// CONSTRUCTEUR
// INIT LINES
//
// FROM
// TO
// DAYS VISIBLE
//
// DATE TO ROW
// SEARCH TASK
//
// SHOW DATE
// ADD DAYS ON TOP
// ADD DAYS ON BOTTOM
// REMOVE DAYS ON TOP
// REMOVE DAYS ON BOTTOM
// CLEAR
//
// DATA
// FLAGS
// HEADER DATA
// COLUMN COUNT
// ROW COUNT
//
// UPDATE TASKS
///////////////////////////////////////////////////////////////////////////////
// CONSTRUCTEUR ///////////////////////////////////////////////////////////////
DashboardTableModel::DashboardTableModel(QObject *parent) : QAbstractTableModel{parent}
{
m_users << User{0,"Serge","Karamazov","SKZ"};
m_users << User{1,"Patrick","Biales","PBL"};
m_users << User{2,"Odile","Deray","ODR"};
m_users << User{3,"Mevatlaver","Kraspek","MLK"};
QDate from = QDate::currentDate().addDays(initNbDaysBefore);
QDate to = QDate::currentDate().addDays(initNbDaysAfter);
this->initLines(from,to);
}
// INIT LINES /////////////////////////////////////////////////////////////////
void DashboardTableModel::initLines(const QDate &from, const QDate &to)
{
this->clear();
int nbCols = this->columnCount();
int nbRowsInit = from.daysTo(to) + 1;
QDate date = from;
if (emitLayoutChange) emit layoutAboutToBeChanged();
this->beginInsertRows(QModelIndex{},0,nbRowsInit-1);
for (int row=0; row<=nbRowsInit; ++row)
{
date = date.addDays(1);
m_data << DataLine{date,QVector<TaskGroup>(nbCols)};
}
this->endInsertRows();
if (emitLayoutChange) emit layoutChanged();
}
// FROM ///////////////////////////////////////////////////////////////////////
QDate DashboardTableModel::from() const
{
if (m_data.size() == 0) {return {};}
return m_data[0].date;
}
// TO /////////////////////////////////////////////////////////////////////////
QDate DashboardTableModel::to() const
{
if (m_data.size() == 0) {return {};}
return m_data.last().date;
}
// DAYS VISIBLE ///////////////////////////////////////////////////////////////
int DashboardTableModel::daysVisible() const
{
if (m_data.size() == 0) {return 0;}
QDate from = m_data[0].date;
QDate to = m_data.last().date;
return from.daysTo(to)+1;
}
// DATE TO ROW ////////////////////////////////////////////////////////////////
int DashboardTableModel::dateToRow(const QDate &date)
{
if (m_data.size() == 0) {return -1;}
QDate date1 = m_data[0].date;
QDate date2 = m_data.last().date;
if (date < date1 || date > date2) {return -1;}
return date1.daysTo(date);
}
// USER ID TO COLUMN //////////////////////////////////////////////////////////
int DashboardTableModel::userIdToColumn(int userId) const
{
return userId;
}
// SEARCH TASK ////////////////////////////////////////////////////////////////
int DashboardTableModel::searchTask(const TaskGroup &g, int taskId)
{
for (int i=0; i<g.size(); ++i)
{
if (g[i].id == taskId)
return i;
}
return -1;
}
// SHOW DATE //////////////////////////////////////////////////////////////////
int DashboardTableModel::showDate(const QDate &date)
{
if (m_data.size() == 0) {return -1;}
QDate from = this->from();
QDate to = this->to();
if (from <= date && date <= to) {return this->dateToRow(date);}
int fromToDate = from.daysTo(date);
int toToDate = to.daysTo(date);
if (fromToDate >= -15 && fromToDate < 0)
{
this->addDaysOnTop(15);
emit newTimeRange(date,from);
return this->dateToRow(date);
}
else if (toToDate > 0 && toToDate <= 15)
{
this->addDaysOnBottom(15);
emit newTimeRange(to,date);
return this->dateToRow(date);
}
// we discard the existing data and restart from nothing
this->clear();
QDate newFrom = date.addDays(initNbDaysBefore);
QDate newTo = date.addDays(initNbDaysAfter);
this->initLines(newFrom,newTo);
emit newTimeRange(newFrom,newTo);
return this->dateToRow(date);
}
// ADD DAYS ON TOP ////////////////////////////////////////////////////////////
void DashboardTableModel::addDaysOnTop(int nbDays)
{
if (emitLayoutChange) emit layoutAboutToBeChanged();
this->beginInsertRows(QModelIndex{},0,nbDays-1);
QDate date = this->from();
int nbCol = this->columnCount();
for (int row=0; row<nbDays; ++row)
{
date = date.addDays(-1);
m_data.push_front(DataLine{date,QVector<TaskGroup>(nbCol)});
}
this->endInsertRows();
if (emitLayoutChange) emit layoutChanged();
}
// ADD DAYS ON BOTTOM /////////////////////////////////////////////////////////
void DashboardTableModel::addDaysOnBottom(int nbDays)
{
int nOld = m_data.size();
if (emitLayoutChange) emit layoutAboutToBeChanged();
this->beginInsertRows(QModelIndex{},nOld,nOld+nbDays-1);
QDate date = this->to();
int nbCol = this->columnCount();
for (int row=0; row<nbDays; ++row)
{
date = date.addDays(1);
m_data << DataLine{date,QVector<TaskGroup>(nbCol)};
}
this->endInsertRows();
if (emitLayoutChange) emit layoutChanged();
}
// REMOVE DAYS ON TOP /////////////////////////////////////////////////////////
bool DashboardTableModel::removeDaysOnTop(int nbDays)
{
if (m_data.size() < nbDays) {return false;}
// remove tasks ids from index
for (int row=0; row<nbDays; ++row)
{
for (const TaskGroup &g : m_data[row].data)
{
for (const Task &t : g)
{m_index.remove(t.id);}
}
}
// removal
if (emitLayoutChange) emit layoutAboutToBeChanged();
this->beginRemoveRows(QModelIndex{},0,nbDays-1);
for (int i=0; i<nbDays; ++i) {m_data.removeFirst();}
this->endRemoveRows();
if (emitLayoutChange) emit layoutChanged();
return true;
}
// REMOVE DAYS ON BOTTOM //////////////////////////////////////////////////////
bool DashboardTableModel::removeDaysOnBottom(int nbDays)
{
if (m_data.size() < nbDays) {return false;}
int start = m_data.size() - nbDays;
int end = m_data.size() - 1;
// remove tasks ids from index
for (int row=start; row<end; ++row)
{
for (const TaskGroup &g : m_data[row].data)
{
for (const Task &t : g)
{m_index.remove(t.id);}
}
}
// removal
if (emitLayoutChange) emit layoutAboutToBeChanged();
this->beginRemoveRows(QModelIndex{},start,end);
for (int i=0; i<nbDays; ++i) {m_data.removeLast();}
this->endRemoveRows();
if (emitLayoutChange) emit layoutChanged();
return true;
}
// CLEAR //////////////////////////////////////////////////////////////////////
void DashboardTableModel::clear()
{
if (m_data.size() == 0) {return;}
// this->beginResetModel();
if (emitLayoutChange) emit layoutAboutToBeChanged();
this->beginRemoveRows(QModelIndex{},0,m_data.size()-1);
m_data.clear();
m_index.clear();
this->endRemoveRows();
if (emitLayoutChange) emit layoutChanged();
// this->endResetModel();
}
// DATA ///////////////////////////////////////////////////////////////////////
QVariant DashboardTableModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
int col = index.column();
if (!index.isValid() || row < 0 || row >= this->rowCount(QModelIndex{})) {return {};}
if (role == Qt::DisplayRole || role == Qt::EditRole)
{
const TaskGroup &g = m_data[row].data[col];
return QVariant::fromValue(g);
}
else if (role == Qt::BackgroundRole)
{
QDate date = m_data[row].date;
if (!date.isValid()) {return {};}
if (date == QDate::currentDate()) {return QBrush{greenToday};}
int d = date.dayOfWeek();
if (d == 6 || d == 7) {return QBrush{grayWeekEnd};}
return {};
}
else if (role == Qt::UserRole)
{
int nbPostTasks = 0;
for (const TaskGroup &g : m_data[row].data) {nbPostTasks += g.size();}
return nbPostTasks;
}
else if (role == Qt::UserRole+1)
{
int nbPostTasks = 0;
for (const TaskGroup &g : m_data[row].data) {nbPostTasks += g.size();}
return (nbPostTasks > 0 ? "Keep" : "Skip");
}
return {};
}
// FLAGS //////////////////////////////////////////////////////////////////////
Qt::ItemFlags DashboardTableModel::flags(const QModelIndex &index) const
{
Q_UNUSED(index)
return (Qt::ItemIsEnabled | Qt::ItemIsSelectable);
}
// HEADER DATA ////////////////////////////////////////////////////////////////
QVariant DashboardTableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
{
if (section < 0 || section >= m_users.size()) {return {};}
//return m_users[section].trigramme;
const User &u = m_users[section];
return u.firstName + " " + u.lastName + "\n" + u.trigramme;
}
else if (orientation == Qt::Vertical && role == Qt::DisplayRole)
{
if (section >= m_data.size()) {return {};}
QDate date = m_data[section].date;
if (!date.isValid()) {return {};}
if (date == QDate::currentDate()) {return "Today";}
QString str = daysOfWeek[date.dayOfWeek()-1] + "\n" + date.toString("dd/MM/yy");
if (date.dayOfWeek() == 1) {str += "\nW" + QString::number(date.weekNumber());}
return str;
}
else if (orientation == Qt::Horizontal && role == Qt::TextAlignmentRole)
{
return Qt::AlignCenter;
}
else if (orientation == Qt::Vertical && role == Qt::TextAlignmentRole)
{
return Qt::AlignCenter;
}
else if (orientation == Qt::Vertical && role == Qt::BackgroundRole)
{
if (section >= m_data.size()) {return {};}
QDate date = m_data[section].date;
if (!date.isValid()) {return {};}
if (date == QDate::currentDate()) {return QBrush{greenToday};}
int d = date.dayOfWeek();
if (d == 6 || d == 7) {return QBrush{grayWeekEnd};}
return {};
}
return QAbstractTableModel::headerData(section, orientation, role);
}
// COLUMN COUNT ///////////////////////////////////////////////////////////////
int DashboardTableModel::columnCount(const QModelIndex &parent) const
{
if (parent.isValid()) {return 0;}
return m_users.size();
}
// ROW COUNT //////////////////////////////////////////////////////////////////
int DashboardTableModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {return 0;}
return m_data.size();
}
// UPDATE TASKS ///////////////////////////////////////////////////////////////
void DashboardTableModel::updateTasks(const QVector<Task> &tasks)
{
for (const Task &t : tasks)
{
int newRow = this->dateToRow(t.target);
int newCol = this->userIdToColumn(t.user_id);
bool bDisplayTask = (newRow != -1 && !t.deleted);
if (m_index.contains(t.id)) // the task is already displayed
{
Location loc = m_index[t.id];
int oldRow = this->dateToRow(loc.first);
int oldCol = loc.second;
if (bDisplayTask)
{
if (newRow != oldRow || newCol != oldCol)
{
// we move the post-it (and maybe update it)
TaskGroup &gOld = m_data[oldRow].data[oldCol];
int i = DashboardTableModel::searchTask(gOld,t.id);
if (i != -1) {gOld.removeAt(i);}
m_index.remove(t.id);
m_data[newRow].data[newCol] << t;
m_index.insert(t.id,std::make_pair(t.target,newCol));
QModelIndex oldIndex = this->index(oldRow,oldCol,{});
QModelIndex oldLineIndex = this->index(oldRow,0,{});
QModelIndex newLineIndex = this->index(newRow,0,{});
emit dataChanged(oldIndex,oldIndex);
emit dataChanged(oldLineIndex,oldLineIndex);
emit dataChanged(newLineIndex,newLineIndex);
}
else
{
// we only update the post-it
TaskGroup &g = m_data[newRow].data[newCol];
int i = DashboardTableModel::searchTask(g,t.id);
if (i != -1) {g[i] = t;}
}
QModelIndex newIndex = this->index(newRow,newCol,{});
emit dataChanged(newIndex,newIndex);
}
else
{
// we delete the post-it
TaskGroup &g = m_data[oldRow].data[oldCol];
int i = DashboardTableModel::searchTask(g,t.id);
if (i != -1) {g.removeAt(i);}
m_index.remove(t.id);
QModelIndex index = this->index(oldRow,oldCol,{});
QModelIndex oldLineIndex = this->index(oldRow,0,{});
emit dataChanged(index,index);
emit dataChanged(oldLineIndex,oldLineIndex);
}
}
else // the task is not displayed yet
{
if (bDisplayTask)
{
// we add the post-it
m_data[newRow].data[newCol] << t;
m_index.insert(t.id,std::make_pair(t.target,newCol));
QModelIndex index = this->index(newRow,newCol,{});
QModelIndex newLineIndex = this->index(newRow,0,{});
emit dataChanged(index,index);
emit dataChanged(newLineIndex,newLineIndex);
}
}
}
}
解决方案
推荐阅读
- docker - 要复制到父文件夹中的 dockerfile 构建错误
- python - 主仿真中的 PyFMI 多个输入
- ruby-on-rails - Ruby,如何在多个屏幕上拆分具有多个字段的 simple_form,以便每个屏幕有 1 个字段
- regex - 读取非结构化文本文件后,如何在 Spark Scala 中使用正则表达式将 RDD 转换为 Dataframe?
- python - python 正则表达式的怪异
- django - 为什么在 django 项目中使用 mongoDB
- c# - 获取特定用户的 Azure AD 用户组详细信息
- google-apps-script - 根据列值将工作表 1 的行转置到工作表 2
- django-filter - 向 ModelChoiceFilter 添加 None 选项
- security - 如何从 Java TrustStore (JKS) 文件中列出唯一的主题备用名称