QTreeView使用整理

在Qt开发过程中,树控件QTreeView使用的非常频繁。各种批量展示和编辑信息的地方,都用得上该控件。
在使用QTreeView过程中,用到各种常规、不常规的功能,并进行过各种改造。
这里将这些知识和技巧作一个总结。

一、Model/View框架介绍

1.简介

Model/View架构分为三部分:模型、视图和委托。主要目的是将数据的存储与显示分离。

  • Model模型:对外提供标准接口存取数据,不关心数据如何显示。
  • View视图:自定义数据的显示方式,不关心数据如何组织存储。
  • Delegate委托:在视图的基础上可以自定义特殊的显示和编辑效果。

在这里插入图片描述

一般只有Model与内存上的原始数据打交道,通过原始数据构造一个Model,然后View从Model取数据进行展示。
两者耦合度低,一般对其中一方进行修改,另一方影响很小,因此把一种QT View控件替换为另一种View控件时,Model的代码基本不需要多少改动即可正常运行。
委托一般在我需要定制一些特殊的显示效果或输入方式时使用,提供了很大的灵活性。
这种分层架构给代码的逻辑性和可维护性带来了很大的提升。

2.常用Model和View

最常用的两种View控件为QTableView和QTreeView。其实,QTreeView一般也能满足QTableView的功能。
最常用的Model类型为QStandardItemModel,能满足大部分开发需要。
如果数据量比较大,对性能和内存要求比较高,可以使用自定义model,下文有讲。

完整的Model类型有下面这些:

  • QStringListModel:存储简单的字符串列表
  • QStandardItemModel:可以用于树结构的存储,提供了层次数据
  • QFileSystemModel:本地系统的文件和目录信息
  • QSqlQueryModel、QSqlTableModel、QSqlRelationalTableModel:存取数据库数据

QT提供了多个预定义好的视图类:

  • QListView:用于显示列表
  • QTableView:用于显示表格
  • QTreeView:用于显示层次数据

委托一般继承自QStyledItemDelegate类进行定制开发。

二、QSS风格美化

默认的QTreeView是这样的:
在这里插入图片描述

用QSS来改造QTreeView的样式,一般会处理这些项:

  • 表头:背景色、文字色、边框、高度
  • 控件整体:背景色、文字色、边框
  • 元素:背景色、文字色、边框、高度(处理normal、hover、press三态的颜色)
  • 分支:颜色或图片(normal、hover、press三态)
    示例代码如下:
QHeaderView::section
{ 
    height:25px;
    color:white;
    background:#505050;
    border-left:0px solid gray; 
    border-right:1px solid gray; 
    border-top:0px solid gray; 
    border-bottom:0px solid gray;
}
QTreeView
{
    border:none;
    background: #404040;
    show-decoration-selected: 1;  /* 设置整行颜色一致 */
}
QTreeView::item
{
    height: 25px;
    border: none;
    color: white;
    background: transparent;
}
QTreeView::item:hover
{
    background: #2CAEFF;
}
QTreeView::item:selected
{
    background: #1E90FF;
}
QTreeView::branch
{
    background: transparent;
}
QTreeView::branch:hover
{
    background: transparent;
}
QTreeView::branch:selected
{
    background: #1E90FF;
}
QTreeView::branch:closed:has-children
{
    image: url(:/QtExample/Resources/fold_normal.png);
}
QTreeView::branch:closed:has-children:hover
{
    image: url(:/QtExample/Resources/fold_hover.png);
}
QTreeView::branch:open:has-children
{
    image: url(:/QtExample/Resources/unfold_normal.png);
}
QTreeView::branch:open:has-children:hover
{
    image: url(:/QtExample/Resources/unfold_hover.png);
}

示例效果图:
在这里插入图片描述

三、自定义Delegate

虽然QSS可以定制绝大多数QTreeView的样式,但是对于某些情况,QSS实现起来比较棘手。接下来介绍一些高级的用法和改造技巧,用delegate,即委托,对QTreeView的item进行改造,以实现特殊的输入方式和显示效果。

