Qt表格入门

摘要

表格作为数据展示的界面,会在很多场景下使用。Qt为我们提供了使用简单方便和扩展性强的表格视图,这里做一个简单的入门整理。

个人能力有限,有错误欢迎留言指正,如果你有更好的方法,也欢迎分享讨论。

关键词

Qt、表格、过滤、筛选、自定义单元格、排序、委托、代理

主要类

QTableWidget、QTableView、QStandardItemModel、QStyledItemDelegate、QSortFilterProxyModel

〇、准备数据

  • Qt 5.14.2
  • 数据类
C++ 复制代码
// 学生类
class Student
{
public:
    Student(const QString &id, const QString &name, int age, int score, int sex);
    ~Student();

    QString mId;   // 学号
    QString mName; // 名字
    int mAge;      // 年龄
    int mScore;    // 分数
    int mSex;      // 性别
};
// 初始化数据
void MainWindow::initStudent()
{
    // QStringList mHeader;
    mHeader << "学号" << "姓名" << "年龄" << "分数" << "性别";
    // QList<Student *> mStudents;
    mStudents << new Student("501", "小明", 20, 85, 0)
              << new Student("402", "小红", 29, 19, 1)
              << new Student("311", "小刚", 25, 79, 1)
              << new Student("813", "小李", 27, 33, 1)
              << new Student("514", "小赵", 23, 21, 0)
              << new Student("425", "小王", 24, 50, 0)
              << new Student("326", "小张", 26, 44, 1)
              << new Student("28", "小淘", 28, 93, 1)
              << new Student("30", "小杨", 21, 77, 1);
}

在这段代码中,

  • 定义学生类,学生类主要包括学生的学号、姓名、年龄、分数、性别;

  • 定义了表格的表头;

  • 同时创建了几个学生,并将学生存储到QList里。

一、显示数据(QTableWidget)

C++ 复制代码
void MainWindow::initTableWidget()
{
    ui->tableWidget->setRowCount(mStudents.size());
    ui->tableWidget->setColumnCount(5);
    ui->tableWidget->setHorizontalHeaderLabels(mHeader);
    ui->tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    for (int i = 0; i < mStudents.size(); ++i) {
        Student *s = mStudents.at(i);
        QTableWidgetItem *item0 = new QTableWidgetItem;
        item0->setData(Qt::DisplayRole, s->mId);
        QTableWidgetItem *item1 = new QTableWidgetItem;
        item1->setData(Qt::DisplayRole, s->mName);
        QTableWidgetItem *item2 = new QTableWidgetItem;
        item2->setData(Qt::DisplayRole, s->mAge);
        QTableWidgetItem *item3 = new QTableWidgetItem;
        item3->setData(Qt::DisplayRole, s->mScore);
        QTableWidgetItem *item4 = new QTableWidgetItem;
        item4->setData(Qt::DisplayRole, s->mSex);

        ui->tableWidget->setItem(i, 0, item0);
        ui->tableWidget->setItem(i, 1, item1);
        ui->tableWidget->setItem(i, 2, item2);
        ui->tableWidget->setItem(i, 3, item3);
        ui->tableWidget->setItem(i, 4, item4);
    }
}

在这段代码中,使用QTableWidget显示数据。

  • 首先设置了行数和列数;

  • 然后设置QTableWidget的水平表头的列名,同时设置为平铺拉伸模式;

  • 再然后遍历数据对表格进行了填充,使用的是QTableWidgetItem,同时使用setData和Qt::DisplayRole可以方便以后对数字列进行排序。

  • 运行效果如下:

二、显示数据(QTableView和QStandardItemModel)

C++ 复制代码
void MainWindow::initTableView()
{
    // QStandardItemModel *mTableViewModel;
    mTableViewModel = new QStandardItemModel(this);
    mTableViewModel->setRowCount(mStudents.size());
    mTableViewModel->setColumnCount(5);
    mTableViewModel->setHorizontalHeaderLabels(mHeader);
    for (int i = 0; i < mStudents.size(); ++i) {
        Student *s = mStudents.at(i);
        QStandardItem *item0 = new QStandardItem;
        item0->setData(s->mId, Qt::DisplayRole);
        QStandardItem *item1 = new QStandardItem;
        item1->setData(s->mName, Qt::DisplayRole);
        QStandardItem *item2 = new QStandardItem;
        item2->setData(s->mAge, Qt::DisplayRole);
        QStandardItem *item3 = new QStandardItem;
        item3->setData(s->mScore, Qt::DisplayRole);
        QStandardItem *item4 = new QStandardItem;
        item4->setData(s->mSex, Qt::DisplayRole);

        mTableViewModel->setItem(i, 0, item0);
        mTableViewModel->setItem(i, 1, item1);
        mTableViewModel->setItem(i, 2, item2);
        mTableViewModel->setItem(i, 3, item3);
        mTableViewModel->setItem(i, 4, item4);
    }
    ui->tableView->setModel(mTableViewModel);
    ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
}

