业务场景
假设有如下表格,该表格记录了以下五个人员信息,初始数据展示依据 Number
倒序排列。
Number | Name | Age |
---|---|---|
5 | Name_5 | 14 |
4 | Name_4 | 13 |
3 | Name_3 | 14 |
2 | Name_2 | 12 |
1 | Name_1 | 13 |
该表格支持搜索,支持排序,支持新建人员信息,以及删除人员信息,人员 Name
和 Age
可修改。
现需要实现以下业务需求:
1.排序支持正序、倒序、默认等三种排序方式,默认排序回到初始状态,即依据 Number
倒序排列
2.排序状态下,新建的人员信息放在第一行,不参与连续,连续新建的人员信息延续此规则
3.排序状态下,修改人员信息不影响当前的排序状态
4.搜索只支持 Name
和 Age
列搜索,Number
列不参与搜索
5.搜索状态下,新建的人员信息放在第一行,连续新建的人员信息延续此规则
(即使新建的人员信息不在搜索结果内,退出搜索时也要保证新建的人员信息在第一行)
功能实现
现自定义各种表格相关类,如下:
// 自定义表格
class CustomTableView : public QTableView
// 自定义表格表头
class CustomHeaderView : public QHeaderView
// 自定义表格数据 model
class CustomDataModel : public QStandardItemModel
// 排序和搜索代理 model
class CustomSortFilterModel : public QSortFilterProxyModel
CustomTableView* m_pDataTable;
CustomHeaderView* m_pDataTableHeader;
CustomDataModel* m_pDataModel;
CustomSortFilterModel* m_pSortFilterModel;
m_pDataTable = new CustomTableView(this);
m_pDataTableHeader = new CustomHeaderView(this);
m_pDataModel = new CustomDataModel(this);
m_pSortFilterModel = new CustomSortFilterModel(this);
QStringList headerLabels;
headerLabels << QString("Number")
<< QString("Name")
<< QString("Age");
m_pDataModel->setHorizontalHeaderLabels(headerLabels);
m_pDataModel->setColumnCount(headerLabels.size());
m_pSortFilterModel->setSourceModel(m_pDataModel );
m_pDataTable->setModel(m_pSortFilterModel);
m_pDataTable->setHorizontalHeader(m_pDataTableHeader);
1.排序支持正序、倒序、默认等三种排序方式,默认排序回到初始状态,即依据 Number
倒序排列
QTableView 有自带的排序功能,使用一行代码即可实现。
m_pDataTable->setSortingEnabled(true);
这个自带的排序,直接点击表头即可依据当前列对表格进行排序,支持正序和倒序排列两种,且在开启的过程中会自动进行排序。
// QTableView::setSortingEnabled 源码部分
void QTableView::setSortingEnabled(bool enable)
{
Q_D(QTableView);
d->sortingEnabled = enable;
horizontalHeader()->setSortIndicatorShown(enable);
if (enable) {
disconnect(d->horizontalHeader, SIGNAL(sectionEntered(int)),
this, SLOT(_q_selectColumn(int)));
disconnect(horizontalHeader(), SIGNAL(sectionPressed(int)),
this, SLOT(selectColumn(int)));
connect(horizontalHeader(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)),
this, SLOT(sortByColumn(int)), Qt::UniqueConnection);
// 默认进行排序
sortByColumn(horizontalHeader()->sortIndicatorSection(),
horizontalHeader()->sortIndicatorOrder());
} else {
connect(d->horizontalHeader, SIGNAL(sectionEntered(int)),
this, SLOT(_q_selectColumn(int)), Qt::UniqueConnection);
connect(horizontalHeader(), SIGNAL(sectionPressed(int)),
this, SLOT(selectColumn(int)), Qt::UniqueConnection);
disconnect(horizontalHeader(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)),
this, SLOT(sortByColumn(int)));
}
}
QTableView 自带的这种排序不支持当前的业务场景,则需要进行自定义。
思路就是通过表头的点击信号,并且根据当前的排序状态,手动去调用 sortByColumn
接口去实现排序。
// 定义表格排序类型枚举类型
enum TableSortType
{
TST_Defalut = 0, // 默认排序
TST_Ascending = 1, // 升序
TST_Descending = 2, // 降序
};
// 表头开启可点击,开启之后点击表头即可收到 sectionClicked 信号
m_pDataTableHeader->setSectionsClickable(true);
// 连接表头点击信号
connect(m_pDataTableHeader, &QHeaderView::sectionClicked,
this, &MainWindow::dataTableHeaderSectionClicked);
void MainWindow::dataTableHeaderSectionClicked(int column)
{
// 获取当前的排序状态,根据排序状态获取下一种排序类型,然后重新进行排序
TableSortType sortType = m_pDataTableHeader->getColumnSortType(column);
switch (sortType)
{
case TableSortType::TST_Defalut:
{
sortType = TableSortType::TST_Ascending;
m_pDataTable->sortByColumn(column, Qt::AscendingOrder);
}break;
case TableSortType::TST_Ascending:
{
sortType = TableSortType::TST_Descending;
m_pDataTable->sortByColumn(column, Qt::DescendingOrder);
}break;
case TableSortType::TST_Descending:
{
sortType = TableSortType::TST_Defalut;
m_pDataTable->sortByColumn(0, Qt::DescendingOrder);
}break;
default:
break;
}
// 排序完成后,更新该列的排序状态
m_pDataTableHeader->setColumnSortType(column, sortType );
}
void CustomHeaderView::setColumnSortType(int column, TableSortType sortType)
{
m_sortType = sortType;
m_sortColumn = column;
m_sorted = true;
}
TableSortType CustomHeaderView::getColumnSortType(int column)
{
if (m_sortColumn == column)
{
return m_sortType;
}
return TableSortType::TST_Defalut;
}
2.排序状态下,新建的人员信息放在第一行,不参与连续,连续新建的人员信息延续此规则
新建人员信息,放在表格第一行,代码实现如下:
int row = 0;
m_pDataModel->insertRow(row);
int number = m_pDataModel->rowCount();
QString name = QString("Name_%1").arg(number);
int age = 12;
QStandardItem* pNumberItem = new QStandardItem();
pNumberItem->setData(number, Qt::DisplayRole);
pNumberItem->setTextAlignment(Qt::AlignCenter);
m_pDataModel->setItem(row, 0, pNumberItem);
QStandardItem* pNameItem = new QStandardItem();
pNameItem->setData(name, Qt::DisplayRole);
pNameItem->setTextAlignment(Qt::AlignCenter);
m_pDataModel->setItem(row, 1, pNameItem);
QStandardItem* pAgeItem = new QStandardItem();
pAgeItem->setData(age, Qt::DisplayRole);
pAgeItem->setTextAlignment(Qt::AlignCenter);
m_pDataModel->setItem(row, 2, pAgeItem);
本人习惯性使用这种方法给表格添加数据,但是这种方法会有问题,
在排序状态下,新建的行会自动进行排序;在搜索状态下,新建的行会导致数据设置错误,
新的实现方式如下:
QList<QStandardItem*> itemsList;
int row = 0;
int number = m_pDataModel->rowCount();
QString name = QString("Name_%1").arg(number);
int age = 12;
QStandardItem* pNumberItem = new QStandardItem();
pNumberItem->setData(number, Qt::DisplayRole);
pNumberItem->setTextAlignment(Qt::AlignCenter);
itemsList << pNumberItem;
QStandardItem* pNameItem = new QStandardItem();
pNameItem->setData(name, Qt::DisplayRole);
pNameItem->setTextAlignment(Qt::AlignCenter);
itemsList << pNumberItem;
QStandardItem* pAgeItem = new QStandardItem();
pAgeItem->setData(age, Qt::DisplayRole);
pAgeItem->setTextAlignment(Qt::AlignCenter);
itemsList << pAgeItem;
m_pDataModel->insertRow(row, itemsList);
之前习惯了第一种方式去实现表格数据插入,在看 QTableView 文档的时候实在找不到什么合适的方法。
最后试了这种表格数据插入方式,就不会有问题了。这种实现方式在搜索和排序状态下,都能保证数据的正确插入。
3.排序状态下,修改人员信息不影响当前的排序状态
这个就比较简单了,接口文档也很容易查到对应的接口,一行代码解决,实现方式如下:
m_pSortFilterModel->setDynamicSortFilter(false);
4.搜索只支持 Name 和 Age 列搜索,Number 列不参与搜索
QSortFilterProxyModel 有自带的搜索,主要是通过 setFilterKeyColumn
和 setFilterRegExp
接口实现。
// 搜索列默认值为 -1,就是所有列都支持搜索
m_pSortFilterModel->setFilterKeyColumn(-1);
// 设置第 2 列为搜索列
m_pSortFilterModel->setFilterKeyColumn(2);
// 搜做的时候调用 setFilterRegExp 即可
QString filterText = "12";
m_pSortFilterModel->setFilterRegExp(filterText );
// 经测试,如此操作并不能使得第 2 列和第 3 列都成为搜索列,最后还是只有第 3 列为搜索列。
m_pSortFilterModel->setFilterKeyColumn(1);
m_pSortFilterModel->setFilterKeyColumn(2);
这是自带的搜索功能,如果对于搜索功能没什么特别要求的话,自带的功能即可完成需求。
但是自带的搜索功能不符合业务需求,所以需要自定义。
QRegExp m_filterRegExp;
// 自定义 setCustomFilterRegExp 接口,记录搜索条件
void CustomSortFilterModel::setCustomFilterRegExp(const QString& pattern)
{
QRegExp exp(pattern, Qt::CaseInsensitive);
m_filterRegExp = exp;
QSortFilterProxyModel::setFilterRegExp(exp);
}
QList<int> m_filterColumnList;
// 自定义 setCustomFilterColumn 接口,记录搜索列。
void CustomSortFilterModel::setCustomFilterColumn(int column)
{
if (-1 == column)
{
m_filterColumnList.clear();
QSortFilterProxyModel::setFilterKeyColumn(column);
}
else
{
m_filterColumnList << column;
}
}
// 重写虚函数 filterAcceptsRow,自定义搜索内容
// 该函数会遍历每一行,返回 true 则该行显示,返回 false 则该行不显示
bool CustomSortFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
if (m_filterRegExp.isEmpty())
{
// 在表格初始化时,会调用该函数,搜索条件为空时,直接返回 true
return true;
};
if (m_filterColumnList.isEmpty())
{
// 如果记录的搜索列为空,则每一列都是搜索列,直接调用默认德函数即可
return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
}
else
{
// 遍历记录德搜索列,如何符合搜索条件直接返回 true,不符合继续遍历
// 遍历完成后,则说明都不符合搜索结果,直接返回 false
for (auto column : m_filterColumnList)
{
QModelIndex index = this->sourceModel()->index(source_row, column, source_parent);
if (!index.isValid())
{
// index 不可用情况下的处理,参考了源码,直接返回了 true
return true;
}
QString text = this->sourceModel()->data(index).toString();
bool bDone = text.contains(m_filterRegExp);
if (bDone)
{
return true;
}
}
return false;
}
}
准备好了就可以调用搜索功能了,实现如下:
m_pSortFilterModel->setCustomFilterColumn(1);
m_pSortFilterModel->setCustomFilterColumn(2);
QString filterText = "12";
m_pSortFilterModel->setCustomFilterColumn(filterText );
5.搜索状态下,新建的人员信息放在第一行,不参与连续,连续新建的人员信息延续此规则
这个和第 2 个业务场景一样,换种表格数据插入的实现方式就可以了。
QList<QStandardItem*> itemsList;
int row = 0;
int number = m_pDataModel->rowCount();
QString name = QString("Name_%1").arg(number);
int age = 12;
QStandardItem* pNumberItem = new QStandardItem();
pNumberItem->setData(number, Qt::DisplayRole);
pNumberItem->setTextAlignment(Qt::AlignCenter);
itemsList << pNumberItem;
QStandardItem* pNameItem = new QStandardItem();
pNameItem->setData(name, Qt::DisplayRole);
pNameItem->setTextAlignment(Qt::AlignCenter);
itemsList << pNumberItem;
QStandardItem* pAgeItem = new QStandardItem();
pAgeItem->setData(age, Qt::DisplayRole);
pAgeItem->setTextAlignment(Qt::AlignCenter);
itemsList << pAgeItem;
m_pDataModel->insertRow(row, itemsList);
相关问题解决
以上业务需求实现完成后,会遇到一些其他的问题。探究如下。
1.排序状态下,进行搜索,退出搜索结果,表格顺序异常
表格先按照 Name
正序排序,结果如下:
Number | Name | Age |
---|---|---|
1 | Name_1 | 13 |
2 | Name_2 | 12 |
3 | Name_3 | 14 |
4 | Name_4 | 13 |
5 | Name_5 | 14 |
在搜索条件为 14
时,搜索结果如下:
Number | Name | Age |
---|---|---|
3 | Name_3 | 14 |
5 | Name_5 | 14 |
然后清空搜索添加,表格内容如下:
Number | Name | Age |
---|---|---|
3 | Name_3 | 14 |
5 | Name_5 | 14 |
1 | Name_1 | 13 |
2 | Name_2 | 12 |
4 | Name_4 | 13 |
这个结果就很奇怪,退出搜索结果后的内容就是搜索结果的数据内容和不是搜索结果的数据内容简单地组合在了一起,根据就不符合当前按照 Name
正序排序的规则。
所以只能在搜索完成之后,根据当前的排序状态,重新进行排序一次,实现如下:
void MainWindow::filterTextChanged(const QString& filterText)
{
m_pSortFilterModel->setCustomFilterRegExp(filterText);
if (filterText.isEmpty() && m_pDataTableHeader->getSorted())
{
TableSortType sortType = m_pDataTableHeader->getCurrentSortType();
switch (sortType)
{
case TableSortType::TS_Defalut:
{
m_pDataTable->sortByColumn(0, Qt::DescendingOrder);
}break;
case TableSortType::TS_Ascending:
{
m_pDataTable->sortByColumn(m_pDataTableHeader->getCurrentSortColumn(), Qt::AscendingOrder);
}break;
case TableSortType::TS_Descending:
{
m_pDataTable->sortByColumn(m_pDataTableHeader->getCurrentSortColumn(), Qt::DescendingOrder);
}break;
default:
break;
}
}
}
bool CustomHeaderView::getSorted()
{
return m_sorted;
}
TableSort CustomHeaderView::getCurrentSortType()
{
return m_sortType;
}
int CustomHeaderView::getCurrentSortColumn()
{
return m_sortColumn;
}
2.排序状态下,进行搜索,然后新建人员信息,退出搜索结果,表格顺序异常
在排序和搜索双重状态下,然后新建人员信息,按照上文的需求是要把新建的人员信息放在第一行。
退出搜素结果的时候会发现,新建的那一行被排序了,需要重新让他回到第一行。实现方式如下:
首先,新建人员信息的时候,存储该人员信息的 Number
,并作为人员信息的唯一标识。
QList<QStandardItem*> itemsList;
int row = 0;
int number = m_pDataModel->rowCount();
QString name = QString("Name_%1").arg(number);
int age = 12;
int numberRole = Qt::UserRole + 1;
QStandardItem* pNumberItem = new QStandardItem();
pNumberItem->setData(number, Qt::DisplayRole);
pNumberItem->setData(number, numberRole );
pNumberItem->setTextAlignment(Qt::AlignCenter);
itemsList << pNumberItem;
QStandardItem* pNameItem = new QStandardItem();
pNameItem->setData(name, Qt::DisplayRole);
pNameItem->setData(number, numberRole );
pNameItem->setTextAlignment(Qt::AlignCenter);
itemsList << pNumberItem;
QStandardItem* pAgeItem = new QStandardItem();
pAgeItem->setData(age, Qt::DisplayRole);
pAgeItem->setData(number, numberRole );
pAgeItem->setTextAlignment(Qt::AlignCenter);
itemsList << pAgeItem;
m_pDataModel->insertRow(row, itemsList);
然后,在新建的时候记录新建人员信息的 Numver
标识。
void CustomSortFilterModel::addCreatedNumber(int number)
{
m_createdNumberList << number;
}
void CustomSortFilterModel::clearCreatedNumber()
{
m_createdNumberList.clear();
}
// 新建的时候,插入新的 number
m_pSortFilterModel->addCreatedNumber(number);
// 重新排序之后清空 number
m_pSortFilterModel->clearCreatedNumber();
最后,重写 CustomSortFilterModel
的 lessThan
函数。
bool CustomSortFilterModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const
{
if (!m_createdNumberList.isEmpty())
{
int leftNumber = source_left.data(Qt::UserRole + 1).toInt();
int rightNumber = source_right.data(Qt::UserRole + 1).toInt();
auto order = sortOrder();
if (Qt::AscendingOrder == order)
{
// 正序排序,leftNumber 符合则返回 true,就是排列在表格上方,rightNumber 符合则返回 false, 排列在表格上方
if (m_createdNumberList.contains(leftNumber) && m_createdNumberList.contains(rightNumber))
{
return leftNumber < rightNumber ? false : true;
}
else if (m_createdNumberList.contains(leftNumber))
{
return true;
}
else if (m_createdNumberList.contains(rightNumber))
{
return false;
}
}
else if (Qt::DescendingOrder == order)
{
// 正序排序,leftNumber 符合则返回 false,就是排列在表格上方,rightNumber 符合则返回 true, 排列在表格上方
if (m_createdNumberList.contains(leftNumber) && m_createdNumberList.contains(rightNumber))
{
return leftNumber < rightNumber ? true : false;
}
else if (m_createdNumberList.contains(leftNumber))
{
return false;
}
else if (m_createdNumberList.contains(rightNumber))
{
return true;
}
}
}
return QSortFilterProxyModel::lessThan(source_left, source_right);
}
经过这样的处理后,在排序和搜索双重状态下,然后新建人员信息,退出搜素状态,重新进行排序,排序顺序是正确的,并且新建的人员信息也会保持在第一行。
3.中文排序异常
排序状态下,遇到中文排序,顺序不对,一行代码即可解决。
m_pSortFilterModel->setSortLocaleAware(true);
4.排序状态下数据更新错误
依据 Number
正序排序状态下,选中人员信息Number 4
,更新其 Age
数据:
QStandardItem* item = m_pDataModel->item(m_pDataTable->currentIndex().row(), 2);
if (item)
{
item->setData(12, Qt::DisplayRole);
}
排序状态下,view 层的当前行为 3,但是 model 层中对应的实际行为 1,使用以上代码会导致数据更新到别的表格单元格中,正确实现如下:
auto sourceIndex = m_pSortFilterModel->mapToSource(m_pDataTable->currentIndex());
QStandardItem* item = m_pDataModel->item(sourceIndex.row(), 2);
if (item)
{
item->setData(12, Qt::DisplayRole);
}
Ps:
写在最后,为什么会有如此奇怪的业务场景?
只能说,算了,说多了都是泪 …