Qt基于QSortFilterProxyModel的树形穿梭框实现

发布于:2022-11-02 ⋅ 阅读:(505) ⋅ 点赞:(0)

引言

穿梭框的实现在其他博文中已有涉及《Qt穿梭框实现》,但原本的实现是基于QListWidget,也就是说之前的穿梭框只支持列表结构,不支持树形结构的选择,这就有导致应用无法覆盖到带层级结构的树形结构,因此有了这一篇博文。

原本列表穿梭框的实现是通过将子项从左侧列表移动至右侧列表,但这个方式在树形结构中行不通,因为子项在移动时需要考虑父项,如果只选择了部分子项移动到右侧,那就意味着在左侧和右侧都存在相同的父项,而这个多出来的父项还要考虑在后续操作中的增删,导致维护较为困难。为了避免上述问题,此处采用了通过选中项控制子项显示隐藏的方式去规避繁琐的增删操作,也就是左右时两颗相同的树,左侧树是隐藏选中项,右侧是显示选中项,达到在视觉效果上的穿梭框。

此处需要使用到前面两篇博文中的内容《QSortFilterProxyModel的使用》《QTreeView复选框的实现》,去实现是Model和View。最后的实现效果如下:

在这里插入图片描述

Model与View实现

class SortFilterProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT

public:
    SortFilterProxyModel(QObject *parent = nullptr);
    virtual ~SortFilterProxyModel();

private:
    virtual bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const;
    virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const;

protected:
    virtual bool filterIndexByCustom(const QModelIndex & source_index) const;
};


class CustomProxyModel : public SortFilterProxyModel
{
    Q_OBJECT

public:
    CustomProxyModel(QObject* parent = nullptr);
    virtual ~CustomProxyModel();

    enum FilterMode{
        FM_NORMAL,
        FM_ACCEPT,
        FM_UNACCEPT,
    };

    void setAccept(const QSet<int>& acceptSet);
    void setUnaccept(const QSet<int>& unacceptSet);

protected:
    bool filterIndexByCustom(const QModelIndex & source_index) const override;

private:
    FilterMode m_filterMode;
    QSet<int> m_acceptSet;
    QSet<int> m_unacceptSet;
};

基类SortFilterProxyModel提供了虚函数filterIndexByCustom,用于子类中不同过滤需求,CustomProxyModel使用时需要提供唯一标识用于后续的过滤,目前是占用角色值Qt::UserRole,过滤方式提供三种模式(正常、接受、不接受),"接受"代表只显示m_acceptSet容器中对应ID的子项,"不接受"代表隐藏m_unacceptSet容器中对应ID的子项,针对左侧与右侧两种情况,左侧使用"不接受"模式,右侧使用"接受"模式。代码实现如下:

SortFilterProxyModel::SortFilterProxyModel(QObject *parent)
    : QSortFilterProxyModel(parent)
{

}

SortFilterProxyModel::~SortFilterProxyModel()
{

}

bool SortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
    QModelIndex source_index = sourceModel()->index(source_row, this->filterKeyColumn(), source_parent) ;
    if(source_index.isValid()){
        // 遍历子节点
        for(int i=0; i<sourceModel()->rowCount(source_index); ++i){
            if(filterAcceptsRow(i, source_index))
                return true;
        }

        // 遍历父节点
        //QModelIndex parentIndex = source_parent;
        //while (parentIndex.isValid()) {
        //    if(filterIndexByCustom(parentIndex))
        //        return true;
        //    parentIndex = parentIndex.parent();
        //}

        return filterIndexByCustom(source_index);
    }

    return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
}

bool SortFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
    // 通过当前视图中的index位置获取model中实际的数据
    QVariant leftData = sourceModel()->data(source_left);
    QVariant rightData = sourceModel()->data(source_right);

    // 颠倒顺序
    return leftData.toString() > rightData.toString();
}

bool SortFilterProxyModel::filterIndexByCustom(const QModelIndex &source_index) const
{
    QString key = sourceModel()->data(source_index, filterRole()).toString();
    return key.contains(filterRegExp()) ;
}



CustomProxyModel::CustomProxyModel(QObject *parent)
    : SortFilterProxyModel(parent)
    , m_filterMode(FM_NORMAL)
{

}

CustomProxyModel::~CustomProxyModel()
{

}

void CustomProxyModel::setAccept(const QSet<int> &acceptSet)
{
    m_filterMode = FM_ACCEPT;
    m_acceptSet = acceptSet;
    setFilterRegExp(filterRegExp());
}

void CustomProxyModel::setUnaccept(const QSet<int> &unacceptSet)
{
    m_filterMode = FM_UNACCEPT;
    m_unacceptSet = unacceptSet;
    setFilterRegExp(filterRegExp());
}

bool CustomProxyModel::filterIndexByCustom(const QModelIndex &source_index) const
{
    int itemID = source_index.data(Qt::UserRole).toInt();

    switch (m_filterMode) {
    case FM_ACCEPT:
        return SortFilterProxyModel::filterIndexByCustom(source_index) && m_acceptSet.contains(itemID);
    case FM_UNACCEPT:
        return SortFilterProxyModel::filterIndexByCustom(source_index) && !m_unacceptSet.contains(itemID);
    default:
        return SortFilterProxyModel::filterIndexByCustom(source_index);
    }
}