在这段代码中,使用了QStandardItemModel和QTableView来显示数据。通过view和model分离的模式,可以创建更多的高级功能。后面将使用QTableView来进行高级功能的演示。

  • 首先,创建了model,设置行数、列数和表头;

  • 然后,创建单元格,填充model;需要注意的是QTableWidgetItem和QStandardItem的setData函数的两个参数顺序是相反的;

  • 最后,给view设置model,设置表头平铺拉伸显示。

  • 运行效果如下:

三、数据代理(委托)(QStyledItemDelegate)

QStyledItemDelegate可以让表格拥有更高级的显示效果和编辑功能。

1、使用QComboBox代理显示性别

C++ 复制代码
// 代理类
class ComboxDelegate : public QStyledItemDelegate
{
    Q_OBJECT

public:
    ComboxDelegate(QObject *parent = nullptr);
    ~ComboxDelegate();

protected:
    QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
    void setEditorData(QWidget *editor, const QModelIndex &index) const override;
    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};

在这段代码中,继承了QStyledItemDelegate类,来实现一个代理类。需要实现四个函数。

  • createEditor:在点击表格视图进入编辑状态时,该函数会创建一个QWidget,在此函数中实现QComboBox的生成;

  • setEditorData:该函数会对QComboBox设置值;

  • setModelData:在编辑完成后,该函数应该对model中的数据进行修改;

  • updateEditorGeometry:该函数应该设置编辑区域的大小;

C++ 复制代码
ComboxDelegate::ComboxDelegate(QObject *parent)
{

}

ComboxDelegate::~ComboxDelegate()
{

}

QWidget *ComboxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    QComboBox *combox = new QComboBox(parent);
    combox->addItem("女");
    combox->addItem("男");
    return combox;
}

void ComboxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    int sex = index.data(Qt::EditRole).toInt();
    QComboBox *combox = qobject_cast<QComboBox *>(editor);
    combox->setCurrentIndex(sex);
}

void ComboxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    QComboBox *combox = qobject_cast<QComboBox *>(editor);
    int sex = combox->currentIndex();
    model->setData(index, sex, Qt::EditRole);
}

void ComboxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    editor->setGeometry(option.rect);
}

在这段代码中,实现四个函数的具体功能。

  • createEditor:创建了一个QComboBox,并设置了两个选项;

  • setEditorData:从QModelIndex中获取数据,转换QWidget为QComboBox,并给其设置值;

  • setModelData:获取编辑后的值,将编辑后的值设置给model;

  • updateEditorGeometry:设置几何大小;

C++ 复制代码
void MainWindow::initSource()
{
    mSourceModel = new QStandardItemModel(this);
    mSourceModel->setRowCount(mStudents.size());
    mSourceModel->setColumnCount(5);
    mSourceModel->setHorizontalHeaderLabels(mHeader);
    for (int i = 0; i < mStudents.size(); ++i) {
        Student *s = mStudents.at(i);
        QStandardItem *item0 = new QStandardItem;
        item0->setData(s->mId, Qt::DisplayRole);
        QStandardItem *item1 = new QStandardItem;
        item1->setData(s->mName, Qt::DisplayRole);
        QStandardItem *item2 = new QStandardItem;
        item2->setData(s->mAge, Qt::DisplayRole);
        QStandardItem *item3 = new QStandardItem;
        item3->setData(s->mScore, Qt::DisplayRole);
        QStandardItem *item4 = new QStandardItem;
        item4->setData(s->mSex, Qt::DisplayRole);

        mSourceModel->setItem(i, 0, item0);
        mSourceModel->setItem(i, 1, item1);
        mSourceModel->setItem(i, 2, item2);
        mSourceModel->setItem(i, 3, item3);
        mSourceModel->setItem(i, 4, item4);
    }
    ui->tableView_Source->setModel(mSourceModel);
    ui->tableView_Source->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

    ComboxDelegate *d = new ComboxDelegate(this);
    ui->tableView_Source->setItemDelegateForColumn(4, d);
}

在这段代码中,

  • 首先,给model填充数据;

  • 然后,创建了ComboxDelegate代理对象;

  • 最后,给QTableView的性别列设置了代理对象;

  • 运行效果如下:

    更多的例子可以参考Qt自带的例子,在「示例」中搜索「Color Editor Factory Example」、「Spin Box Delegate Example」、「Star Delegate Example」


2、使用自定义的窗口代理显示性别

除了自带的控件,如果想将自定义的控件或者窗口插入到表格的单元格中应该怎么做呢?

与刚才的例子类似,只不过把QComboBox换成自定义的窗口类就可以了。

C++ 复制代码
class RadioWidget : public QWidget
{
    Q_OBJECT
public:
    explicit RadioWidget(const QModelIndex &index, QWidget *parent = nullptr);
    ~RadioWidget();

    void setSex(int sex);
    int getSex() const;

    QStandardItem *getItem() const;
    void setItem(QStandardItem *item);

signals:
    void sexChangedByQModelIndex(int row, int sex);
    void sexChangedByQStandardItem(int row, int sex);


private:
    QRadioButton *mRadioMale;
    QRadioButton *mRadioFemale;

    int mSex;

    QStandardItem *mItem = nullptr;
    QModelIndex mIndex;

