QTreeWidget提供了一个树形视图来展示数据,它可以显示多级层次结构的数据
用户可以通过展开和折叠来浏览整个树,使用QTreeWidgetItem作为节点项

1. 效果演示

本案例展示表格控件的以下操作:
✅ 表头、交替显示背景色、单元格可编辑
✅ 选择行为:单元格选择或行选择
✅ 数据的增删改查

tree

2. 属性和方法

QTableWidget有很多属性和方法,完整的可查看帮助文档。

2.1 表头

  • 设置和获取列的数目、表头文本
// 获取/设置列的数目
int columnCount() const
void setColumnCount(int columns)

// 设置表头的文本
void setHeaderLabels(const QStringList &labels) 
  • 设置列的宽度
// 首先,获取表头
QHeaderView *header()() const

// 然后,设置列的宽度
void QHeaderView::setSectionResizeMode(QHeaderView::ResizeMode mode)

其中ResizeMode是一个枚举,取值如下:

枚举 含义
HeaderView::Interactive 0 用户可拖动改变列宽
QHeaderView::Fixed 2 固定列宽
QHeaderView::Stretch 1 拉伸自适应列宽大小
QHeaderView::ResizeToContents 3 根据内容设置列宽

通常,先整体设置为QHeaderView::Stretch, 然后根据需要对单独的列进行设置,如下:

// 首先,设置所有列的宽度模式为拉伸
ui->treeWidget->header()->setSectionResizeMode(QHeaderView::Stretch);

// 然后,可以单独设置某一列的宽度模式
//    ui->treeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
// 或者,可以单独设置某一列为固定宽度
//    ui->treeWidget->header()->setSectionResizeMode(0, QHeaderView::Fixed);
//    ui->treeWidget->setColumnWidth(0, 200);

2.2 Item标记

QTreeWidget中,ItemFlagsQTreeWidgetItem项的标记,用于描述项的属性和状态,从而控制项的外观和行为

// 获取和设置单元格是否可编辑
Qt::ItemFlags flags() const;
void setFlags(Qt::ItemFlags flags);

Qt::ItemFlags是一个枚举,这些标记可以单独使用,也可以组合使用,以提供丰富的项属性控制
常用取值如下:

枚举 含义
Qt::NoItemFlags 0 无标记,表示项没有任何标记设置
Qt::ItemIsSelectable 1 项可以选择。这是默认设置,除非明确禁止
Qt::ItemIsEditable 2 项可以编辑
Qt::ItemIsDragEnabled 4 项可以拖拽。如果设置了此标记,用户可以将项拖拽到其他位置或控件中
Qt::ItemIsDropEnabled 8 项可以接受拖拽。如果设置了此标记,其他项可以被拖拽到此项上
Qt::ItemIsUserCheckable 16 该选项允许用户可以通过勾选或取消勾选来更改项目的选中状态
Qt::ItemIsEnabled 32 使能。项将响应用户交互(如点击、拖拽等)。默认启用
Qt::ItemIsAutoTristate 64 根据子项自动设置该项为三态之一
(子项全选,则该项选中,子项全不选,则该项未选中,子项部分旋转,则该项半选)
QT::ItemNeverHasChildren 128 项永远不会有子项。如果设置了此标记,将无法向项添加子项。
Qt::ItemIsUserTristate 256 用户可以设置项为三态之一

2.3 隔行交替背景色

将奇数行和偶数行,它们的背景色不同,便于用户浏览

// 获取和设置是否允许隔行交替背景色
bool alternatingRowColors() const
void setAlternatingRowColors(bool enable)

2.4 选择模式、选择行为

2.4.1 选择行为

所谓选择行为,是指当点击一个单元格时,是选中该单元格,还是选中一整行

// 获取和设置选择行为
QAbstractItemView::SelectionBehavior selectionBehavior() const
void setSelectionBehavior(QAbstractItemView::SelectionBehavior behavior)

