背景:
【QT表格-1】QStandardItem的堆内存释放需要单独delete,还是随QStandardItemModel的remove或clear自动销毁?-CSDN博客
【QT表格-2】QTableWidget单元格结束编辑操作endEditting_qtablewidget 单元格编辑事件-CSDN博客
【QT表格-3】QTableWidget导入/导出excel通用代码,不需要安装office,不依赖任何多余环境,甚至不依赖编程语言_qt excel-CSDN博客
【QT表格-4】由QTableView/QTableWidget显示进度条和按钮,理解qt代理delegate用法_qtablewidget代理-CSDN博客
【QT表格-5】QTableView用代码设置选中状态-CSDN博客
一个主子表结构,当切换主表行时, 子表对应更新显示数据。主子表都可以编辑并保存。
当子表编辑后未保存时,如果切换主表行,应提示保存,用户可以选择“是”、“否”、“取消”。其实“是”和“否”好实现,因为都是保持顺序执行,只不过选择是否执行保存而已。但“取消”就不一样了,需要停止下面的操作。
这种情况比较多见,比如某个文本编辑器,如果编辑的内容,关闭时就应该有这样的询问。并根据用户选择进行相应操作。
按说用过vs的winform的同行应该知道,这个并不难。现在看来是因为vs提供了相当丰富的消息事件响应机制。比如关闭窗口会有一个类似CloseQuery这样的消息,只要对应写它的事件就好了。但是qt当中,思路会有很大区别。
问题:
实际上我尝试了很多方法已经就差cancel动作了。在QTableWidget::currentCellChanged槽当中判断,如果用户放弃的操作,我会重新把焦点放到previous位置,
this->setCurrentCell(previousRow, previousColumn);
这样看起来就是“回滚”了用户操作。但实际上效果是,焦点确实回去了,所有属性也回去了,比方说,currentRow或者currentItem之类,都没问题,但单元格的背景色没回去,也就是看起来还是选择了下一个位置。
这不傻了么?怎么试都不行,我猜想qt肯定是在currentCellChanged之后还干了什么事,而这个信号没有提供返回值和指针参数或者引用参数,等于没法控制。所以开始研究。
开胃菜:
以上述“窗口关闭前询问”为例,其实qt有个closeEvent函数,重写它就行了。它有个event指针参数,通过它是否accept就能控制是否继续。比如:
void MainWindow::closeEvent(QCloseEvent *event)
{
const QString sTitle = "程序退出";
const QString sMessage = "此操作会退出系统\n"
"当前未保存的数据将丢失\n"
"要继续吗?";
if (QMessageBox::question(this, sTitle, sMessage, QMessageBox::Yes|QMessageBox::Cancel,QMessageBox::Cancel)
== QMessageBox::Yes)
{
event->accept();
}
else
{
event->ignore();
}
}
就像上图这样,挺简单的。
同理,很多需要控制是否继续的做法,都类似。
回到正题,最初我的需求怎么办?我需要切换主表行时来个询问,并决定是否继续。
分析:
如果直接套用closeEvent的思路,是想不通的。因为那是继承重写的做法。而表格是某个界面中的子对象,询问的操作需要在表格外实现,怎么重写?
像这种常见的界面互动,要么直接调用函数,要么信号槽。不想随便触动函数指针的概念,我感觉应该先深入了解qt的方式。
直接调用:业务是需要表格内部,根据外界的用户选择,来决定内部的流程是否继续。理论上是表格内部调用外部。但制作表格类的时候,是不知道外界是否需要询问,或者如何询问的。貌似无解。
信号槽:界面线程的互动属于directConnection,效果很顺序执行一样。这里涉及到信号槽的一些基础概念。主要是connect函数最后一个连接参数的应用。以手册为准。
【qt信号槽-5】信号槽相关注意事项记录-CSDN博客
但是信号槽怎么互动?发出去再传回来?难道需要收发两次?显然不是好办法,毕竟繁琐,主要是用起来感觉还不是随大流的风格。
过程:
过程艰辛,最终我是下载的qt源码才知道怎么回事的。这里只说关键步骤。
对于我的需求,主要用到QTableWidget::currentCellChanged信号,目的是能根据用户选择决定是否继续,还是取消。经过研究qt源码,QTableWidget.cpp有这样一段:
void QTableWidgetPrivate::_q_emitCurrentItemChanged(const QModelIndex ¤t,
const QModelIndex &previous)
{
Q_Q(QTableWidget);
QTableWidgetItem *currentItem = tableModel()->item(current);
QTableWidgetItem *previousItem = tableModel()->item(previous);
if (currentItem || previousItem)
emit q->currentItemChanged(currentItem, previousItem);
emit q->currentCellChanged(current.row(), current.column(), previous.row(), previous.column());
}
这样看着后面没干什么事,只是看到currentItemChanged比currentCellChanged要靠前触发,而且有先决条件。接着看,_q_emitCurrentItemChanged这个信号是怎么来的。
void QTableWidgetPrivate::setup()
{
...
// selection signals
QObject::connect(q->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
q, SLOT(_q_emitCurrentItemChanged(QModelIndex,QModelIndex)));
QObject::connect(q->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
q, SIGNAL(itemSelectionChanged()));
...
}
那个q->selectionModel()跟踪一下就知道,它是QAbstractItemView::selectionModel(),是个QItemSelectionModel。主要看它的currentChanged和selectionChanged这俩信号的途径。
在qitemselectionmodel.cpp中搜索currentChanged就看见原因了,确实是currentChanged发送以后,会有更新界面的代码,最后再发送selectionChanged。代码太多就不贴了。
所以selectionChanged才是最后执行的,如果要回滚界面,也要在这里。但有个很恶心的事情。看这个:
QObject::connect(q->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
q, SIGNAL(itemSelectionChanged()));
连接时丢了两个很重要的参数,要不然非常好用,不知道qt为什么这样,真心不好。也许后期的版本中修复了。
原因找到,解决就容易了。
方法:
我的QTableWidget自己包装了一个类,
先定义一个发往外界的查询信号:void sigRowChangeQuery(QEvent *event);。内含event指针,用于判断用户操作,很符合qt风格。这里注意,因为是界面交互,都在ui线程,所以默认是direct连接方式,所以可以接收到event的更改。
当然还有另外一个信号:void sigRowChanged(int iRow);,见名知意,通知外界行选发生。
写了槽on_currentCellChanged用于处理行选。其中:
if (currentRow != previousRow)
{
QEvent event(QEvent::None);
emit sigRowChangeQuery(&event);
if (!event.isAccepted())//If the slot was canceled by the user.
{
m_iRow_Rollback = previousRow;
m_iCol_Rollback = previousColumn;
return;
}
emit sigRowChanged(currentRow);
}
用两个变量记住要回滚的位置。再写槽on_itemSelectionChanged处理界面回滚:
if (m_iRow_Rollback > -1 && m_iCol_Rollback > -1)
{
this->blockSignals(true);
this->setCurrentCell(m_iRow_Rollback, m_iCol_Rollback);
this->blockSignals(false);
//Restore the variable.
m_iRow_Rollback = -1;
m_iCol_Rollback = -1;
}
这样就行了。
外面处理用户操作时,写槽onGridMain_RowChangeQuery(QEvent *event),根据判断再设置event的accept标志,这样的用法就顺畅多了。是不是跟closeEvent用法一样?就是要这种效果。
所以这样的做法可以延伸的其它类似的场景。
本文完。