首页 > 解决方案 > QSortFilterProxyModel:从源模型添加行时有时会崩溃

问题描述

我正在编写一个 Qt 应用程序,其中包含一个表格视图,该视图显示不同人执行的任务。每人一栏,每天一排。视图初始化为从今天减去 10 天到今天 +15 天的 26 行。应用程序概述当垂直滚动条处于最小或最大并且用户做出向上/向下移动的操作时,顶部/底部会添加 10 个新行(即天)。问题是有时它会因为一些内部 QPersistentModelIndex 问题而崩溃,但只是因为我在源模型和视图之间添加了一个 QSortFilterProxyModel 以仅显示某人有任务要做的日子。

函数调用传播如下:

我试过了:

有时崩溃发生在多次操作之后(我无法理解导致崩溃的事件的连续性)。可能的操作是:

我在我的 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);
            }
        }
    }
}

标签: qtfilteringinsertionqsortfilterproxymodel

解决方案


推荐阅读