使用QTreeView时通常存在使用复选框的需求,如选中节点A后勾选其下的所有子节点,但qt原生控件并没有很好的支持这一功能,而查阅网上资料大都是改变Model的角色值Qt::CheckStateRole,这会直接改变源数据,如果使用的一个Model对应多个View的,会在多个View上显示相同的选中结构,这大概率不是想要的结果。
为了不能修改原数据,要将显示与数据的完全分离,这就意味着选中的状态要有单独的容器进行存储,根据该容器显示不同的选中状态,如选中、未选中和不完全选中。
考虑到单独的数据容器,需要对其进行维护,即随着原Model的增删改进行更新,有一定工作量且容易出错,可以复用View原有的selectionModel,将原有选中状态的显示替换为复选框即可,如下:
void StyledItemDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
{
// 此处省略源码
// ...
option->index = index;
option->features |= QStyleOptionViewItem::HasCheckIndicator;
// 选中状态替换为复选框
if(m_view->selectionModel()->isSelected(index)){
option->checkState = Qt::Checked;
}
else{
// 非选中状态需要判断是否不完全选中
option->checkState = isPartially(index) ? Qt::PartiallyChecked: Qt::Unchecked;
}
// 此处省略源码
// ...
}
class StyledItemDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
StyledItemDelegate(QAbstractItemView *parent);
virtual ~StyledItemDelegate();
void setCheckable();// 选中状态增加复选框
bool isCheckable() const;
protected:
virtual void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const;
private:
bool isPartially(const QModelIndex &index) const;
private:
QAbstractItemView* m_view;
bool m_isCheckable;
};
class CustomTreeView : public QTreeView
{
Q_OBJECT
public:
CustomTreeView(QWidget *parent = nullptr);
virtual ~CustomTreeView();
void setCheckable();// 选中状态增加复选框
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
void setChildSelected(QModelIndex index, bool isSelected);// 子节点状态与当前节点状态统一
void setParentSelected(QModelIndex index, bool isSelected);// 父节点状态由当前节点的同级节点共同决定,只用同级节点全选中才为选中
private:
StyledItemDelegate* m_delegate;
QTimer* m_selectionTimer;
};
StyledItemDelegate::StyledItemDelegate(QAbstractItemView *parent)
: m_view(parent)
, m_isCheckable(false)
{
}
StyledItemDelegate::~StyledItemDelegate()
{
}
void StyledItemDelegate::setCheckable()
{
m_isCheckable = true;
}
bool StyledItemDelegate::isCheckable() const
{
return m_isCheckable;
}
// 截取源码,部分修改
void StyledItemDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
{
QVariant value = index.data(Qt::FontRole);
if (value.isValid() && !value.isNull()) {
option->font = qvariant_cast<QFont>(value).resolve(option->font);
option->fontMetrics = QFontMetrics(option->font);
}
value = index.data(Qt::TextAlignmentRole);
if (value.isValid() && !value.isNull())
option->displayAlignment = Qt::Alignment(value.toInt());
value = index.data(Qt::ForegroundRole);
if (value.canConvert<QBrush>())
option->palette.setBrush(QPalette::Text, qvariant_cast<QBrush>(value));
option->index = index;
if(isCheckable()){
option->features |= QStyleOptionViewItem::HasCheckIndicator;
// 选中状态替换为复选框
if(m_view->selectionModel()->isSelected(index)){
option->checkState = Qt::Checked;
}
else{
// 非选中状态需要判断是否不完全选中
option->checkState = isPartially(index) ? Qt::PartiallyChecked: Qt::Unchecked;
}
}
else{
// 原复选框
value = index.data(Qt::CheckStateRole);
if (value.isValid() && !value.isNull()) {
option->features |= QStyleOptionViewItem::HasCheckIndicator;
option->checkState = static_cast<Qt::CheckState>(value.toInt());
}
}
value = index.data(Qt::DecorationRole);
if (value.isValid() && !value.isNull()) {
option->features |= QStyleOptionViewItem::HasDecoration;
switch (value.type()) {
case QVariant::Icon: {
option->icon = qvariant_cast<QIcon>(value);
QIcon::Mode mode;
if (!(option->state & QStyle::State_Enabled))
mode = QIcon::Disabled;
else if (option->state & QStyle::State_Selected)
mode = QIcon::Selected;
else
mode = QIcon::Normal;
QIcon::State state = option->state & QStyle::State_Open ? QIcon::On : QIcon::Off;
QSize actualSize = option->icon.actualSize(option->decorationSize, mode, state);
// For highdpi icons actualSize might be larger than decorationSize, which we don't want. Clamp it to decorationSize.
option->decorationSize = QSize(qMin(option->decorationSize.width(), actualSize.width()),
qMin(option->decorationSize.height(), actualSize.height()));
break;
}
case QVariant::Color: {
QPixmap pixmap(option->decorationSize);
pixmap.fill(qvariant_cast<QColor>(value));
option->icon = QIcon(pixmap);
break;
}
case QVariant::Image: {
QImage image = qvariant_cast<QImage>(value);
option->icon = QIcon(QPixmap::fromImage(image));
option->decorationSize = image.size() / image.devicePixelRatio();
break;
}
case QVariant::Pixmap: {
QPixmap pixmap = qvariant_cast<QPixmap>(value);
option->icon = QIcon(pixmap);
option->decorationSize = pixmap.size() / pixmap.devicePixelRatio();
break;
}
default:
break;
}
}
value = index.data(Qt::DisplayRole);
if (value.isValid() && !value.isNull()) {
option->features |= QStyleOptionViewItem::HasDisplay;
option->text = displayText(value, option->locale);
}
option->backgroundBrush = qvariant_cast<QBrush>(index.data(Qt::BackgroundRole));
// disable style animations for checkboxes etc. within itemviews (QTBUG-30146)
option->styleObject = 0;
}
bool StyledItemDelegate::isPartially(const QModelIndex &index) const
{
if(m_view->model()->rowCount(index)){
QModelIndexList indexList = m_view->model()->match(index.child(0,0), Qt::DisplayRole, "*", -1, Qt::MatchWildcard | Qt::MatchRecursive);
foreach(QModelIndex childIndex, indexList){
if(m_view->selectionModel()->isSelected(childIndex)){
return true;
}
}
}
return false;
}
CustomTreeView::CustomTreeView(QWidget *parent)
: QTreeView(parent)
{
m_delegate = new StyledItemDelegate(this);
setItemDelegate(m_delegate);
}
CustomTreeView::~CustomTreeView()
{
}
void CustomTreeView::setCheckable()
{
setSelectionMode(QAbstractItemView::MultiSelection);
m_delegate->setCheckable();
m_selectionTimer = new QTimer(this);
connect(m_selectionTimer, &QTimer::timeout, this, [this]{
m_selectionTimer->stop();
update();
});
}
void CustomTreeView::mousePressEvent(QMouseEvent *event)
{
QTreeView::mousePressEvent(event);
if(m_delegate->isCheckable()){
// 开启复选框后刷新父子节点的选中状态
QModelIndex index = indexAt(event->pos());
setChildSelected(index, selectionModel()->isSelected(index));
setParentSelected(index, selectionModel()->isSelected(index));
// 更新parent不完全选中状态
QModelIndex parentIndex = index.parent();
while (parentIndex.isValid()) {
update(parentIndex);
parentIndex = parentIndex.parent();
}
}
}
void CustomTreeView::mouseMoveEvent(QMouseEvent *event)
{
// 开启复选框后屏蔽通过鼠标移动多选
if(m_delegate->isCheckable())
return;
QTreeView::mouseMoveEvent(event);
}
void CustomTreeView::setChildSelected(QModelIndex index, bool isSelected)
{
int rowCount = model()->rowCount(index);
if(rowCount == 0)
return;
for(int i=0; i<rowCount; i++){
QModelIndex childIndex = index.child(i,0);
selectionModel()->select(childIndex, isSelected ? QItemSelectionModel::Select : QItemSelectionModel::Deselect);
setChildSelected(childIndex, isSelected);
}
}
void CustomTreeView::setParentSelected(QModelIndex index, bool isSelected)
{
QModelIndex parentIndex = index.parent();
if(!parentIndex.isValid())
return;
if(isSelected){
for(int i=0; i<model()->rowCount(parentIndex); i++){
// 若父节点有一个子节点不为选中状态,则无需向上传递
if(!selectionModel()->isSelected(parentIndex.child(i,0))){
return;
}
}
selectionModel()->select(parentIndex, QItemSelectionModel::Select);
}
else{
selectionModel()->select(parentIndex, QItemSelectionModel::Deselect);
}
setParentSelected(parentIndex, isSelected);
}