    void changeState(QAbstractButton *button, bool checked);
};

在这段代码中,定义了一个自定义的窗口:

  • 它包含两个QRadioButton和性别;

  • 对应单元格的QStandardItem和QModelIndex;

  • 当性别变化时发出的sexChangedByQModelIndex和sexChangedByQStandardItem信号,发送的参数是单元格所在行号和当前性别;

需要注意的是,为了定位单元格,使用了QStandardItem和QModelIndex---------这个会在后面进行演示,添加这两变量是为了:

  • 1、由于代理的存在,自定义窗口和模型单元格之间隔了一层,需要将定位信息(主要是为了行号)存在自定义窗口中;

  • 2、比较QStandardItem和QModelIndex,结论就是应该使用QStandardItem;

C++ 复制代码
RadioWidget::RadioWidget(const QModelIndex &index, QWidget *parent) :
    QWidget(parent)
{
    mIndex = index;

    QHBoxLayout *layout = new QHBoxLayout();
    mRadioMale = new QRadioButton(this);
    mRadioMale->setText("男");
    mRadioFemale = new QRadioButton(this);
    mRadioFemale->setText("女");

    layout->addWidget(mRadioMale, Qt::AlignCenter);
    layout->addWidget(mRadioFemale, Qt::AlignCenter);

    layout->setMargin(0);
    this->setLayout(layout);

    mSex = 1;
    mRadioMale->setChecked(true);
    mRadioFemale->setChecked(false);

    QButtonGroup *button_group = new QButtonGroup(this);
    button_group->addButton(mRadioMale);
    button_group->addButton(mRadioFemale);

    connect(button_group, QOverload<QAbstractButton *, bool>::of(&QButtonGroup::buttonToggled),
            this, &RadioWidget::changeState);
}

RadioWidget::~RadioWidget()
{

}

void RadioWidget::setSex(int sex)
{
    mSex = sex;
    if (mSex == 1) {
        mRadioMale->setChecked(true);
        mRadioFemale->setChecked(false);
    } else {
        mRadioMale->setChecked(false);
        mRadioFemale->setChecked(true);
    }
}

QStandardItem *RadioWidget::getItem() const
{
    return mItem;
}

void RadioWidget::setItem(QStandardItem *item)
{
    mItem = item;
}

int RadioWidget::getSex() const
{
    return mSex;
}

void RadioWidget::changeState(QAbstractButton *button, bool checked)
{
    if (checked == false) {
        return ;
    }

    if (button == mRadioMale) {
        mSex = 1;
    } else if (button == mRadioFemale) {
        mSex = 0;
    }
    emit sexChangedByQModelIndex(mIndex.row(), mSex);
    if (mItem) {
        emit sexChangedByQStandardItem(mItem->row(), mSex);
    }
}

在这段代码中,具体实现了各个函数的功能:

  • 在构造函数中,创建了两个性别的QRadioButton和按钮组,并创建了窗口的布局;

  • 在setSex函数中,设置了对应的按钮状态;

  • 在changeState函数中,当性别变化时,发出对应的两个信号;

C++ 复制代码
class RadioDelegate : public QStyledItemDelegate
{
   Q_OBJECT
public:
   RadioDelegate(QObject *parent = nullptr);
   ~RadioDelegate();

   void setSourceModel(QStandardItemModel *model);

protected:
   QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
   void setEditorData(QWidget *editor, const QModelIndex &index) const override;
   void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
   void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;

private:
   QStandardItemModel *mSourceModel = nullptr;
};

RadioDelegate::RadioDelegate(QObject *parent) : QStyledItemDelegate(parent)
{

}

RadioDelegate::~RadioDelegate()
{

}

QWidget *RadioDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
   RadioWidget *rw = new RadioWidget(index, parent);

   QStandardItem *item = mSourceModel->itemFromIndex(index);
   rw->setItem(item);
   rw->setSex(item->data(Qt::DisplayRole).toInt());

   return rw;
}

void RadioDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
   int sex = index.model()->data(index).toInt();
   RadioWidget *rw = static_cast<RadioWidget *>(editor);
   rw->setSex(sex);
}

void RadioDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
   editor->setGeometry(option.rect);
}

void RadioDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
   RadioWidget *rw = static_cast<RadioWidget *>(editor);
   int sex = rw->getSex();
   model->setData(index, sex);
}

void RadioDelegate::setSourceModel(QStandardItemModel *model)
{
   mSourceModel = model;
}

在这段代码中,实现了自定义窗口对应的代理,和上面的代理类差不多,需要注意的是:

  • 1、要将上面代码中的QComboBox替换成自定义窗口RadioWidget;

  • 2、在createEditor函数中,设置了RadioWidget的QModelIndex和QStandardItem,以及性别;

  • 3、在调用时使用代码:

C++ 复制代码
    RadioDelegate *d = new RadioDelegate(this);
    d->setSourceModel(mSourceModel);
    ui->tableView_Source->setItemDelegateForColumn(4, d);
  • 4、运行效果如下:

3、如何让代理始终显示呢