这是继承自其父类QAbstractItemView中的方法,QAbstractItemView::SelectionBehavior是一个枚举,取值为:

枚举 含义
QAbstractItemView::SelectItems 0 选中单元格
QAbstractItemView::SelectRows 1 选中单元格所在行
QAbstractItemView::SelectColumns 2 选中单元格所在列

2.4.2 选择模式

所谓选择模式,是指设置表格控件只可选择单行、可选择多行等

// 获取和设置选择模式
QAbstractItemView::SelectionMode selectionMode() const
void setSelectionMode(QAbstractItemView::SelectionMode mode)

这是继承自其父类QAbstractItemView中的方法,QAbstractItemView::SelectionMode是一个枚举,取值为:

枚举 含义
QAbstractItemView::NoSelection 0 不可选择
QAbstractItemView::SingleSelection 1 单行选择,一次只允许选择一行
QAbstractItemView::MultiSelection 2 多行选择,鼠标单击就可以选择多行
QAbstractItemView::ExtendedSelection 3 扩展选择,按shift键选中一个范围内的行,ctrl键可以选中不相邻的行
QAbstractItemView::ContiguousSelection 4 相邻选择,按shift键或ctrl键都可以选中一个范围内的行

2.5 增加、删除

QTreeWidget中增加和删除项目,分两种情况:顶层项目、子项目

// 插入/追加一个顶层项
void insertTopLevelItem(int index, QTreeWidgetItem *item);
void addTopLevelItem(QTreeWidgetItem *item);

// 插入/追加多个顶层项
void insertTopLevelItems(int index, const QList<QTreeWidgetItem*> &items);
void addTopLevelItems(const QList<QTreeWidgetItem*> &items);

// 删除顶层项、删除子项
QTreeWidgetItem *takeTopLevelItem(int index);
void removeChild(QTreeWidgetItem *child);

2.6 信号槽

// 单击、双击树形控件中的项时触发。参数:item表示被点击的项,column表示被点击的列
void itemClicked(QTreeWidgetItem *item, int column)
void itemDoubleClicked(QTreeWidgetItem *item, int column)

// 展开、折叠树形控件中的项时触发。参数:item表示被展开、折叠的项。
void itemExpanded(QTreeWidgetItem *item)
void itemCollapsed(QTreeWidgetItem *item)

3. 从零实现

从零写代码实现整体效果,以演示树形控件的属性以及信号槽的用法

3.1 布局

在UI设计师界面,拖拽对应的控件,修改显示的文字、控件的名称,然后完成布局

  • 3GroupBox的字体的Point Size属性修改为14,并勾选Bold
  • 将所有其他控件的字体的Point Size属性修改为12
  • 选中MyWidget,在右侧的属性窗口中,将Layout中的layoutStretch属性修改为8,1,也就是左右比例为8:1

布局完成效果如下:
qt-base

3.2 代码实现

3.2.1 初始化表头

来到mywidget.cpp的构造函数,初始化表头,使用R字符串来设置样式表(所见即所得,更直观),如下:

MyWidget::MyWidget(QWidget* parent) : QWidget(parent), ui(new Ui::MyWidget) {
    ui->setupUi(this);

    this->setWindowTitle("明王讲QT | 第二章 常用控件 | 2.14 树形控件QTreeWidget (WX: coding4096)");

    // 1. 设置表头
    // 1.1 共有4列,并添加列的名称
    ui->treeWidget->setColumnCount(4);
    QStringList headers;
    headers << "城市"
            << "面积(平方公里)"
            << "人口(万人)"
            << "GDP(亿元)";
    ui->treeWidget->setHeaderLabels(headers);  // 设置列标题

    // 1.2 设置列的宽度
    // 设置所有列的宽度模式为拉伸
    ui->treeWidget->header()->setSectionResizeMode(QHeaderView::Stretch);

    // 可以单独设置某一列的宽度模式
    //    ui->treeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
    // 或者,可以单独设置某一列为固定宽度
    //    ui->treeWidget->header()->setSectionResizeMode(0, QHeaderView::Fixed);
    //    ui->treeWidget->setColumnWidth(0, 200);

    // 1.3 设置样式表
    ui->treeWidget->setStyleSheet(R"(
        QTreeView {
            color:#222222;
            font:16px "微软雅黑";
            background-color: rgb(255, 255, 255);
            alternate-background-color:rgb(250, 250, 250);
        }
    )");
    ui->treeWidget->header()->setStyleSheet(R"(
        QHeaderView::section {
            color:#222222;
            font:bold 16px "微软雅黑";
            background-color: rgb(255, 255, 255);
            border:0px solid #c8c8c8;
        }
    )");
}