View的实现在本篇中并没有变化,参考《QTreeView复选框的实现》,使用的是CustomTreeView,此处就进行赘述。

穿梭框实现

class TransferDialog : public QDialog
{
    Q_OBJECT

public:
    TransferDialog(QWidget *parent = nullptr);
    virtual ~TransferDialog();

    void setSourceModel(QAbstractItemModel* sourceModel);

    QSet<int> selectionIDs();

private slots:
    void slot_toLeft();
    void slot_toRight();

private:
    CustomTreeView* m_leftView;
    CustomTreeView* m_rightView;

    CustomProxyModel* m_leftModel;
    CustomProxyModel* m_rightModel;

    QPushButton* m_toLeftBtn;
    QPushButton* m_toRightBtn;

    QSet<int> m_selectionIDs;
};

可以看到穿梭框由左右两棵树构成,使用的Model和View,还有两个按钮m_toLeftBtn和m_toRightBtn,顾名思义控制子项的左右移动,最后就是容器m_selectionIDs用于存储已选中的子项ID。对外的接口有两个,一个是setSourceModel,另一个是selectionIDs。selectionIDs返回当前选中的ID,setSourceModel则是设置源模型,因为左右两个Model使用的是代理模型,隐藏需要对源模型进行设置,同时这样能够节省开销以及简化代码。代码实现如下:

TransferDialog::TransferDialog(QWidget *parent)
    : QDialog(parent)
{
    setFixedSize(600, 400);

    m_leftModel = new CustomProxyModel(this);
    m_leftView = new CustomTreeView(this);
    m_leftView->setModel(m_leftModel);
    m_leftView->setCheckable();

    m_rightModel = new CustomProxyModel(this);
    m_rightView = new CustomTreeView(this);
    m_rightView->setModel(m_rightModel);
    m_rightView->setCheckable();

    m_toLeftBtn = new QPushButton("<", this);
    m_toRightBtn = new QPushButton(">", this);

    // 布局
    auto btnLayout = new QVBoxLayout;
    btnLayout->addStretch();
    btnLayout->addWidget(m_toLeftBtn);
    btnLayout->addWidget(m_toRightBtn);
    btnLayout->addStretch();

    auto mainLayout = new QHBoxLayout(this);
    mainLayout->addWidget(m_leftView);
    mainLayout->addLayout(btnLayout);
    mainLayout->addWidget(m_rightView);

    // 信号槽连接
    connect(m_toLeftBtn, &QPushButton::clicked, this, &TransferDialog::slot_toLeft);
    connect(m_toRightBtn, &QPushButton::clicked, this, &TransferDialog::slot_toRight);
}

TransferDialog::~TransferDialog()
{

}

void TransferDialog::setSourceModel(QAbstractItemModel* sourceModel)
{
    m_leftModel->setSourceModel(sourceModel);
    m_leftModel->setUnaccept(m_selectionIDs);

    m_rightModel->setSourceModel(sourceModel);
    m_rightModel->setAccept(m_selectionIDs);
}

QSet<int> TransferDialog::selectionIDs()
{
    return m_selectionIDs;
}

void TransferDialog::slot_toLeft()
{
    QModelIndexList indexList = m_rightView->currentIndexes();
    if(indexList.isEmpty()){
        return;
    }

    for(auto item : indexList){
        QVariant tmpData = item.data(Qt::UserRole);
        if(tmpData.isValid()){
            m_selectionIDs.remove(tmpData.toInt());
        }
    }

    m_leftModel->setUnaccept(m_selectionIDs);
    m_rightModel->setAccept(m_selectionIDs);
}

void TransferDialog::slot_toRight()
{
    QModelIndexList indexList = m_leftView->currentIndexes();
    if(indexList.isEmpty()){
        return;
    }

    for(auto item : indexList){
        QVariant tmpData = item.data(Qt::UserRole);
        if(tmpData.isValid()){
            m_selectionIDs.insert(tmpData.toInt());
        }
    }

    m_leftModel->setUnaccept(m_selectionIDs);
    m_rightModel->setAccept(m_selectionIDs);
}

源模型构造

前文也说到,过滤是基于每个子项的唯一标识来的,所以在构造源模型时需要增加ID的写入,此处举一个例子,构造了三层子项的源模型,代码如下:

	m_sourceModel = new QStandardItemModel(this);

    int id = 1;
    foreach(QString str, QColor::colorNames()){
        auto tmpItem = new QStandardItem(str);
        tmpItem->setData(++id, Qt::UserRole);

        for (int i=0; i<3; i++) {
            auto midItem = new QStandardItem(QString::number(i));
            midItem->setData(++id, Qt::UserRole);
            tmpItem->appendRow(midItem);

            for (int j=0; j<2; j++) {
                auto leafItem = new QStandardItem(QString::number(j + 100));
                leafItem->setData(++id, Qt::UserRole);
                tmpItem->child(i)->appendRow(leafItem);
            }
        }
        m_sourceModel->appendRow(tmpItem);
    }