从上图的效果可以看出,需要双击才可以显示出单元格的自定义窗口,但是需要始终显示的时候应该怎么做呢?

只需要调用void QAbstractItemView::openPersistentEditor(const QModelIndex &index)函数就可以了。

修改后的代码如下:

C++ 复制代码
void MainWindow::initSource()
{
    mSourceModel = new QStandardItemModel(this);
    mSourceModel->setRowCount(mStudents.size());
    mSourceModel->setColumnCount(5);
    mSourceModel->setHorizontalHeaderLabels(mHeader);
    for (int i = 0; i < mStudents.size(); ++i) {
        Student *s = mStudents.at(i);
        QStandardItem *item0 = new QStandardItem;
        item0->setData(s->mId, Qt::DisplayRole);
        QStandardItem *item1 = new QStandardItem;
        item1->setData(s->mName, Qt::DisplayRole);
        QStandardItem *item2 = new QStandardItem;
        item2->setData(s->mAge, Qt::DisplayRole);
        QStandardItem *item3 = new QStandardItem;
        item3->setData(s->mScore, Qt::DisplayRole);
        QStandardItem *item4 = new QStandardItem;
        item4->setData(s->mSex, Qt::DisplayRole);

        mSourceModel->setItem(i, 0, item0);
        mSourceModel->setItem(i, 1, item1);
        mSourceModel->setItem(i, 2, item2);
        mSourceModel->setItem(i, 3, item3);
        mSourceModel->setItem(i, 4, item4);
    }
    ui->tableView_Source->setModel(mSourceModel);
    ui->tableView_Source->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

//    ComboxDelegate *d = new ComboxDelegate(this);
//    ui->tableView_Source->setItemDelegateForColumn(4, d);
    RadioDelegate *d = new RadioDelegate(this);
    d->setSourceModel(mSourceModel);
    ui->tableView_Source->setItemDelegateForColumn(4, d);
    // 让代理自定义窗口始终显示
    for (int i = 0, size = mSourceModel->rowCount(); i < size; ++i) {
        ui->tableView_Source->openPersistentEditor(mSourceModel->index(i, 4));
    }
}

运行效果如下:

四、筛选过滤、排序(QSortFilterProxyModel)

QSortFilterProxyModel可以让表格实现筛选过滤和排序的功能。

1、实现筛选过滤

在这个例子中实现了名字和性别的筛选过滤。

C++ 复制代码
class SortFilterProxyModel : public QSortFilterProxyModel
{
public:
    SortFilterProxyModel(QObject *parent = nullptr);

    void setSex(int sex);

protected:
    bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;

private:
    int mSex = -1;
};

在这段代码中,定义了一个自定义的排序筛选代理模型:

  • 新增了性别变量,当设置性别setSex时,需要调用invalidateFilter()重新筛选;

  • 在filterAcceptsRow函数中,获取到源数据,返回比较的结果布尔值;

C++ 复制代码
void MainWindow::initProxy()
{
    mProxyModel = new SortFilterProxyModel(this);
    mProxyModel->setSourceModel(mSourceModel);
    ui->tableView_Proxy->setModel(mProxyModel);
    ui->tableView_Proxy->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

    // 过滤筛选
    connect(ui->lineEdit, &QLineEdit::textChanged, this, [=](const QString &text) {
        mProxyModel->setFilterRegExp(text);
    });

    ui->comboBox->addItem("全部", -1);
    ui->comboBox->addItem("女", 0);
    ui->comboBox->addItem("男", 1);
    connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [=](int index) {
        int sex = ui->comboBox->itemData(index).toInt();
        mProxyModel->setSex(sex);
    });
}

在这段代码中,

  • 创建了SortFilterProxyModel对象,设置了一些基础属性:设置源模型,显示到view上,设置表头;

  • 通过检测lineEdit文本变化,过滤名字;

  • 通过检测comboBox选项变化,过滤性别;

  • 运行效果如下:

2、实现排序

在上面基础上添加以下代码:

C++ 复制代码
    // 排序
    connect(ui->tableView_Proxy->horizontalHeader(), &QHeaderView::sortIndicatorChanged,
            this, [=](int logicalIndex, Qt::SortOrder order)
    {
        ui->tableView_Proxy->model()->sort(logicalIndex, order);
    });

对于排序的实现,可以自定义void QSortFilterProxyModel::sort(int column, Qt::SortOrder order = Qt::AscendingOrder)函数实现,这里不做过多演示。

运行效果如下:

3、显示代理

需要对上面的RadioDelegate做一些修改,添加以下内容:

C++ 复制代码
void setProxyModel(QSortFilterProxyModel *sortModel);
QSortFilterProxyModel *mProxyModel = nullptr;


void RadioDelegate::setProxyModel(QSortFilterProxyModel *sortModel)
{
    mProxyModel = sortModel;
    QAbstractItemModel *source_model = mProxyModel->sourceModel();
    mSourceModel = static_cast<QStandardItemModel *>(source_model);
}

QWidget *RadioDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    RadioWidget *rw = new RadioWidget(index, parent);