3.2.2 初始化数据,并遍历

首先,在mywidget.h中声明初始化和遍历的成员函数,如下:

#include <QTreeWidgetItem>

class MyWidget : public QWidget {
private:
    void initData();

    // 遍历
    int getItemDepth(QTreeWidgetItem* item);      // 获取QTreeWidgetItem的深度(缩进层次)
    void traverseItem(QTreeWidgetItem* item);     // 递归调用,遍历当前项的所有子项
    void traverseItems(QTreeWidget* treeWidget);  // 遍历QTreeWidget中的所有顶级项
};

然后,在mywidget.cpp中实现初始化函数,如下:

void MyWidget::initData() {
    // 1. 添加顶级item-北京
    QTreeWidgetItem* beiJing = new QTreeWidgetItem({"北京市", "16807", "2186", "43800"});
    ui->treeWidget->addTopLevelItem(beiJing);

    // 2. 添加顶级item-上海
    QTreeWidgetItem* shangHai = new QTreeWidgetItem({"上海市", "6340", "2487", "47200"});
    ui->treeWidget->addTopLevelItem(shangHai);

    // 3. 添加顶级item-广东省
    QTreeWidgetItem* guangDong = new QTreeWidgetItem({"广东省"});
    ui->treeWidget->addTopLevelItem(guangDong);

    QTreeWidgetItem* guangZhou = new QTreeWidgetItem({"广州市", "7434", "1882", "30355"});
    QTreeWidgetItem* shenZhen = new QTreeWidgetItem({"深圳市", "1997", "1779", "34606"});

    guangDong->addChild(guangZhou);
    guangDong->addChild(shenZhen);

    // 4. 添加顶级item-浙江省
    QTreeWidgetItem* zheJiang = new QTreeWidgetItem({"浙江省"});
    ui->treeWidget->addTopLevelItem(zheJiang);

    QTreeWidgetItem* hangZhou = new QTreeWidgetItem({"杭州市", "16850", "1252", "20059"});
    QTreeWidgetItem* ningBo = new QTreeWidgetItem({"宁波市", "9816", "969", "16452"});
    QTreeWidgetItem* wenZhou = new QTreeWidgetItem({"温州市", "11784", "976", "8730"});

    zheJiang->addChild(hangZhou);
    zheJiang->addChild(ningBo);
    zheJiang->insertChild(1, wenZhou);

    // 5. 插入顶级item-天津
    QTreeWidgetItem* tianJin = new QTreeWidgetItem({"天津", "11966", "1363", "16737"});
    ui->treeWidget->insertTopLevelItem(2, tianJin);  // 2 表示插入到第2个位置

    // 6. 添加顶级item-江苏
    QTreeWidgetItem* jiangSu = new QTreeWidgetItem({"江苏"});
    ui->treeWidget->addTopLevelItem(jiangSu);

    QTreeWidgetItem* suZhou = new QTreeWidgetItem({"苏州市", "8657", "1295", "24653"});
    QTreeWidgetItem* nanJing = new QTreeWidgetItem({"南京市", "6587", "954", "17421"});
    QTreeWidgetItem* wuXi = new QTreeWidgetItem({"无锡市", "4627", "749", "15456"});

    jiangSu->addChild(suZhou);
    jiangSu->addChild(nanJing);
    jiangSu->insertChild(1, wuXi);
}