1.使用委托:定制item输入效果

这里演示一个让某列使用QComboBox进行编辑的效果:
在这里插入图片描述

继承QStyledItemDelegate,写一个MyDelegate类,实现里面的以下方法:

  • createEditor: 当item激活编辑状态时,显示的内容。这里创建一个QComboBox
  • setEditorData:用以初始化createEditor里创建的控件内容。这里直接把当前item的text设置为QComboBox选中项。
  • setModelData:应用编辑后,修改model的data。这里把QComboBox的当前选中项文本设置为item的显示文本。
  • updateEditorGeometry:更新控件位置状态。
    示例代码如下:

MyEditDelegate.h


#include <QStyledItemDelegate>

class MyEditDelagate : public QStyledItemDelegate
{
    Q_OBJECT

public:
    MyEditDelagate(QObject *parent);
    ~MyEditDelagate();
    QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
    void setEditorData(QWidget *editor, const QModelIndex &index) const override;
    void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};

MyEditDelegate.cpp


#include "MyEditDelagate.h"
#include <QComboBox>

MyEditDelagate::MyEditDelagate(QObject *parent)
    : QStyledItemDelegate(parent)
{
}

MyEditDelagate::~MyEditDelagate()
{
}

QWidget *MyEditDelagate::createEditor(QWidget *parent, const QStyleOptionViewItem &/* option */, const QModelIndex & index) const
{
    /* 只对第4列采用此方法编辑 */
    if (index.column() == 3)
    {
        QComboBox* box = new QComboBox(parent);
        box->addItems({
            QString::fromLocal8Bit("测试输入0"),
            QString::fromLocal8Bit("测试输入1"),
            QString::fromLocal8Bit("测试输入2") });
        return box;
    }
    return NULL;
}

void MyEditDelagate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
    QString value = index.model()->data(index, Qt::EditRole).toString();
    QComboBox* box = static_cast<QComboBox*>(editor);
    box->setCurrentText(value);
}

void MyEditDelagate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
    QComboBox* box = static_cast<QComboBox*>(editor);
    model->setData(index, box->currentText(), Qt::EditRole);
}

void MyEditDelagate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
{
    editor->setGeometry(option.rect);
}

在初始化QTreeView时,新建一个MyDelegate,设itemDelegate:

注意:要设置单元格编辑属性为可编辑(setEditTriggers),否则点击单元格不会有反应。

//ui.treeView->setEditTriggers(QTreeView::NoEditTriggers); /* 单元格不能编辑 */
ui.treeView->setSelectionBehavior(QTreeView::SelectRows); /* 一次选中整行 */
ui.treeView->setSelectionMode(QTreeView::SingleSelection); /* 单选,配合上面的整行就是一次选单行 */
//ui.treeView->setAlternatingRowColors(true); /* 每间隔一行颜色不一样,当有qss时该属性无效 */
ui.treeView->setFocusPolicy(Qt::NoFocus); /* 去掉鼠标移到单元格上时的虚线框 */

MyEditDelagate* delegate = new MyEditDelagate(ui.treeView);
ui.treeView->setItemDelegate(delegate);

2.使用委托:定制item显示效果

可能相比于控制输入,定制item显示效果不那么常用,但是有时候通过委托来绘制是比较容易实现的。比如上述QSS中定义的hover高亮效果:

QTreeView::item:hover
{
    background: #2CAEFF;
}

上述QSS定义存在如下图所示无法高亮整行的问题:

在这里插入图片描述

可以通过在Delegate中(比如上述MyEditDelegate)中实现下面这个paint方法来轻松实现:

void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;

想要保持原来绘制的部分可以先写如下代码:

QStyleOptionViewItem viewOption(option);
initStyleOption(&viewOption, index);
QStyledItemDelegate::paint(painter, viewOption, index);