//    QStandardItem *item = mSourceModel->itemFromIndex(index);
//    rw->setItem(item);
//    rw->setSex(item->data(Qt::DisplayRole).toInt());

    if (mProxyModel) {
        QStandardItem *item = mSourceModel->itemFromIndex(mProxyModel->mapToSource(index));
        rw->setItem(item);
        rw->setSex(item->data(Qt::DisplayRole).toInt());
    } else {
        QStandardItem *item = mSourceModel->itemFromIndex(index);
        rw->setItem(item);
        rw->setSex(item->data(Qt::DisplayRole).toInt());
    }


    connect(rw, &RadioWidget::sexChangedByQModelIndex, this, [=](int row, int sex) {
        if (mSourceModel) {
//            qDebug() << "sexChangedByQModelIndex" << row << mSourceModel->item(row, 0)->text();
            mSourceModel->item(row, 4)->setData(sex, Qt::DisplayRole);
        }
    });
    connect(rw, &RadioWidget::sexChangedByQStandardItem, this, [=](int row, int sex) {
        if (mSourceModel) {
//            qDebug() << "sexChangedByQStandardItem" << row << mSourceModel->item(row, 0)->text();
            mSourceModel->item(row, 4)->setData(sex, Qt::DisplayRole);
        }
    });

    return rw;
}

在这段代码中,

  • 添加了QSortFilterProxyModel成员变量;

  • 为了方便(偷懒),在createEditor函数中判断后,再setItem;

  • 连接了两个信号;

C++ 复制代码
void MainWindow::initProxy()
{
    mProxyModel = new SortFilterProxyModel(this);
    mProxyModel->setSourceModel(mSourceModel);
    ui->tableView_Proxy->setModel(mProxyModel);
    ui->tableView_Proxy->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

    // 代理
    RadioDelegate *rd = new RadioDelegate(ui->tableView_Proxy);
    rd->setProxyModel(mProxyModel);
    ui->tableView_Proxy->setItemDelegateForColumn(4, rd);


    for (int i = 0, size = mProxyModel->rowCount(); i < size; ++i) {
        ui->tableView_Proxy->openPersistentEditor(mProxyModel->index(i, 4));
    }

    // 过滤筛选
    connect(ui->lineEdit, &QLineEdit::textChanged, this, [=](const QString &text) {
        mProxyModel->setFilterRegExp(text);
        for (int i = 0, size = mProxyModel->rowCount(); i < size; ++i) {
            ui->tableView_Proxy->openPersistentEditor(mProxyModel->index(i, 4));
        }
    });

    ui->comboBox->addItem("全部", -1);
    ui->comboBox->addItem("女", 0);
    ui->comboBox->addItem("男", 1);
    connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [=](int index) {
        int sex = ui->comboBox->itemData(index).toInt();
        mProxyModel->setSex(sex);
        for (int i = 0, size = mProxyModel->rowCount(); i < size; ++i) {
            ui->tableView_Proxy->openPersistentEditor(mProxyModel->index(i, 4));
        }
    });

    // 排序
    connect(ui->tableView_Proxy->horizontalHeader(), &QHeaderView::sortIndicatorChanged,
            this, [=](int logicalIndex, Qt::SortOrder order)
    {
        ui->tableView_Proxy->model()->sort(logicalIndex, order);
        for (int i = 0, size = mProxyModel->rowCount(); i < size; ++i) {
            ui->tableView_Proxy->openPersistentEditor(mProxyModel->index(i, 4));
        }
    });
}

在这段代码中,添加了以下内容:

  • 创建代理,给表格的性别列设置代理,让代理始终显示;

  • 在过滤筛选的两个槽函数中,重新设置代理始终显示;

  • 在排序的槽函数中,重新设置代理始终显示;

4、运行效果

对于RadioDelegate::createEditor中的连接的两个信号,可以注释掉其中一个运行一下效果:

使用sexChangedByQModelIndex:

可以看到在多次的筛选排序后,出现了问题。

sexChangedByQStandardItem:

可以看到在多次筛选排序后,都没有出现问题。

结论:使用QStandardItem的row()函数。

附录一:参考文献

附录二:完整代码

  • mainwindow.h

点击折叠或展开代码

C++ 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QtCore>
#include <QtWidgets>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

// 学生类
class Student
{
public:
   Student(const QString &id, const QString &name, int age, int score, int sex);
   ~Student();

   QString mId;   // 学号
   QString mName; // 名字
   int mAge;      // 年龄
   int mScore;    // 分数
   int mSex;      // 性别
};
// 代理类
class ComboxDelegate : public QStyledItemDelegate
{
   Q_OBJECT

public:
   ComboxDelegate(QObject *parent = nullptr);
   ~ComboxDelegate();

protected:
   QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
   void setEditorData(QWidget *editor, const QModelIndex &index) const override;
   void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
   void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};

// 自定义窗口
class RadioWidget : public QWidget
{
   Q_OBJECT
public:
   explicit RadioWidget(const QModelIndex &index, QWidget *parent = nullptr);
   ~RadioWidget();

   void setSex(int sex);
   int getSex() const;