然后,在mywidget.cpp中实现遍历函数,如下:

// 获取QTreeWidgetItem的深度(缩进层次)
int MyWidget::getItemDepth(QTreeWidgetItem* item) {
    // 如果条目为空,则深度为0
    if ( !item ) {
        return 0;
    }

    // 如果条目没有父项,则为根条目,深度为1
    if ( !item->parent() ) {
        return 1;
    }

    // 递归地获取父项的深度,并加1
    return getItemDepth(item->parent()) + 1;
}

// 递归调用,遍历当前项的所有子项
void MyWidget::traverseItem(QTreeWidgetItem* item) {
    // 如果项为空,则返回
    if ( !item ) {
        return;
    }

    //  首先,根据层次深度,添加打印时的缩进
    QString s = "";
    int depth = getItemDepth(item);
    if ( depth == 1 ) {
    } else if ( depth == 2 ) {
        s += QString(" ").repeated(4);
    } else if ( depth == 3 ) {
        s += QString(" ").repeated(8);
    } else if ( depth == 4 ) {
        s += QString(" ").repeated(12);
    }

    // 然后,追加每一列
    int childCount = item->childCount();
    if ( childCount == 0 ) {
        s += (item->text(0) + "|" + item->text(1) + "|" + item->text(2) + "|" + item->text(3));
    } else {
        s += item->text(0);
    }
    // qDebug() << s;
    qDebug().noquote() << s;

    for ( int i = 0; i < childCount; ++i ) {
        QTreeWidgetItem* childItem = item->child(i);
        traverseItem(childItem);
    }
}

// 遍历QTreeWidget中的所有顶级项
void MyWidget::traverseItems(QTreeWidget* treeWidget) {
    // 遍历所有顶级项
    int topLevelItemCount = treeWidget->topLevelItemCount();
    for ( int i = 0; i < topLevelItemCount; ++i ) {
        QTreeWidgetItem* topLevelItem = treeWidget->topLevelItem(i);
        traverseItem(topLevelItem);  // 对每个顶级项调用递归函数
    }
}

最后,在mywidget.cpp的构造中,调用初始化数据和遍历函数,如下:

MyWidget::MyWidget(QWidget* parent) : QWidget(parent), ui(new Ui::MyWidget) {

    // 2. 初始化数据,并遍历所有的节点,也就是QTreeWidgetItem
    initData();
    traverseItems(ui->treeWidget);
}

此时,控制台打印效果:
qt-base

3.2.3 复选按钮槽函数

首先,在mywidget.h中声明3个槽函数,以及2个成员函数,如下:

class Widget : public QWidget {
private:
    // 设置条目是否可编辑
    void setItemEditable(QTreeWidgetItem* item, bool editable);     // 递归调用,遍历当前项的所有子项
    void setItemsEditable(QTreeWidget* treeWidget, bool editable);  // 遍历QTreeWidget中的所有顶级项

private slots:
    void on_chkHeader_checkStateChanged(Qt::CheckState state);
    void on_chkAlternating_checkStateChanged(Qt::CheckState state);
    void on_chkCellEditable_checkStateChanged(Qt::CheckState state);
};

然后,在mywidget.cpp中实现这5个函数,如下:

// 递归调用,遍历当前项的所有子项
void MyWidget::setItemEditable(QTreeWidgetItem* item, bool editable) {
    // 如果项为空,则返回
    if ( !item ) {
        return;
    }

    // 设置项为可编辑
    //    qDebug() << "before: " << item->flags();
    if ( editable ) {
        item->setFlags(item->flags() | Qt::ItemIsEditable);
    } else {
        item->setFlags(item->flags() & ~Qt::ItemIsEditable);
    }
    qDebug() << "after: " << item->flags();

    for ( int i = 0; i < item->childCount(); ++i ) {
        QTreeWidgetItem* childItem = item->child(i);
        setItemEditable(childItem, editable);
    }
}

