0.前言
之前总结了 QTableView 实现排序 ,但是这里还有个问题,就是 Qt 默认的排序状态只有升序和降序,没法通过点击表头恢复到默认的顺序,他这个 SortOrder 宏也只有升序降序两个枚举值(有点奇怪,Qt 很多枚举都有 None 之类的):
enum SortOrder {
AscendingOrder,
DescendingOrder
};
没办法,要实现恢复默认顺序 ,只能自定义一个 QHeaderView ,最终效果如下:
(网上也有另一个类似的,不过实现细节有区别:https://www.cnblogs.com/swarmbees/p/11519796.html)
1.实现思路
我们是通过点击表头来操作排序的,所以直接去 QHeaderView 源码找鼠标点击事件,看他怎么处理的。结果很遗憾,里面基本都是用的 private 类来操作,也没有什么相关的虚函数接口,只能重写下鼠标点击事件。
//源码片段,大部分都封装在了 private 类
void QHeaderView::mousePressEvent(QMouseEvent *e)
{
Q_D(QHeaderView);
if (d->state != QHeaderViewPrivate::NoState || e->button() != Qt::LeftButton)
return;
int pos = d->orientation == Qt::Horizontal ? e->x() : e->y();
int handle = d->sectionHandleAt(pos);
d->originalSize = -1; // clear the stored original size
if (handle == -1) {
d->pressed = logicalIndexAt(pos);
if (d->clickableSections)
emit sectionPressed(d->pressed);
bool acceptMoveSection = d->movableSections;
if (acceptMoveSection && d->pressed == 0 && !d->allowUserMoveOfSection0)
acceptMoveSection = false; // Do not allow moving the tree nod
if (acceptMoveSection) {
d->section = d->target = d->pressed;
if (d->section == -1)
return;
d->state = QHeaderViewPrivate::MoveSection;
d->setupSectionIndicator(d->section, pos);
} else if (d->clickableSections && d->pressed != -1) {
updateSection(d->pressed);
d->state = QHeaderViewPrivate::SelectSections;
}
} else if (sectionResizeMode(handle) == Interactive) {
d->originalSize = sectionSize(handle);
d->state = QHeaderViewPrivate::ResizeSection;
d->section = handle;
d->preventCursorChangeInSetOffset = false;
}
d->firstPos = pos;
d->lastPos = pos;
d->clearCascadingSections();
}
在鼠标按下时,我们保存按下的表头列号,此时也要判断下是否在拉伸列宽(我用的鼠标形状来判断,因为没有相关公有接口)。鼠标弹起时,判断列号相同则没有 move 交换位置(还可以和点击时的位置计算下移动距离),满足条件就将该列切换排序状态。
这里我自己定义了一个枚举:
//排序状态
enum SortOrder
{
NoOrder = 0 //未排序
, DescOrder //降序
, AscOrder //升序
};
切换排序状态的时候,先判断是否点击的上次那一列,是的话则用上一次的排序状态来计算下一次的,如果点击的另一列则直接用默认状态来计算下一次状态:
SortHeaderView::SortOrder SortHeaderView::getNextOrder(SortHeaderView::SortOrder order) const
{
//状态循环:无排序-降序-升序
switch (order)
{
case NoOrder: return DescOrder;
case DescOrder: return AscOrder;
case AscOrder: break;
}
return NoOrder;
}
切换状态后,我是通过信号槽通知外部来修改状态(一般可以放到 QTableView 进行):
//排序自定义信号
//如果是自定义view,这段加到view里去
connect(header, &SortHeaderView::sortOrderChanged,
this, [this,header,proxy_model](int logicIndex,SortHeaderView::SortOrder order){
QTableView *view=ui->tableView;
//无效index或NoOrder就设置为默认未排序状态
if (logicIndex < 0 || order == SortHeaderView::NoOrder){
//去掉排序三角样式
header->setSortIndicator(-1, Qt::DescendingOrder);
//-1则还原model默认顺序
proxy_model->sort(-1, Qt::DescendingOrder);
}
else{
switch (order) {
case SortHeaderView::DescOrder:
view->sortByColumn(logicIndex, Qt::DescendingOrder);
break;
case SortHeaderView::AscOrder:
view->sortByColumn(logicIndex, Qt::AscendingOrder);
break;
}
}
});
Qt 文档显示,QSortFilterProxyModel sort 列号为 1 则恢复默认顺序,如果 Model sort 也是我们自己写的,就留一个对应的接口。
另外,我们还可以设置不排序的列号,在点击时作判断。
2.实现代码:
完整代码链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qt/SortHeaderView
主要实现:
#ifndef SORTHEADERVIEW_H
#define SORTHEADERVIEW_H
#include <QHeaderView>
/**
* @brief 自定义表头,以支持取消排序状态
*/
class SortHeaderView : public QHeaderView
{
Q_OBJECT
public:
//排序状态
enum SortOrder
{
NoOrder = 0 //未排序
, DescOrder //降序
, AscOrder //升序
};
public:
explicit SortHeaderView(Qt::Orientation orientation = Qt::Horizontal,
QWidget *parent = nullptr);
//不排序的行列
//(为什么放header不放proxy?因为排序是点击header调用的)
QList<int> getUnsortIndexs() const;
void setUnsortIndexs(const QList<int> &rowOrColumns);
void appendUnsortIndex(int rowOrColumn);
protected:
//显示的时候同步indicator三角状态,
//因为初始化可能是用的sortByColumn来排序,但是这个接口不是虚函数没法自定义
//所以就在显示的时候同步下状态
void showEvent(QShowEvent *event) override;
// 点击表头
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
// 根据当前排序状态得到下一个状态
SortOrder getNextOrder(SortOrder order) const;
signals:
//因为view里关联了自带的sortIndicatorChanged,
//所以自定义一个,避免冲突
void sortOrderChanged(int index, SortOrder order);
private:
// 不进行排序的行列
QList<int> unsortIndexs;
//鼠标按下时对应原model index
int pressLogicIndex = -1;
//鼠标按下时对应显示的index,可能是交换了位置的
int pressVisualIndex = -1;
//上一次点击对应的logic index
int previousLogicIndex = -1;
//当前排序状态
SortOrder sortOrder = NoOrder;
};
#endif // SORTHEADERVIEW_H
#include "SortHeaderView.h"
#include <QMouseEvent>
#include <QCursor>
SortHeaderView::SortHeaderView(Qt::Orientation orientation, QWidget *parent)
: QHeaderView(orientation,parent)
{
//禁用默认的点击处理,这样就不会触发默认排序
//自己处理点击来设置排序
setSectionsClickable(false);
}
QList<int> SortHeaderView::getUnsortIndexs() const
{
return unsortIndexs;
}
void SortHeaderView::setUnsortIndexs(const QList<int> &rowOrColumns)
{
unsortIndexs = rowOrColumns;
}
void SortHeaderView::appendUnsortIndex(int rowOrColumn)
{
unsortIndexs.push_back(rowOrColumn);
}
void SortHeaderView::showEvent(QShowEvent *event)
{
QHeaderView::showEvent(event);
//因为初始化可能是用的sortByColumn来排序,但是这个接口不是虚函数没法自定义
//所以就在显示的时候同步下状态
previousLogicIndex = sortIndicatorSection();
if (previousLogicIndex >= 0) {
sortOrder = (sortIndicatorOrder() == Qt::AscendingOrder) ? AscOrder : DescOrder;
}
else {
sortOrder = NoOrder;
}
}
void SortHeaderView::mousePressEvent(QMouseEvent *event)
{
QHeaderView::mousePressEvent(event); //注意顺序
if (event->button() != Qt::LeftButton)
return;
const int pos = (orientation() == Qt::Horizontal) ? event->x() : event->y();
const int index = logicalIndexAt(pos);
Qt::CursorShape shape = cursor().shape();
//先判断index是否为有效的section,
//判断cursor-shape是为了过滤点resize-handle情况
pressLogicIndex = -1;
pressVisualIndex = -1;
if (index >= 0 && shape != Qt::SplitHCursor&&shape != Qt::SplitVCursor)
{
//排除不排序的index
if (!unsortIndexs.contains(index))
{
pressLogicIndex = index;
pressVisualIndex = visualIndex(index);
}
}
}
void SortHeaderView::mouseReleaseEvent(QMouseEvent *event)
{
QHeaderView::mouseReleaseEvent(event); //注意顺序
if (event->button() != Qt::LeftButton)
return;
const int pos = (orientation() == Qt::Horizontal) ? event->x() : event->y();
const int index = logicalIndexAt(pos);
const int visual_index = visualIndex(index);
//弹起后再进行排序操作
//此处还可以判断下鼠标的移动距离,暂略
if (pressLogicIndex >= 0 && visual_index == pressVisualIndex)
{
//和上次点击的index比较,相同的就轮换排序状态,不同的就降序
//放release不放pressed是为了避免双击时判断不准
if (pressLogicIndex == previousLogicIndex)
{
sortOrder = getNextOrder(sortOrder);
}
else
{
sortOrder = getNextOrder(NoOrder);
}
emit sortOrderChanged(pressLogicIndex, sortOrder);
previousLogicIndex = pressLogicIndex;
}
//注释不复位是为了避免双击时判断不准
//pressLogicIndex=-1;
}
SortHeaderView::SortOrder SortHeaderView::getNextOrder(SortHeaderView::SortOrder order) const
{
//状态循环:无排序-降序-升序
switch (order)
{
case NoOrder: return DescOrder;
case DescOrder: return AscOrder;
case AscOrder: break;
}
return NoOrder;
}
//Ui中放了一个QTableView
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "SortHeaderView.h"
#include <QRandomGenerator>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
initTable();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::initTable()
{
//设置自定义的表头
SortHeaderView *header=new SortHeaderView(Qt::Horizontal,ui->tableView);
ui->tableView->setHorizontalHeader(header);
//可以交换列
header->setSectionsMovable(true);
//单元格默认宽高
ui->tableView->verticalHeader()->setDefaultSectionSize(24);
ui->tableView->horizontalHeader()->setDefaultSectionSize(50);
//数据model
QStandardItemModel* model = new QStandardItemModel(this);
const int col_max=10;
const int row_max=20;
//设置列表头
model->setColumnCount(col_max);
for(int i=0;i<col_max;i++)
model->setHeaderData(i,Qt::Horizontal, QString("Col %1").arg(i));
//设置行表头
model->setRowCount(row_max);
for(int i=0;i<row_max;i++)
model->setHeaderData(i,Qt::Vertical, QString("Row %1").arg(i));
//使用随机数填充
QRandomGenerator *rand_gen=QRandomGenerator::global();
int value=0;
for(int i=0;i<row_max;i++){
for(int j=0;j<col_max;j++){
//第一列用行数,其他的用随机数
value=(j==0)?(i): (rand_gen->bounded(0,100));
QStandardItem *new_item=new QStandardItem(QString::number(value));
//用文本排序的话,默认是字典顺序
new_item->setData(value,Qt::InitialSortOrderRole);
//文本居中
new_item->setData(Qt::AlignCenter,Qt::TextAlignmentRole);
model->setItem(i, j, new_item);
}
}
//view会根据model提供的数据来渲染
//ui->tableView->setModel(model);
//屏蔽编辑
ui->tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
//设置排序
//给普通的 ItemModel 加一层 SortProxy
QSortFilterProxyModel *proxy_model = new QSortFilterProxyModel(this);
//默认排序role应该是dispalyrole
proxy_model->setSortRole(Qt::InitialSortOrderRole);
proxy_model->setSourceModel(model);
ui->tableView->setModel(proxy_model);
//允许点击排序
ui->tableView->setSortingEnabled(true);
//按第0列升序
// ui->tableView->sortByColumn(0,Qt::AscendingOrder);
//排序自定义信号
//如果是自定义view,这段加到view里去
connect(header, &SortHeaderView::sortOrderChanged,
this, [this,header,proxy_model](int logicIndex,SortHeaderView::SortOrder order){
QTableView *view=ui->tableView;
//无效index或NoOrder就设置为默认未排序状态
if (logicIndex < 0 || order == SortHeaderView::NoOrder){
//去掉排序三角样式
header->setSortIndicator(-1, Qt::DescendingOrder);
//QSortFilterProxyModel can be sorted by column -1,
//in which case it returns to the sort order of the underlying source model.
//-1则还原model默认顺序
proxy_model->sort(-1, Qt::DescendingOrder);
}
else{
switch (order) {
case SortHeaderView::DescOrder:
view->sortByColumn(logicIndex, Qt::DescendingOrder);
break;
case SortHeaderView::AscOrder:
view->sortByColumn(logicIndex, Qt::AscendingOrder);
break;
}
}
});
}