那么如何hover高亮整行呢。
上述QSS之所以无法高亮整行,是因为item的范围就不是从最左边开始的。因此只能高亮item范围内,item前面多余的空白部分无法填充。
先设置QTreeView的属性indentation为0,让所有的column的x坐标相同。
然后绘制高亮代码如下:

QRect rectBackground = option.rect;
rectBackground.setLeft(0);
if (option.state & QStyle::State_MouseOver)
{
   painter->fillRect(rectBackground, QColor("##E5F3FF"));
}

示例效果如下:

在这里插入图片描述

上述高亮会覆盖前面paint的部分,可以根据需要调整绘制顺序。
由于属性indentation为0,绘制原先部分时需要根据column作相应右偏移,只需要在

QStyledItemDelegate::paint(painter, viewOption, index);

前对viewOption的rect设置适当右偏移即可。或者也在委托里面定制。

在项目中使用delegate高亮部分关键字,效果如下:

在这里插入图片描述

可以把要显示的文本setData到model中,然后在paint的时候分别绘制高亮部分和非高亮部分。
示例代码:


QString strSearchKeywords;  /* 需要高亮的关键字 */
QString strProjectName = index.data(OrderRole = Qt::UserRole + 1).toString();  /* 完整的字符串 */
if (strProjectName.contains(strSearchKeywords))
{
     QStringList listSplit = strProjectName.split(strSearchKeywords);
     QStringList listProjectName;

      /* split默认保留分割后的空字符串,使得原字符串以关键字为分界分成多段 */
      if (listSplit.size() > 0)
      {
          for (int i = 0; i < listSplit.size(); i++)
          {
              /* 第0个若empty,说明是关键字,跳过,因为到第1个时可以加上 */
              if (i == 0 && listSplit[i].isEmpty())
               {
                    continue;
               }
                if (i > 0)
                {
                    /* 关键字作为分界的位置,需要将关键字也加入list以显示 */
                    listProjectName.append(strSearchKeywords);
                }
                if (!listSplit[i].isEmpty())
                {
                    listProjectName.append(listSplit[i]);
                }
            }
        }
        else
        {
            listProjectName.append(strSearchKeywords);
        }

        int iLeftOffset = rectName.left();
        for (QString strPart : listProjectName)
        {
            /* 如果该分段是关键字,则高亮,否则不高亮 */
            painter->setPen((strPart == strSearchKeywords) ? COLOR_HIGHLIGHT : COLOR_NORMAL);

            /* 根据该分段文字与名称总长的比例,获取获取该分段文字rect宽度 */
            QRect rectPart = rectName;
            rectPart.setLeft(iLeftOffset);
            rectPart.setWidth(QFontMetrics(font).width(strPart));
            painter->drawText(rectPart, Qt::AlignLeft | Qt::AlignVCenter | Qt::ElideRight, strPart);

            /* 下一个的左侧距离必须加上实际显示出来的文字宽度 */
            iLeftOffset += QFontMetrics(font).width(strPart);
     }
}

另外,还可以在delegate里面实现以下两个函数控制点击事件和toolTips事件

/* 捕捉点击事件 */
virtual bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index);

/* 捕捉toolTips事件 */
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index);

通过delegate绘制超出treeView可见宽度的内容,会导致treeView不能自动调整横向滚动条,可以重写QTreeView的setExpanded和expandAll来调整

示例图:
在这里插入图片描述

超出可见宽度item展开时,显示横向滚动条;
超出可见宽度item隐藏时,隐藏横向滚动条。

代码:


    setColumnWidth(0, this->width());   /* 设置item宽度 */
    QModelIndex modelIndex = this->indexAt(this->rect().topLeft());
    QFont font;
    font.setFamily("Microsoft YaHei");
    font.setPixelSize(13);

    /* 遍历所有可见index(从右上角可见index开始向下找) */
    while (modelIndex.isValid())
    {
        /* item宽度要足够宽,能够完整显示下拉箭头、名称、选中√图标 */
        int iItemWidth = QFontMetrics(font).width(QString(modelIndex.data(Qt::UserRole + 1).toString()));
        if (iItemWidth > this->columnWidth(0))
        {
            this->setColumnWidth(0, iItemWidth);
        }
        modelIndex = this->indexBelow(modelIndex);
    }