   QStandardItem *getItem() const;
   void setItem(QStandardItem *item);

signals:
   void sexChangedByQModelIndex(int row, int sex);
   void sexChangedByQStandardItem(int row, int sex);


private:
   QRadioButton *mRadioMale;
   QRadioButton *mRadioFemale;

   int mSex;

   QStandardItem *mItem = nullptr;
   QModelIndex mIndex;

   void changeState(QAbstractButton *button, bool checked);
};
// 自定义窗口对应的代理类
class RadioDelegate : public QStyledItemDelegate
{
   Q_OBJECT
public:
   RadioDelegate(QObject *parent = nullptr);
   ~RadioDelegate();

   void setSourceModel(QStandardItemModel *model);

   void setProxyModel(QSortFilterProxyModel *sortModel);

protected:
   QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
   void setEditorData(QWidget *editor, const QModelIndex &index) const override;
   void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
   void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;

private:
   QStandardItemModel *mSourceModel = nullptr;
   QSortFilterProxyModel *mProxyModel = nullptr;
};

class SortFilterProxyModel : public QSortFilterProxyModel
{
public:
   SortFilterProxyModel(QObject *parent = nullptr);

   void setSex(int sex);

protected:
   bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;

private:
   int mSex = -1;
};

class MainWindow : public QMainWindow
{
   Q_OBJECT

public:
   MainWindow(QWidget *parent = nullptr);
   ~MainWindow();

private:
   Ui::MainWindow *ui;

   void initStudent();
   void initTableWidget();
   void initTableView();
   void initSource();
   void initProxy();

   QStringList mHeader;
   QList<Student *> mStudents;
   QStandardItemModel *mTableViewModel;

   QStandardItemModel *mSourceModel;
   SortFilterProxyModel *mProxyModel;
};
#endif // MAINWINDOW_H
  • mainwindow.cpp

点击折叠或展开代码

C++ 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
   : QMainWindow(parent)
   , ui(new Ui::MainWindow)
{
   ui->setupUi(this);

   initStudent();

   initTableWidget();
   initTableView();
   initSource();
   initProxy();
}

MainWindow::~MainWindow()
{
   delete ui;
}
// 初始化数据
void MainWindow::initStudent()
{
   // QStringList mHeader;
   mHeader << "学号" << "姓名" << "年龄" << "分数" << "性别";
   // QList<Student *> mStudents;
   mStudents << new Student("501", "小明", 20, 85, 0)
             << new Student("402", "小红", 29, 19, 1)
             << new Student("311", "小刚", 25, 79, 1)
             << new Student("813", "小李", 27, 33, 1)
             << new Student("514", "小赵", 23, 21, 0)
             << new Student("425", "小王", 24, 50, 0)
             << new Student("326", "小张", 26, 44, 1)
             << new Student("28", "小淘", 28, 93, 1)
             << new Student("30", "小杨", 21, 77, 1);
}

void MainWindow::initTableWidget()
{
   ui->tableWidget->setRowCount(mStudents.size());
   ui->tableWidget->setColumnCount(5);
   ui->tableWidget->setHorizontalHeaderLabels(mHeader);
   ui->tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
   for (int i = 0; i < mStudents.size(); ++i) {
       Student *s = mStudents.at(i);
       QTableWidgetItem *item0 = new QTableWidgetItem;
       item0->setData(Qt::DisplayRole, s->mId);
       QTableWidgetItem *item1 = new QTableWidgetItem;
       item1->setData(Qt::DisplayRole, s->mName);
       QTableWidgetItem *item2 = new QTableWidgetItem;
       item2->setData(Qt::DisplayRole, s->mAge);
       QTableWidgetItem *item3 = new QTableWidgetItem;
       item3->setData(Qt::DisplayRole, s->mScore);
       QTableWidgetItem *item4 = new QTableWidgetItem;
       item4->setData(Qt::DisplayRole, s->mSex);

       ui->tableWidget->setItem(i, 0, item0);
       ui->tableWidget->setItem(i, 1, item1);
       ui->tableWidget->setItem(i, 2, item2);
       ui->tableWidget->setItem(i, 3, item3);
       ui->tableWidget->setItem(i, 4, item4);
   }
}

void MainWindow::initTableView()
{
   // QStandardItemModel *mTableViewModel;
   mTableViewModel = new QStandardItemModel(this);
   mTableViewModel->setRowCount(mStudents.size());
   mTableViewModel->setColumnCount(5);
   mTableViewModel->setHorizontalHeaderLabels(mHeader);
   for (int i = 0; i < mStudents.size(); ++i) {
       Student *s = mStudents.at(i);
       QStandardItem *item0 = new QStandardItem;
       item0->setData(s->mId, Qt::DisplayRole);
       QStandardItem *item1 = new QStandardItem;
       item1->setData(s->mName, Qt::DisplayRole);
       QStandardItem *item2 = new QStandardItem;
       item2->setData(s->mAge, Qt::DisplayRole);
       QStandardItem *item3 = new QStandardItem;
       item3->setData(s->mScore, Qt::DisplayRole);
       QStandardItem *item4 = new QStandardItem;
       item4->setData(s->mSex, Qt::DisplayRole);

       mTableViewModel->setItem(i, 0, item0);
       mTableViewModel->setItem(i, 1, item1);
       mTableViewModel->setItem(i, 2, item2);
       mTableViewModel->setItem(i, 3, item3);
       mTableViewModel->setItem(i, 4, item4);
   }
   ui->tableView->setModel(mTableViewModel);
   ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
}

