QTreeView复选框的实现

引言

使用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;
    }

    // 此处省略源码
	// ...
}

完整代码

Demo效果如下:
在这里插入图片描述

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);
}

卡顿优化

上述代码中存在卡顿问题,主要是选中态是单独设置所有子项,需要更换为批量接口,代码如下:

void DataTreeView::setChildSelected(QModelIndex index, bool isSelected)
{
	int rowCount = model()->rowCount(index);
	if (rowCount == 0)
		return;

	selectionModel()->select(QItemSelection(index.child(0, 0), index.child(rowCount - 1, 0)), isSelected ? QItemSelectionModel::Select : QItemSelectionModel::Deselect);

	for (int i = 0; i < rowCount; i++) {
		setChildSelected(index.child(i, 0), isSelected);
	}
}
  • 11
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Arui丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值