三、自定义Model

上文已经简单介绍过Qt的Model/View框架,提到了Qt预定义的几个model类型:

  • QStringListModel:存储简单的字符串列表
  • QStandardItemModel:可以用于树结构的存储,提供了层次数据
  • QFileSystemModel:本地系统的文件和目录信息
  • QSqlQueryModel、QSqlTableModel、QSqlRelationalTableModel:存取数据库数据

一般情况下满足需求了,不过有时候需要一些定制功能,或者是大量数据下对性能和开销比较注重,觉得自带的model无用功能太多效率比较低,这时候自定义model就比较适合了。
以上述示例程序为例,当行数达到10W的数据量级时,常规QStandardItemModel 在初始化tree的过程比自定义model慢很多,而且所占用的内存开销是自定义model的数倍甚至数十倍。数据量越大内存差距越明显。如果考虑百万、千万级别的数据,常规model相比于自定义model内存也大很多。

以下为自定义model需要实现的一些虚函数,将会被Qt在查询model数据时调用:

  • headerData: 获取表头第section列的数据
  • data: 核心函数,获取某个索引index的元素的各种数据role决定获取哪种数据,常用有下面几种:
    1)DisplayRole(默认):就是界面显示的文本数据
    2)TextAlignmentRole:就是元素的文本对齐属性
    3)TextColorRole、BackgroundRole:分别指文本颜色、单元格背景色
  • flags: 获取index的一些标志,一般不怎么改
  • index: Qt向你的model请求一个索引为parent的节点下面的row行column列子节点的元素,在本函数里你需要返回该元素的正确索引
  • parent:获取指定元素的父元素
  • rowCount: 获取指定元素的子节点个数(下一级行数)
  • columnCount: 获取指定元素的列数

下面是对比QStandardItemModel和自定义Model的初始化tree时间和占用内存(不设置model程序本身内存:15.4MB)对比:

Model类型初始化tree时间占用内存
QStandardItemModel1263ms96.9MB
自定义Model75ms20MB

程序示例图:

在这里插入图片描述

示例代码:

模拟数据数据结构:


/* 子节点 */
typedef struct _CHILD
{
    QString name;
    int number1;
    int number2;
    int number3;
    _CHILD()
    {
        name = "";
        number1 = number2 = number3 = 0;
    }
}CHILD, *PCHILDITEM;

/* 父节点 */
typedef struct _PARENT
{
    QString name;
    QVector<CHILD*> childs;
    _PARENT()
    {
        name = "";
    }
}PARENT;

模拟数据初始化:

 /* 10个父节点,每个父节点有1W个子节点,共10W行记录 */
    int nParent = 10;
    int nChild = 10000;
    for (int i = 0; i < nParent; i++)
    {
        PARENT* c = new PARENT;
        c->name = QString::fromLocal8Bit("父节点%1").arg(i);
        for (int j = 0; j < nChild; j++)
        {
            CHILD* s = new CHILD;
            s->name = QString::fromLocal8Bit("子节点%1").arg(j);
            s->number1 = 0;
            s->number2 = 1;
            s->number3 = 2;
            c->childs.append(s);
        }
        mDatas.append(c);
    }

模拟数据

QVector<PARENT*> mDatas;