void MainWindow::initSource()
{
   mSourceModel = new QStandardItemModel(this);
   mSourceModel->setRowCount(mStudents.size());
   mSourceModel->setColumnCount(5);
   mSourceModel->setHorizontalHeaderLabels(mHeader);
   for (int i = 0; i < mStudents.size(); ++i) {
       Student *s = mStudents.at(i);
       QStandardItem *item0 = new QStandardItem;
       item0->setData(s->mId, Qt::DisplayRole);
       QStandardItem *item1 = new QStandardItem;
       item1->setData(s->mName, Qt::DisplayRole);
       QStandardItem *item2 = new QStandardItem;
       item2->setData(s->mAge, Qt::DisplayRole);
       QStandardItem *item3 = new QStandardItem;
       item3->setData(s->mScore, Qt::DisplayRole);
       QStandardItem *item4 = new QStandardItem;
       item4->setData(s->mSex, Qt::DisplayRole);

       mSourceModel->setItem(i, 0, item0);
       mSourceModel->setItem(i, 1, item1);
       mSourceModel->setItem(i, 2, item2);
       mSourceModel->setItem(i, 3, item3);
       mSourceModel->setItem(i, 4, item4);
   }
   ui->tableView_Source->setModel(mSourceModel);
   ui->tableView_Source->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

//    ComboxDelegate *d = new ComboxDelegate(this);
//    ui->tableView_Source->setItemDelegateForColumn(4, d);
   RadioDelegate *d = new RadioDelegate(this);
   d->setSourceModel(mSourceModel);
   ui->tableView_Source->setItemDelegateForColumn(4, d);
   for (int i = 0, size = mSourceModel->rowCount(); i < size; ++i) {
       ui->tableView_Source->openPersistentEditor(mSourceModel->index(i, 4));
   }
}

void MainWindow::initProxy()
{
   mProxyModel = new SortFilterProxyModel(this);
   mProxyModel->setSourceModel(mSourceModel);
   ui->tableView_Proxy->setModel(mProxyModel);
   ui->tableView_Proxy->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

   // 代理
   RadioDelegate *rd = new RadioDelegate(ui->tableView_Proxy);
   rd->setProxyModel(mProxyModel);
   ui->tableView_Proxy->setItemDelegateForColumn(4, rd);


   for (int i = 0, size = mProxyModel->rowCount(); i < size; ++i) {
       ui->tableView_Proxy->openPersistentEditor(mProxyModel->index(i, 4));
   }

   // 过滤筛选
   connect(ui->lineEdit, &QLineEdit::textChanged, this, [=](const QString &text) {
       mProxyModel->setFilterRegExp(text);
       for (int i = 0, size = mProxyModel->rowCount(); i < size; ++i) {
           ui->tableView_Proxy->openPersistentEditor(mProxyModel->index(i, 4));
       }
   });

   ui->comboBox->addItem("全部", -1);
   ui->comboBox->addItem("女", 0);
   ui->comboBox->addItem("男", 1);
   connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [=](int index) {
       int sex = ui->comboBox->itemData(index).toInt();
       mProxyModel->setSex(sex);
       for (int i = 0, size = mProxyModel->rowCount(); i < size; ++i) {
           ui->tableView_Proxy->openPersistentEditor(mProxyModel->index(i, 4));
       }
   });

   // 排序
   connect(ui->tableView_Proxy->horizontalHeader(), &QHeaderView::sortIndicatorChanged,
           this, [=](int logicalIndex, Qt::SortOrder order)
   {
       ui->tableView_Proxy->model()->sort(logicalIndex, order);
       for (int i = 0, size = mProxyModel->rowCount(); i < size; ++i) {
           ui->tableView_Proxy->openPersistentEditor(mProxyModel->index(i, 4));
       }
   });
}


Student::Student(const QString &id, const QString &name, int age, int score, int sex)
{
   mId = id;
   mName = name;
   mAge = age;
   mScore = score;
   mSex = sex;
}

Student::~Student()
{

}

ComboxDelegate::ComboxDelegate(QObject *parent) :
   QStyledItemDelegate(parent)
{

}

ComboxDelegate::~ComboxDelegate()
{

}

QWidget *ComboxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
   QComboBox *combox = new QComboBox(parent);
   combox->addItem("女");
   combox->addItem("男");
   return combox;
}

void ComboxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
   int sex = index.data(Qt::EditRole).toInt();
   QComboBox *combox = qobject_cast<QComboBox *>(editor);
   combox->setCurrentIndex(sex);
}

void ComboxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
   QComboBox *combox = qobject_cast<QComboBox *>(editor);
   int sex = combox->currentIndex();
   model->setData(index, sex, Qt::EditRole);
}

void ComboxDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
   editor->setGeometry(option.rect);
}

RadioWidget::RadioWidget(const QModelIndex &index, QWidget *parent) :
   QWidget(parent)
{
   mIndex = index;

   QHBoxLayout *layout = new QHBoxLayout();
   mRadioMale = new QRadioButton(this);
   mRadioMale->setText("男");
   mRadioFemale = new QRadioButton(this);
   mRadioFemale->setText("女");

   layout->addWidget(mRadioMale, Qt::AlignCenter);
   layout->addWidget(mRadioFemale, Qt::AlignCenter);

   layout->setMargin(0);
   this->setLayout(layout);

   mSex = 1;
   mRadioMale->setChecked(true);
   mRadioFemale->setChecked(false);

   QButtonGroup *button_group = new QButtonGroup(this);
   button_group->addButton(mRadioMale);
   button_group->addButton(mRadioFemale);

   connect(button_group, QOverload<QAbstractButton *, bool>::of(&QButtonGroup::buttonToggled),
           this, &RadioWidget::changeState);
}

RadioWidget::~RadioWidget()
{

}

void RadioWidget::setSex(int sex)
{
   mSex = sex;
   if (mSex == 1) {
       mRadioMale->setChecked(true);
       mRadioFemale->setChecked(false);
   } else {
       mRadioMale->setChecked(false);
       mRadioFemale->setChecked(true);
   }
}

QStandardItem *RadioWidget::getItem() const
{
   return mItem;
}

void RadioWidget::setItem(QStandardItem *item)
{
   mItem = item;
}

int RadioWidget::getSex() const
{
   return mSex;
}

void RadioWidget::changeState(QAbstractButton *button, bool checked)
{
   if (checked == false) {
       return ;
   }

   if (button == mRadioMale) {
       mSex = 1;
   } else if (button == mRadioFemale) {
       mSex = 0;
   }
   emit sexChangedByQModelIndex(mIndex.row(), mSex);
   if (mItem) {
       emit sexChangedByQStandardItem(mItem->row(), mSex);
   }
}

RadioDelegate::RadioDelegate(QObject *parent) : QStyledItemDelegate(parent)
{

}

RadioDelegate::~RadioDelegate()
{

}

QWidget *RadioDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
   RadioWidget *rw = new RadioWidget(index, parent);

//    QStandardItem *item = mSourceModel->itemFromIndex(index);
//    rw->setItem(item);
//    rw->setSex(item->data(Qt::DisplayRole).toInt());

   if (mProxyModel) {
       QStandardItem *item = mSourceModel->itemFromIndex(mProxyModel->mapToSource(index));
       rw->setItem(item);
       rw->setSex(item->data(Qt::DisplayRole).toInt());
   } else {
       QStandardItem *item = mSourceModel->itemFromIndex(index);
       rw->setItem(item);
       rw->setSex(item->data(Qt::DisplayRole).toInt());
   }


//    connect(rw, &RadioWidget::sexChangedByQModelIndex, this, [=](int row, int sex) {
//        if (mSourceModel) {
////            qDebug() << "sexChangedByQModelIndex" << row << mSourceModel->item(row, 0)->text();
//            mSourceModel->item(row, 4)->setData(sex, Qt::DisplayRole);
//        }
//    });
   connect(rw, &RadioWidget::sexChangedByQStandardItem, this, [=](int row, int sex) {
       if (mSourceModel) {
//            qDebug() << "sexChangedByQStandardItem" << row << mSourceModel->item(row, 0)->text();
           mSourceModel->item(row, 4)->setData(sex, Qt::DisplayRole);
       }
   });

   return rw;
}

void RadioDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
   int sex = index.model()->data(index).toInt();
   RadioWidget *rw = static_cast<RadioWidget *>(editor);
   rw->setSex(sex);
}

void RadioDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
   editor->setGeometry(option.rect);
}

void RadioDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
   RadioWidget *rw = static_cast<RadioWidget *>(editor);
   int sex = rw->getSex();
   model->setData(index, sex);
}

void RadioDelegate::setProxyModel(QSortFilterProxyModel *sortModel)
{
   mProxyModel = sortModel;
   QAbstractItemModel *source_model = mProxyModel->sourceModel();
   mSourceModel = static_cast<QStandardItemModel *>(source_model);
}

void RadioDelegate::setSourceModel(QStandardItemModel *model)
{
   mSourceModel = model;
}

SortFilterProxyModel::SortFilterProxyModel(QObject *parent)
   : QSortFilterProxyModel(parent)
{

}

void SortFilterProxyModel::setSex(int sex)
{
   mSex = sex;
   invalidateFilter();
}

bool SortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
   QModelIndex index1 = sourceModel()->index(source_row, 1);
   QString name = sourceModel()->data(index1).toString();

   QModelIndex index4 = sourceModel()->index(source_row, 4);
   int sex = sourceModel()->data(index4).toInt();

   bool sex_flag = (mSex == -1) ? true : (sex == mSex);
   return sex_flag && name.contains(filterRegExp());
}
  • ui_mainwindow.ui