// 遍历QTreeWidget中的所有顶级项
void MyWidget::setItemsEditable(QTreeWidget* treeWidget, bool editable) {
    // 遍历所有顶级项
    int topLevelItemCount = treeWidget->topLevelItemCount();
    for ( int i = 0; i < topLevelItemCount; ++i ) {
        QTreeWidgetItem* topLevelItem = treeWidget->topLevelItem(i);
        setItemEditable(topLevelItem, editable);  // 对每个顶级项调用递归函数
    }
}

void MyWidget::on_chkHeader_checkStateChanged(Qt::CheckState state) {
    if ( state == Qt::Checked ) {
        ui->treeWidget->header()->show();
    } else if ( state == Qt::Unchecked ) {
        ui->treeWidget->header()->hide();
    }
}

void MyWidget::on_chkAlternating_checkStateChanged(Qt::CheckState state) {
    if ( state == Qt::Checked ) {
        ui->treeWidget->setAlternatingRowColors(true);
    } else if ( state == Qt::Unchecked ) {
        ui->treeWidget->setAlternatingRowColors(false);
    }
}

void MyWidget::on_chkCellEditable_checkStateChanged(Qt::CheckState state) {
    if ( state == Qt::Checked ) {
        // 当双击单元格/选中单元格然后单击/按下编辑键F2,都可以编辑单元格。
        // QTreeWidget本身没有直接设置所有项为可编辑的接口。
        // ui->treeWidget->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed);

        // 这里遍历所有的item,将它们逐一设置为可编辑(更简单地,通常先将item设置为可编辑,再添加到treeWidget中)
        setItemsEditable(ui->treeWidget, true);
    } else if ( state == Qt::Unchecked ) {
        // ui->treeWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);

        setItemsEditable(ui->treeWidget, false);
    }
}

最后,在mywidget.cpp的构造函数中,默认勾选上 “表头” 复选框,如下:

MyWidget::MyWidget(QWidget* parent) : QWidget(parent), ui(new Ui::MyWidget) {

    // 3. 默认显示表头
    ui->chkHeader->click();
}

3.2.4 行选择、单元格选择

首先,在mywidget.h中声明槽函数,如下:

#include <QButtonGroup>

class MyWidget : public QWidget {

private slots:
    void onBtnGroupClicked(int id);

private:
    QButtonGroup* btnGroup;
};

然后,在mywidget.cpp中实现这个槽函数,如下:

void MyWidget::onBtnGroupClicked(int id) {
    ui->treeWidget->setSelectionMode(QAbstractItemView::SingleSelection);
    ui->treeWidget->setSelectionBehavior(id == 0 ? QAbstractItemView::SelectItems : QAbstractItemView::SelectRows);
}

最后,在mywidget.cpp的构造函数中,关联信号槽,并将两个单选按钮放到一个组内,如下:

MyWidget::MyWidget(QWidget* parent) : QWidget(parent), ui(new Ui::MyWidget) {

    // 4. 设置行选择还是单元格选择
    btnGroup = new QButtonGroup(this);
    btnGroup->addButton(ui->radioCellSelect, 0);
    btnGroup->addButton(ui->radioRowSelect, 1);
    connect(btnGroup, &QButtonGroup::idClicked, this, &MyWidget::onBtnGroupClicked);
    ui->radioRowSelect->setChecked(true);
}

3.2.5 增删改查

首先,在mywidget.h中声明槽函数,如下:

class MyWidget : public QWidget {

private slots:
    void on_treeWidget_itemClicked(QTreeWidgetItem* item, int columnitem);  // 条目点击

    void on_btnAppendSibling_clicked();  // 追加同级
    void on_btnInsertSibling_clicked();  // 插入同级
    void on_btnAddChild_clicked();       // 添加子节点
    void on_btnModify_clicked();         // 修改
    void on_btnDelete_clicked();         // 删除
};

然后,在mywidget.cpp中实现这几个槽函数,如下:

void MyWidget::on_treeWidget_itemClicked(QTreeWidgetItem* item, int columnitem) {
    ui->lineEditCity->setText(item->text(0));
    ui->lineEditArea->setText(item->text(1));
    ui->lineEditPopulation->setText(item->text(2));
    ui->lineEditGDP->setText(item->text(3));
}

// 在当前条目同级的最后追加
void MyWidget::on_btnAppendSibling_clicked() {
    QTreeWidgetItem* currentItem = ui->treeWidget->currentItem();
    if ( currentItem ) {
        QString city = ui->lineEditCity->text().trimmed();
        QString area = ui->lineEditArea->text().trimmed();
        QString population = ui->lineEditPopulation->text().trimmed();
        QString gdp = ui->lineEditGDP->text().trimmed();
        QTreeWidgetItem* item = new QTreeWidgetItem({city, area, population, gdp});

        QTreeWidgetItem* parent = currentItem->parent();
        if ( parent ) {
            parent->addChild(item);
        } else {
            ui->treeWidget->addTopLevelItem(item);
        }
    }
}

// 在当前条目之前插入
void MyWidget::on_btnInsertSibling_clicked() {
    QTreeWidgetItem* currentItem = ui->treeWidget->currentItem();
    if ( currentItem ) {
        QString city = ui->lineEditCity->text().trimmed();
        QString area = ui->lineEditArea->text().trimmed();
        QString population = ui->lineEditPopulation->text().trimmed();
        QString gdp = ui->lineEditGDP->text().trimmed();
        QTreeWidgetItem* item = new QTreeWidgetItem({city, area, population, gdp});

        QTreeWidgetItem* parent = currentItem->parent();
        if ( parent ) {
            int index = parent->indexOfChild(currentItem);  // 获取该节点在父节点中的索引
            parent->insertChild(index, item);
        } else {
            int index = ui->treeWidget->indexOfTopLevelItem(currentItem);
            ui->treeWidget->insertTopLevelItem(index, item);
        }
    }
}

// 为当前条目添加子节点
void MyWidget::on_btnAddChild_clicked() {
    QTreeWidgetItem* currentItem = ui->treeWidget->currentItem();
    if ( currentItem ) {
        QString city = ui->lineEditCity->text().trimmed();
        QString area = ui->lineEditArea->text().trimmed();
        QString population = ui->lineEditPopulation->text().trimmed();
        QString gdp = ui->lineEditGDP->text().trimmed();

        QTreeWidgetItem* item = new QTreeWidgetItem({city, area, population, gdp});
        currentItem->addChild(item);
    }
}

// 修改选中的条目
void MyWidget::on_btnModify_clicked() {
    QTreeWidgetItem* currentItem = ui->treeWidget->currentItem();
    if ( currentItem ) {
        currentItem->setText(0, ui->lineEditCity->text().trimmed());
        currentItem->setText(1, ui->lineEditArea->text().trimmed());
        currentItem->setText(2, ui->lineEditPopulation->text().trimmed());
        currentItem->setText(3, ui->lineEditGDP->text().trimmed());
    }
}

// 删除当前条目
void MyWidget::on_btnDelete_clicked() {
    QTreeWidgetItem* currentItem = ui->treeWidget->currentItem();
    if ( currentItem ) {
        QTreeWidgetItem* parent = currentItem->parent();
        if ( parent ) {
            parent->removeChild(currentItem);
            delete currentItem;
        } else {
            int index = ui->treeWidget->indexOfTopLevelItem(currentItem);
            QTreeWidgetItem* item = ui->treeWidget->takeTopLevelItem(index);
            delete item;
        }
    }
}

4. 点赞、获取源码

看到这里的小伙伴,去B站给明王一个【免费的点赞】吧,你的支持,是我持续更新优质内容的动力,感谢~

源码下载地址
链接: https://pan.baidu.com/s/1-Bb-VFhb5208LLkoZtrTMQ
提取码: ming