常规QStandardItemModel构造tree代码:


 QStandardItemModel* model = new QStandardItemModel(ui.treeView);
    model->setHorizontalHeaderLabels(headers);
    foreach(PARENT* p, mDatas)
    {
        /* 一级节点:父节点 */
        QStandardItem* itemParent = new QStandardItem(p->name);
        model->appendRow(itemParent);

        foreach(CHILD* c, p->childs)
        {
            /* 二级节点:子节点 */
            QList<QStandardItem*> items;
            QStandardItem* item0 = new QStandardItem(c->name);
            QStandardItem* item1 = new QStandardItem(QString::number(c->number1));
            QStandardItem* item2 = new QStandardItem(QString::number(c->number2));
            QStandardItem* item3 = new QStandardItem(QString::number(c->number3));
            items << item0 << item1 << item2 << item3;
            itemParent->appendRow(items);
        }
    }

自定义Model构造tree代码:


MyModel* model = new MyModel(headers, ui.treeView);
    MyItem* root = model->root();
    foreach(PARENT* p, mDatas)
    {
        /* 一级节点:父节点 */
        MyItem* itemParent = new MyItem(root);

        /* 设为一级节点,供显示时判断节点层级来转换数据指针类型 */
        itemParent->setLevel(1);

        /* 保存PARENT* p为其数据指针,显示时从该PARENT*取内容显示 */
        itemParent->setPtr(p);
        root->appendChild(itemParent);

        foreach(CHILD* c, p->childs)
        {
            MyItem* itemChild = new MyItem(itemParent);

            /* 设为二级节点,供显示时判断节点层级来转换数据指针类型 */
            itemChild->setLevel(2);

            /* 保存CHILD* c为其数据指针,显示时从CHILD*取内容显示 */
            itemChild->setPtr(c);
            itemParent->appendChild(itemChild);
        }
    }

自定义Model MyModel.h代码


#include <QAbstractItemModel>
#include "MyItem.h"

class MyModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    MyModel(QStringList headers, QObject *parent = 0);
    ~MyModel();

    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
    QVariant data(const QModelIndex &index, int role) const override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &index) const override;
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

public:
    MyItem *itemFromIndex(const QModelIndex &index) const;
    MyItem *root();

private:
    QStringList mHeaders;   /* 表头内容 */
    MyItem *mRootItem;      /* 根节点 */
};

自定义Model MyModel.cpp


#include "MyModel.h"

MyModel::MyModel(QStringList headers, QObject *parent)
    : QAbstractItemModel(parent)
{
    mHeaders = headers;
    mRootItem = new MyItem;
}

MyModel::~MyModel()
{
    delete mRootItem;
}

MyItem *MyModel::itemFromIndex(const QModelIndex &index) const
{
    if (!index.isValid())
        return NULL;
    MyItem *item = static_cast<MyItem*>(index.internalPointer());
    return item;
}

MyItem *MyModel::root()
{
    return mRootItem;
}

QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (orientation == Qt::Horizontal)
    {
        if (role == Qt::DisplayRole)
        {
            return mHeaders.at(section);
        }
    }
    return QVariant();
}

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    MyItem *item = static_cast<MyItem*>(index.internalPointer());
    if (role == Qt::DisplayRole)
    {
        return item->data(index.column());
    }
    return QVariant();
}

Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return 0;

    return QAbstractItemModel::flags(index);
}

QModelIndex MyModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent))
        return QModelIndex();

    MyItem *parentItem;

    if (!parent.isValid())
        parentItem = mRootItem;
    else
        parentItem = static_cast<MyItem*>(parent.internalPointer());

    MyItem *childItem = parentItem->child(row);
    if (childItem)
        return createIndex(row, column, childItem);
    else
        return QModelIndex();
}

QModelIndex MyModel::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return QModelIndex();

    MyItem *childItem = static_cast<MyItem*>(index.internalPointer());
    MyItem *parentItem = childItem->parentItem();

    if (parentItem == mRootItem)
        return QModelIndex();

    return createIndex(parentItem->row(), 0, parentItem);
}

int MyModel::rowCount(const QModelIndex &parent) const
{
    MyItem *parentItem;
    if (parent.column() > 0)
        return 0;

    if (!parent.isValid())
        parentItem = mRootItem;
    else
        parentItem = static_cast<MyItem*>(parent.internalPointer());

    return parentItem->childCount();
}

int MyModel::columnCount(const QModelIndex &parent) const
{
    return mHeaders.size();
}

MyItem.h


#include <QVariant>

class MyItem
{
public:
    explicit MyItem(MyItem *parentItem = 0);
    ~MyItem();

    void appendChild(MyItem *child);      //在本节点下增加子节点
    void removeChilds();                    //清空所有节点

    MyItem *child(int row);               //获取第row个子节点指针
    MyItem *parentItem();                 //获取父节点指针
    int childCount() const;                 //子节点计数
    int row() const;                        //获取该节点是父节点的第几个子节点

    //核心函数:获取节点第column列的数据
    QVariant data(int column) const;

    //设置、获取节点是几级节点(就是树的层级)
    int level(){ return mLevel; }
    void setLevel(int level){ mLevel = level; }

    //设置、获取节点存的数据指针
    void setPtr(void* p){ mPtr = p; }
    void* ptr(){ return mPtr; }

    //保存该节点是其父节点的第几个子节点,查询优化所用
    void setRow(int row){
        mRow = row;
    }

private:
    QList<MyItem*> mChildItems;   //子节点
    MyItem *mParentItem;          //父节点
    int mLevel;     //该节点是第几级节点
    void* mPtr;     //存储数据的指针
    int mRow;       //记录该item是第几个,可优化查询效率
};

MyItem.cpp


#include "MyItem.h"
#include "defines.h"

MyItem::MyItem(MyItem *parent)
{
    mParentItem = parent;
    mPtr = NULL;
    mLevel = 0;
    mRow = 0;
}

MyItem::~MyItem()
{
    removeChilds();
}

void MyItem::appendChild(MyItem *item)
{
    item->setRow(mChildItems.size());   //item存自己是第几个,可以优化效率
    mChildItems.append(item);
}

void MyItem::removeChilds()
{
    qDeleteAll(mChildItems);
    mChildItems.clear();
}

MyItem *MyItem::child(int row)
{
    return mChildItems.value(row);
}

MyItem *MyItem::parentItem()
{
    return mParentItem;
}

int MyItem::childCount() const
{
    return mChildItems.count();
}

int MyItem::row() const
{
    return mRow;
}

QVariant MyItem::data(int column) const
{
    if (mLevel == 1)
    {
        /* 一级节点: 父节点 */
        if (column == 0)
        {
            PARENT* c = (PARENT*)mPtr;
            return c->name;
        }
    }
    else if (mLevel == 2)
    {
        /* 二级节点: 子节点 */
        CHILD* s = (CHILD*)mPtr;
        switch (column)
        {
        case 0: return s->name;
        case 1: return QString::number(s->number1);
        case 2: return QString::number(s->number2);
        case 3: return QString::number(s->number3);
        default:
            return QVariant();
        }
    }
    return QVariant();
}
  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
QTreeViewQt框架中的一个控件,用于显示树形结构的数据。QScrollBar是Qt框架中的一个控件,用于显示滚动条。 在给QTreeView添加QScrollBar时,可以通过判断QTreeView的垂直滚动条是否可见来确定是否需要进行补偿。如果垂直滚动条可见,可以使用rect.setRight(rect.right() - tree->verticalScrollBar()->width())来补偿宽度,使得绘制的内容不会被滚动条遮挡。 在自定义派生类MyTreeDelegate的paint函数中,可以通过获取QTreeView的垂直滚动条宽度,判断是否可见,并根据需要进行补偿操作。然后使用painter->drawText来绘制文本内容,实现显示rect.right的功能。最后调用QStyledItemDelegate::paint来完成绘制。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [QTreeView 一个竖直滚动条引起的问题](https://blog.csdn.net/luoshabugui/article/details/103393021)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [C++ QT5开发教程](https://download.csdn.net/download/prickly/9673714)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值