前言

王超的独立博客

Qt-SerialDebuger

Model/View(模型/视图)结构

Model/View(模型/视图)结构是 Qt 中用界面组件显示与编辑数据的一种结构,视图(View)是显示和编辑数据的界面组件,模型(Model)是视图与原始数据之间的接口

将界面组件与所编辑的数据分离开来,又通过数据源的方式连接起来,是处理界面与数据的一种较好的方式。Qt 使用 Model/View 结构来处理这种关系

代理功能可以让用户定制数据的界面显示和编辑方式。在标准的视图组件中,代理功能显示一个数据,当数据被编辑时,代理通过模型索引与数据模型通信,并为编辑数据提供一个编辑器,一般是一个 QLineEdit 组件

模型、视图和代理之间使用信号和槽通信。当源数据发生变化时,数据模型发射信号通知视图组件;当用户在界面上操作数据时,视图组件发射信号表示这些操作信息;当编辑数据时,代理发射信号告知数据模型和视图组件编辑器的状态

  • 数据(Data)是实际的数据,如数据库的一个数据表或SQL查询结果,内存中的一个 StringList,或磁盘文件结构等。
  • 视图或视图组件(View)是屏幕上的界面组件,视图从数据模型获得每个数据项的模型索引(model index),通过模型索引获取数据,然后为界面组件提供显示数据。Qt 提供一些现成的数据视图组件,如 QListView、QTreeView 和 QTableView 等。
  • 模型或数据模型(Model)与实际数据通信,并为视图组件提供数据接口。它从原始数据提取需要的内容,用于视图组件进行显示和编辑。Qt 中有一些预定义的数据模型,如 QStringListModel 可作为 StringList 的数据模型, QSqlTableModel 可以作为数据库中一个数据表的数据模型。

数据模型

Model 类 用途
QStringListModel 用于处理字符串列表数据的数据模型类
QStandardltemModel 标准的基于项数据的数据模型类,每个项数据可以是任何数据类型
QFileSy stemModel 计算机上文件系统的数据模型类
QSortFilterProxyModel 与其他数据模型结合,提供排序和过滤功能的数据模型类
QSqlQueryModel 用于数据库SQL查询结果的数据模型类
QSqlTableModel 用于数据库的一个数据表的数据模型类
QSqlRelationalTableModel 用于关系型数据表的数据模型类

视图组件

视图组件(View)就是显示数据模型的数据的界面组件,Qt 提供的视图组件如下:

  • QListView:用于显示单列的列表数据,适用于一维数据的操作。
  • QTreeView:用于显示树状结构数据,适用于树状结构数据的操作。
  • QTableView:用于显示表格状数据,适用于二维表格型数据的操作。
  • QColumnView:用多个QListView显示树状层次结构,树状结构的一层用一个QListView显示。
  • QHeaderView:提供行表头或列表头的视图组件,如QTableView的行表头和列表头。

前面介绍了 QListWidget、QTreeWidget 和 QtableWidget 3个可用于数据编辑的组件。这 3 个类称为便利类(convenience classes),它们分别是 3 个视图类的子类

结构的一些概念

【图1】

上图数据模型的 3 种常见表现形式。不管数据模型的表现形式是怎么样的,数据模型中存储数据的基本单元都是项(item),每个项有一个行号、一个列号,还有一个父项。在列表和表格模式下,所有的项都有一个相同的顶层项;在树状结构中,行号、列号、父项稍微复杂一点,但是由这 3 个参数完全可以定义一个项的位置,从而存取项的数据

模型索引(model index)

为了保证数据的表示与数据存取方式隔离,数据模型中引入了模型索引的概念。通过数据模型存取的每个数据都有一个模型索引,视图组件和代理都通过模型索引来获取数据。

QModelIndex 表示模型索引的类。模型索引提供数据存取的一个临时指针,用于通过数据模型提取或修改数据。因为模型内部组织数据的结构随时可能改变,所以模型索引是临时的。如果需要使用持久性的模型索引,则要使用 QPersistentModelIndex 类。

行号和列号

数据模型的基本形式是 用行和列定义的表格数据,但这并不意味着底层的数据是用二维数组存储的,使用行和列只是为了组件之间交互方便的一种规定。通过模型索引的行号和列号就可以存取数据。

要获得一个模型索引,必须提供 3 个参数: 行号、列号、父项的模型索引。例如,对于如图 5 中的表格数据模型中的 3 个数据项 A、B、C,获取其模型索引的代码是:

cpp
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());

在创建模型索引的函数中需要传递行号、列号和父项的模型索引。对于列表和表格模式的数据模型,顶层节点总是用 QModelIndex() 表示。

父项

当数据模型是 列表或表格时,使用行号、列号存储数据比较直观,所有数据项的父项就是顶层项;当数据模型是树状结构时,情况比较复杂(树状结构中,项一般习惯于称为节点), 一个节点可以有父节点,也可以是其他节点的父节点,在构造数据项的模型索引时,必须指定正确的行号、列号和父节点。

对于上面图1 中的树状数据模型,节点 A 和节点 C 的父节点是顶层节点,获取模型索引的代码是:

cpp
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());

但是,节点 B 的父节点是节点 A,节点 B 的模型索引由下面的代码生成:

cpp
QModelIndex indexB = model->index(1, 0, indexA);

项的角色

在为数据模型的一个项设置数据时,可以赋予其不同项的角色的数据。例如,数据模型类 QStandardItemModel 的项数据类是 QStandardItem,其设置数据的函数是:

void QStandardItem::setData(const QVariant &value, int role= Qt::UserRole + 1)

其中,value 是需要设置的数据,role 是设置数据的角色。一个项可以有不同角色的数据,用于不同的场合。

role 是 Qt::ItemDataRole 枚举类型,有多种取值,如 Qt::DisplayRole 角色是在视图组件中显示的字符串,Qt::ToolTipRole 是鼠标提示消息,Qt::UserRole 可以自定义数据。项的标准角色是 Qt::DisplayRole

在获取一个项的数据时也需要指定角色,以获取不同角色的数据:

QVariant QStandardItem::data(int role = Qt::UserRole + 1) const

为一个项的不同角色定义数据,可以告知视图组件和代理组件如何显示数据。例如,在图 6 中,项的 DisplayRole 数据是显示的字符串,DecorationRole 是用于装饰显示的属性,ToolTipRole 定义了鼠标提示信息。不同的视图组件对各种角色数据的解释和显示可能不一样,也可能忽略某些角色的数据。

项目控件组(Item Widgets)

  • 列表控件(ListWidgt)
  • 树型控件(TreeWidget)
  • 表格控件(TableWidget)

QListView、QStandardItemModel、QStandardItem

cpp
#include <QApplication>
#include<QWidget>
#include "widget.h"

#include<QStandardItemModel>	//标准的数据模型
#include<QListView>				//列表视图

#ifdef _DEBUG
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#endif // _DEBUG

int main(int argc, char* argv[])
{
	QApplication a(argc, argv);

	//1,创建模型
	// 创建一个QStandardItemModel对象,并设置其父对象为qApp(应用程序对象)
	auto model = new QStandardItemModel(qApp);

	//2,创建视图
	// 创建一个QListView对象,用于显示数据
	QListView view;

	//3,把模型交给视图进行显示
	// 将模型设置到视图中,以便在视图中显示数据
	view.setModel(model);
	view.show();

	//4,给模型添加数据
	model->appendRow(new QStandardItem("玩蛇"));	// 向模型的末尾添加一行
	model->insertRow(0, new QStandardItem("莫影"));	// 在索引0处插入一行即
	model->insertRow(1, new QStandardItem("mew"));	// 在索引1处插入一行

	model->setItem(0, new QStandardItem("里奇"));	// 将索引0处的项目替换为一个新的QStandardItem对象
	model->setItem(3, new QStandardItem("白榆"));	// 将索引3处的项目替换为一个新的QStandardItem对象

	qDebug() << model->rowCount() << model->columnCount();		// 打印 获取了模型中的行数和列数

	//5,删除item(从模型中移除,并释放内存)
	//model->removeRow(1);
	//model->removeRows(0, 2);	// 删除行(如果越界则不会生效此操作)

	//6,从模型中移除item,但是不释放item 需要手动释放内存
	auto item_list = model->takeRow(2);
	qDebug() << item_list.size();
	for (auto item : item_list)
	{
		delete item;
	}

	//7,item索引
	auto item = new QStandardItem("six");
	model->appendRow(item);	

	QModelIndex idx = model->indexFromItem(item);	// 以从模型中的项目(QStandardItem)获取索引
	qDebug() << "idx" << idx;	//  QModelIndex(3,0,0x1d19f18e6c0,QStandardItemModel(0x1d19f18e8f0))

	//8,角色
	model->setData(idx, "6666", Qt::ItemDataRole::DisplayRole);	// 将字符串 "6666" 设置为索引 idx 的数据 该数据是用于显示的角色
	item->setData(QColor(245, 204, 132), Qt::ItemDataRole::DecorationRole);		// 将颜色设置为 QColor(245, 204, 132),并将其设置为项目的 DecorationRole 角色
	item->setData(17363691111, Qt::ItemDataRole::UserRole);	// 将一个 unsigned long long 类型的值(17363691111)设置为项目的 UserRole 角色

	qDebug() << item->data(Qt::UserRole).toULongLong();	// 输出项目的 UserRole 角色所设置的数据,即上一行代码中设置的 unsigned long long 类型的值

	item->setCheckable(true);	// 用于设置项目是否可选中(checkable)

	// 连接 QListView 的 clicked 信号到一个 lambda 函数中。该信号在用户点击视图中的项目时发出。
	QObject::connect(&view, &QListView::clicked, [=](const QModelIndex& idx)
		{
			// debug 输出被点击项的数据,包括 DisplayRole 和 text() 方法返回值。
			qDebug() << idx.data(Qt::DisplayRole).toString() << "\n"
				<< model->item(idx.row(), idx.column())->text(); 
		});


	return a.exec();
}

示例-QQ联系人(Widget方式)

main.cpp
cpp
#include <QApplication>
#include<QWidget>
#include "widget.h"

#include<QStandardItemModel>	//标准的数据模型
#include<QListView>				//列表视图
#include"ContactView.h"

#ifdef _DEBUG
#pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#endif // _DEBUG

int main(int argc, char* argv[])
{
	QApplication a(argc, argv);

	ContactView v;
	v.show();

	return a.exec();
}
Contact.h
cpp
#ifndef CONTACT_H_    // 预处理指令,防止头文件重复包含
#define CONTACT_H_

#include<QString>          // 包含 QString 类的头文件
#include<qrandom.h>       // 包含 QRandomGenerator 类的头文件

struct Contact
{
    enum Type { NONE, VIP, SVIP };        // 定义枚举类型 Type,表示账户类型,包括 NONE、VIP、SVIP

    Contact(const QString& username, const QString& nickname)
        :username(username)                        // 构造函数,接受用户名和昵称作为参数
        , nickname(nickname)
        , profilePath(":/images/defaultProfile.png")    // 头像路径的初始化值
        , signatrue("这人很懒,啥都没写~")                          // 签名的初始化值
        , type(QRandomGenerator::global()->bounded(3))            // 随机生成账户类型的值,范围为 0-2(包括0不包括3)
    {

    }

    QString profilePath;    // 头像的路径
    QString username;       // 用户名
    QString nickname;       // 备注名
    QString signatrue;      // 签名
    int type;               // 账户类型
};

#endif // !CONTACT_H_
ContactItem.h
cpp
#ifndef CONTACTITEM_H_    // 预处理指令,防止头文件重复包含
#define CONTACTITEM_H_

#include"Contact.h"         // 包含 Contact 结构体的头文件
#include <QWidget>          // 包含 QWidget 类的头文件
#include <QStandardItem>    // 包含 QStandardItem 类的头文件
#include<QLabel>

// ---- 可声明类也可以直接包含头文件
class QLabel;               // 前向声明 QLabel 类
class QPushButton;         // 前向声明 QPushButton 类

class ContactItem : public QWidget, public QStandardItem   // 定义 ContactItem 类,继承自 QWidget 和 QStandardItem
{
public:
    ContactItem(Contact* contact, QWidget* parent = nullptr);   // 构造函数,接受 Contact 指针和 QWidget 父类指针作为参数

private:
    void iniUi();    // 初始化 UI
    void updateContactDisplay();    // 更新联系人的显示

    QLabel* m_profileLab{};      // 头像 QLabel 控件指针
    QLabel* m_nicknameLab{};     // 备注名 QLabel 控件指针
    QLabel* m_usernameLab{};     // 用户名 QLabel 控件指针
    QLabel* m_typeLab{};         // 账户类型 QLabel 控件指针
    QLabel* m_signatureLab{};    // 签名 QLabel 控件指针

    Contact* m_contact{};        // 指向 Contact 结构体的指针
};

#endif // !CONTACTITEM_H_
ContactItem.cpp
cpp
#include "ContactItem.h"
#include <QLabel>
#include <QPushButton>
#include <QGridLayout>

ContactItem::ContactItem(Contact* contact, QWidget* parent)
	: QWidget(parent), m_contact(contact)
{
	this->setData(QVariant::fromValue(m_contact), Qt::UserRole);  // 将 m_contact 保存到用户数据中
	iniUi();    // 初始化 UI
	updateContactDisplay();    // 更新联系人显示
}

void ContactItem::iniUi()
{
	setSizeHint(QSize(width(), 60));      // 设置控件的大小提示为宽度和60像素的大小
	setFixedHeight(60);     // 设置控件的固定高度为60像素

	m_profileLab = new QLabel;      // 创建头像 QLabel 控件
	m_profileLab->setFixedSize(40, 40);   // 设置头像控件的固定大小为40x40像素
	m_profileLab->setScaledContents(true);   // 设置头像控件的内容自适应缩放

	m_nicknameLab = new QLabel;   // 创建备注名 QLabel 控件
	m_usernameLab = new QLabel;   // 创建用户名 QLabel 控件
	m_typeLab = new QLabel;       // 创建账户类型 QLabel 控件
	m_signatureLab = new QLabel;  // 创建签名 QLabel 控件

	auto hlayout = new QHBoxLayout;   // 创建水平布局管理器
	hlayout->addWidget(m_usernameLab);   // 将用户名控件添加到布局中
	hlayout->addWidget(m_nicknameLab);   // 将备注名控件添加到布局中
	hlayout->addWidget(m_typeLab);       // 将账户类型控件添加到布局中
	hlayout->addStretch();                // 添加一个可伸缩的空白部分

	auto glayout = new QGridLayout(this);   // 创建网格布局管理器,并将当前控件设置为其父控件
	glayout->addWidget(m_profileLab, 0, 0, 2, 1);   // 将头像控件添加到网格布局中,并指定其在第一行第一列,占用两行一列
	glayout->addLayout(hlayout, 0, 1);    // 将水平布局添加到网格布局中,并指定其在第一行第二列
	glayout->addWidget(m_signatureLab, 1, 1);   // 将签名控件添加到网格布局中,并指定其在第二行第二列
}

void ContactItem::updateContactDisplay()
{
	if (!m_contact)
		return;

	m_profileLab->setPixmap(QPixmap(m_contact->profilePath));   // 设置头像控件的图片为联系人的头像路径
	m_usernameLab->setText(m_contact->username);   // 设置用户名控件的文本为联系人的用户名
	m_nicknameLab->setText(m_contact->nickname);   // 设置备注名控件的文本为联系人的备注名
	m_signatureLab->setText(m_contact->signatrue);  // 设置签名控件的文本为联系人的签名

	switch (m_contact->type)
	{
	case Contact::NONE:
		break;
	case Contact::VIP:
		m_typeLab->setPixmap(QPixmap(":/images/vip.png"));   // 设置账户类型控件的图片为 VIP 图标
		break;
	case Contact::SVIP:
		m_typeLab->setPixmap(QPixmap(":/images/svip.png"));  // 设置账户类型控件的图片为 SVIP 图标
		break;
	default:
		break;
	}
}
ContactView.h
cpp
#ifndef CONCATVIEW_H  // 条件编译指令,防止头文件重复包含
#define CONCATVIEW_H

#include<QListView>  // 包含 QListView 类的头文件
#include<memory>  // 包含内存管理相关的头文件,用于管理动态分配的对象
#include<QStandardItemModel>  // 包含 QStandardItemModel 类的头文件
#include"ContactItem.h"  // 包含 ContactItem 类的头文件

class ContactView : public QListView  // 定义 ContactView 类,继承自 QListView
{
public:
	ContactView(QWidget* parent = nullptr);  // 构造函数,接收一个 QWidget 父类指针作为参数
public:
	void onClicked(const QModelIndex& index);  // 槽函数,当用户单击某个项时触发,接收一个 QModelIndex 对象作为参数
private:
	void initUi();  // 初始化 UI 的函数
	std::vector<std::unique_ptr<Contact>> m_contacts;  // 使用智能指针 std::unique_ptr 管理 Contact 对象的动态分配
	QStandardItemModel* m_model{};  // 指向 QStandardItemModel 对象的指针,用于设置当前视图的数据模型
};

#endif // !CONCATVIEW_H  // 条件编译指令结束,防止头文件重复包含
ContactView.cpp
cpp
#include "ContactView.h"

ContactView::ContactView(QWidget* parent)
	:QListView(parent)  // 调用父类的构造函数来初始化控件
	, m_model(new QStandardItemModel(this))  // 创建 QStandardItemModel 类型的对象,并将该对象保存到 m_model 中
{
	setModel(m_model);  // 将 QStandardItemModel 对象设置为当前视图的数据模型
	initUi();   // 初始化 UI
	connect(this, &QListView::clicked, this, &ContactView::onClicked);  // 建立信号与槽的连接,当用户单击某个项时触发对应的槽函数
}

void ContactView::onClicked(const QModelIndex& index)
{
	Contact* c = index.data(Qt::UserRole).value<Contact*>();  // 获取选中项的用户数据,即 Contact 类型的指针
	if (!c)  // 如果获取失败,则输出警告信息并返回
	{
		//qWarning() << "联系人信息获取失败";
		return;
	}

	qDebug() << c->username << c->nickname << c->signatrue;  // 输出选中联系人的用户名、备注名和签名
}


void ContactView::initUi()
{
	//准备联系人信息
	m_contacts.emplace_back(new Contact("十五期 卷心菜", "Hello"));  // 创建 Contact 对象,保存到联系人列表中
	m_contacts.emplace_back(new Contact("十五期 余香", "余香"));
	m_contacts.emplace_back(new Contact("vip10啊·哈", "啊·哈"));
	m_contacts.emplace_back(new Contact("DK八期vip", "Dk"));
	m_contacts.emplace_back(new Contact("man~", ""));

	/*
	for (size_t i = 0; i < 20000; i++)
	{
		m_contacts.emplace_back(new Contact(QString("man~ %1").arg(i), ""));
	}
	*/

	for (auto& c : m_contacts)  // 遍历联系人列表
	{
		auto itemWidget = new ContactItem(c.get());  // 创建 ContactItem 对象,并传入对应的 Contact 指针
		m_model->appendRow(itemWidget);   // 将 ContactItem 对象添加到数据模型中

		this->setIndexWidget(itemWidget->index(), itemWidget);  // 设置 ContactItem 对象所在项的视图部件
	}
}

示例-QQ联系人(代理方式)

在上面基础上修改,删除 ContactItem.cppContactItem.h

ContactView.h 只需要包含头文件即可

cpp
#include"Contact.h"
ContactView.cpp
cpp
#include "ContactView.h"
#include "ContactDelegate.h"

ContactView::ContactView(QWidget* parent)
    :QListView(parent)
    ,m_model(new QStandardItemModel(this))
{
    setModel(m_model);  // 设置当前视图的数据模型为 m_model
    setItemDelegate(new ContactDelegate(this));  // 给每一项设置代理,用于自定义项的显示和交互
    setEditTriggers(QListView::NoEditTriggers);  // 设置编辑策略,禁止编辑
    setMouseTracking(true);  // 设置鼠标追踪,使得在鼠标移动时能够捕捉到相应事件
    initUi();  // 初始化 UI
    connect(this, &QListView::clicked, this, &ContactView::onClicked);  // 连接 clicked 信号和槽函数 onClicked
    setFixedSize(340, 480);  // 设置 ContactView 的固定大小为 340x480
}

void ContactView::onClicked(const QModelIndex& index)
{
    Contact* c = index.data(Qt::UserRole).value<Contact*>();  // 获取选中项的用户数据,即 Contact 对象
    if (!c)
    {
        qWarning() << "联系人信息获取失败";
        return;
    }
    qDebug() << c->username << c->nickname << c->signatrue;  // 打印 Contact 对象的用户名、昵称和个性签名
}

void ContactView::initUi()
{
    // 准备联系人信息
    m_contacts.emplace_back(new Contact("十五期 卷心菜", "Hello"));
    m_contacts.emplace_back(new Contact("十五期 余香", "余香"));
    m_contacts.emplace_back(new Contact("vip10啊·哈", "啊·哈"));
    m_contacts.emplace_back(new Contact("DK八期vip", "Dk"));
    m_contacts.emplace_back(new Contact("man~", ""));

    /*
    for (size_t i = 0; i < 20000; i++)
    {
        m_contacts.emplace_back(new Contact(QString("man~ %1").arg(i), ""));
    }
    */

    // 遍历联系人列表,创建并添加 QStandardItem 到数据模型 m_model 中
    for (auto& c : m_contacts)
    {
        auto item = new QStandardItem(c->username);  // 创建 QStandardItem 对象,设置项的显示文本为联系人的用户名
        item->setData(QVariant::fromValue(c.get()), Qt::UserRole);  // 将 Contact 对象作为用户数据设置到项中
        m_model->appendRow(item);  // 将项添加到数据模型的末尾
    }
}
ContactDelegate.h
cpp
#ifndef CONTACTDELEGATE_H_
#define CONTACTDELEGATE_H_

#include <QStyledItemDelegate>
#include <QPoint>

class ContactDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    ContactDelegate(QObject *parent = nullptr);

protected:
    // 重写 paint() 函数,用于绘制列表项
    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;

    // 重写 sizeHint() 函数,用于设置列表项的大小
    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;

    // 重写 editorEvent() 函数,处理列表项的交互事件
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override;

signals:
    // 当类别按钮被点击时,发送 typeClicked 信号
    void typeClicked();

private:
    QPoint m_mousePos{};  // 鼠标位置信息,用于记录鼠标在列表项中的位置
    mutable QRect m_typeRect{};  // 类别按钮所在的矩形区域,用于记录类别按钮的位置
};

#endif // !CONTACTDELEGATE_H_
ContactDelegate.cpp
cpp
#include "ContactDelegate.h"
#include "Contact.h"
#include <QStyleOptionViewItem>
#include <QPainter>
#include <QStaticText>
#include <QEvent>
#include <QMouseEvent>

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

void ContactDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    // 获取联系人数据
    auto c = index.data(Qt::UserRole).value<Contact *>();
    if (!c)
    {
        // 如果数据为空,调用默认的绘制方法
        QStyledItemDelegate::paint(painter, option, index);
        return;
    }

    // 获取绘制区域
    auto rect = option.rect;

    // 设置鼠标悬停和选中时的背景颜色
    QColor color = Qt::white;
    if (option.state & QStyle::State_MouseOver)
        color = QColor(242, 242, 242);
    if (option.state & QStyle::State_Selected)
        color = QColor(235, 235, 235);

    // 填充背景颜色
    painter->fillRect(rect, color);

    // 绘制头像
    QRect profileRect = {rect.x() + 14, rect.y() + 8, 40, 40};
    painter->drawPixmap(profileRect, QPixmap(c->profilePath));

    // 绘制备注
    QRect nRect = {profileRect.right() + 10, rect.y() + 15, 0, 0};

    auto fm = painter->fontMetrics();
    nRect.setWidth(fm.horizontalAdvance(c->username));
    nRect.setHeight(fm.height());

    // 绘制用户名
    painter->drawStaticText(nRect.topLeft(), QStaticText(c->username));

    // 绘制昵称
    QRect uRect = {nRect.right() + 1, nRect.y(), 0, 0};
    uRect.setWidth(fm.horizontalAdvance(c->nickname));
    uRect.setHeight(fm.height());
    painter->drawStaticText(uRect.topLeft(), QStaticText("(" + c->nickname + ")"));

    // 绘制类型图标
    QPixmap typePixmap;
    if (c->type == Contact::VIP)
        typePixmap.load(":/Resource/images/vip.png");
    else if (c->type == Contact::SVIP)
        typePixmap.load(":/Resource/images/svip.png");
    m_typeRect = {uRect.topRight() + QPoint(10, 5), typePixmap.size()};
    painter->drawPixmap(m_typeRect, typePixmap);

    // 检测鼠标是否在类型图标上方
    if (m_typeRect.contains(m_mousePos))
    {
        // 如果鼠标在类型图标上方,绘制红色边框
        painter->save();
        painter->setPen(Qt::red);
        painter->drawRect(m_typeRect);
        painter->restore();
    }

    // 绘制个性签名
    QPoint sPos = {nRect.left(), nRect.bottom() + 5};
    painter->drawStaticText(sPos, QStaticText(c->signature));
}

QSize ContactDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    // 设置列表项的大小
    return QSize(option.rect.width(), 60);
}

bool ContactDelegate::editorEvent(QEvent *ev, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index)
{
    if (ev->type() == QEvent::MouseMove)
    {
        // 处理鼠标移动事件,记录鼠标位置
        auto mev = static_cast<QMouseEvent *>(ev);
        m_mousePos = mev->pos();
        return true;
    }
    else if (ev->type() == QEvent::MouseButtonRelease)
    {
        // 处理鼠标释放事件,如果鼠标在类型图标上方并且左键释放,则发送类型点击信号
        auto mev = static_cast<QMouseEvent *>(ev);
        if (mev->button() == Qt::LeftButton && m_typeRect.contains(m_mousePos))
        {
            emit typeClicked();
        }
    }

    return false;
}

文件系统模型

待学

CMAKE简单入门

待学

多线程

Widgets 必须创建在主线程(GUI)

不能在子线程中操作ui相关的东西,信号和槽(没有问题,而这也是最好的方式了)

三种线程创建方式

QThread::create

cpp
[static] QThread *QThread::create(Function &&f, Args &&... args)	//C++17

【注意】

调用者获得返回的QThread实例的所有权。也就是说,需要自己释放线程对象

不要对返回的QThread实例多次调用start(); 这样做将导致未定义的行为

cpp
#include <QApplication>
#include<QWidget>
#include "widget.h"
#include<Qthread>
#include<QLabel>

#ifdef _DEBUG
    #pragma comment(linker,"/subsystem:console /entry:mainCRTStartup")
#endif // _DEBUG

class Test : public QWidget
{
    Q_OBJECT
public:
    Test()
        : m_lab(new QLabel("<h1>xxxx</h1>", this))
    {
        resize(640, 480);
        // 两个线程
        test_create();
        create_1();
    }
    void test_create()
    {
        //通过QThread::create创建线程
        QThread *thr =  QThread::create([ = ]()
        {
            int i = 0;

            while (i < 1000)
            {
                qDebug() << "sub thread" << QThread::currentThread();
                //1,不能在子线程中操作ui相关的东西
                // m_lab->setText(QString("<h1>%1</h1>").arg(i++));

                // 2. 信号和槽(没有问题,而这也是最好的方式了)
                emit textChanged(QString("<h1>%1</h1>").arg(i++));

                // 3. 使用invoke函数来执行settext函数 -- 直连调的话也会卡死,队列调的话不会
                // if (QMetaObject::invokeMethod(m_lab, "setText",
                //                               Q_ARG(QString, QString("<h1>%1</h1>").arg(i++))))
                // {
                //     qDebug() << "调用成功!";
                // }
            }
        });
        //启动线程
        thr->start();
        // 查询线程是否自动释放,答案是没有的
        connect(thr, &QThread::destroyed, []()
        {
            qDebug() << "thr destroyed";
        });
        // 判断线程完成信号,然后进行释放
        connect(thr, &QThread::finished, [ = ] {thr->deleteLater(); qDebug() << "thr finished"; });
        connect(this, &Test::textChanged, m_lab, &QLabel::setText); // 连接
    }
    void create_1()
    {
        QThread::create(&Test::worker, this)->start();  // 启动
    }
    void worker()
    {
        while (true)
        {
            qDebug() << __FUNCTION__;
        }
    }
signals:
    void textChanged(const QString &text);
private:
    QLabel *m_lab{};
};


int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Test w;
    w.show();
    return a.exec();
}

#include "main.moc"

继承QThread,重写run

QThread类中有一个virtual函数QThread::run(),要创建一个新的线程,我们只需定义一个MyThread类,让其继承QThread,然后重新实现QThread::run()。把需要在线程中执行的代码全部塞到run函数中

run函数是线程的起始点。 在调用start()之后,新创建的线程调用这个函数。 默认实现只是调用exec(),用来处理线程中的事件

cpp
/*
* 必须继承QRunable类,实现run接口(把你需要在线程中处理的逻辑放到run中)
*/
class Worker :public QThread 
{
	Q_OBJECT
protected:
	void run() override
	{
		quint64 sum = 0;
		for (size_t i = 0; i <100; i++)
		{
			sum += i;
			QThread::msleep(1);
		}
		emit calcFinished(sum);
	}

signals:
	void calcFinished(quint64 sum);
};

int main()
{
    Worker w;
	//1,不能手动调用run函数,否则就不是在线程里里面处理
	//w.run();
	//2,必须调用start函数,让run在线程里面跑
	w.start();
}

QObject::moveToThread

QObject 中的 moveToThread() 函数可以在不破坏类结构的前提下依然可以在新线程中运行

cpp
#include "widget.h"
#include<QApplication>
#include<QWidget>
#include<QPlainTextEdit>
#include<qrandom.h>
#include<QTextStream>
#include<QBoxLayout>
#include<QThread>
#include <QApplication>

QString randText()
{
    int length = QRandomGenerator::global()->bounded(50, 100);

    QString str;
    QTextStream stream(&str);
    for (size_t i = 0; i < length; i++)
    {
        if (i % 2 == 0)
        {
            stream << (QRandomGenerator::global()->bounded(0, 26) + 'a');
        }
        else
        {
            stream << QRandomGenerator::global()->bounded(9);
        }
    }
    return str;
}

class widget : public QWidget
{
public:
    widget(QWidget *parent = nullptr)
        : QWidget(parent)
        , m_edit(new QPlainTextEdit(this))
    {
        auto layout = new QHBoxLayout(this);
        layout->addWidget(m_edit);

    }
    ~widget()
    {
        qDebug() << __FUNCTION__;
    }

    void recvData()const
    {
        for (size_t i = 0; i < 1000; i++)
        {
            QMetaObject::invokeMethod(m_edit, "insertPlainText", Q_ARG(QString, randText() + "\n"));
            QThread::msleep(1);
        }
    }
private:
    QPlainTextEdit *m_edit{};
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    widget w;
    w.show();

    QThread thr;
    //1,把需要在线程中工作的对象移动到线程中
    w.moveToThread(&thr);	//不能把Widget移动到子线程中
    //2,关联需要在线程中掉用的函数
    QObject::connect(&thr, &QThread::started, &w, &widget::recvData);	//所以此时这里在主线程中执行的
    //3,开启线程
    thr.start();
    return a.exec();
}

线程绘图

main.cpp
cpp
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
mainwindow.h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QWidget>
#include "MyWorker.h" // 包含自定义的MyWorker类头文件
#include <QThread> // 包含QThread类头文件

QT_BEGIN_NAMESPACE
namespace Ui {
    class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr); // 构造函数,接受一个QWidget类型的父窗口参数,默认为nullptr
    ~MainWindow(); // 析构函数

protected:
    void paintEvent(QPaintEvent *ev) override; // 重写paintEvent函数,用于绘制窗口的内容

private:
    Ui::MainWindow *ui; // 用户界面类指针
    MyWorker worker; // 自定义的MyWorker类对象
    QThread thr; // QThread对象

    QImage m_image; // QImage对象,用于存储图像数据
};
#endif // MAINWINDOW_H
mainwindow.cpp
cpp
#include "mainwindow.h" // 包含自定义的MainWindow类头文件
#include "./ui_mainwindow.h" // 包含自动生成的ui文件的头文件
#include <QPushButton> // 包含QPushButton类头文件
#include <QPainter> // 包含QPainter类头文件

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow) // 创建一个Ui::MainWindow对象,用于设置用户界面
{
    ui->setupUi(this); // 设置用户界面,将UI组件添加到主窗口中
    auto btn = new QPushButton("绘图", this); // 创建一个名为"绘图"的按钮,父对象为当前窗口

    // 将产生图片的工作类移动到线程中
    worker.moveToThread(&thr);

    // 当按钮被点击时,连接到worker对象的drawImage槽函数,用于触发绘图操作
    connect(btn, &QPushButton::clicked, &worker, &MyWorker::drawImage);

    thr.start(); // 启动线程

    // 当worker对象发出newImage信号时,使用lambda表达式进行处理
    connect(&worker, &MyWorker::newImage, [=](const QImage &img) {
        m_image = img; // 将接收到的图像赋值给成员变量m_image
        update(); // 更新窗口内容,触发paintEvent函数进行重绘
    });
}

MainWindow::~MainWindow()
{
    delete ui; // 删除ui对象
}

void MainWindow::paintEvent(QPaintEvent *ev)
{
    QPainter painter(this); // 创建一个QPainter对象,用于绘制窗口内容
    painter.drawImage(0, 0, m_image); // 在窗口上绘制图像,位置为(0,0)
}
MyWorker.h
cpp
#ifndef MYWORKER_H // 如果未定义MYWORKER_H宏,则执行以下代码,防止重复包含头文件
#define MYWORKER_H

#include <QObject> // 包含QObject类头文件

class MyWorker : public QObject // 自定义的MyWorker类继承自QObject类
{
    Q_OBJECT // 使用Q_OBJECT宏,启用信号和槽机制
public:
    void drawImage(); // 绘制图像的函数

signals:
    void newImage(const QImage &img); // 定义一个名为newImage的信号,传递一个QImage对象的引用

private:
    // 私有成员,可以根据需要添加私有成员变量和函数
};

#endif // MYWORKER_H
MyWorker.cpp
cpp
#include "MyWorker.h" // 包含自定义的MyWorker类头文件
#include <QImage> // 包含QImage类头文件
#include <QPainter> // 包含QPainter类头文件
#include <QRandomGenerator> // 包含QRandomGenerator类头文件
#include <QDebug> // 包含QDebug类头文件
#include <QThread> // 包含QThread类头文件

#define mrand(min,max) QRandomGenerator::global()->bounded(min,max) // 定义宏函数,用于产生[min,max)之间的随机数

void MyWorker::drawImage() // 绘制图像的函数
{
    QImage img(640, 480, QImage::Format_RGBA8888); // 创建一个大小为640x480,格式为RGBA8888的QImage对象

    QPainter painter(&img); // 创建一个QPainter对象,用于在QImage对象上绘制图形
    painter.setPen(QPen(QColor(mrand(0, 256), mrand(0, 256), mrand(0, 256)), 3)); // 设置画笔颜色和线宽
    painter.setBrush(QBrush(QColor(mrand(0, 256), mrand(0, 256), mrand(0, 256)), Qt::BrushStyle::Dense2Pattern)); // 设置画刷颜色和填充样式

    // 随机生成12个点
    QPoint pos[] =
    {
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
        QPoint(mrand(0, 640), mrand(0, 460)),
    };

    painter.drawPolygon(pos, 12); // 在QImage对象上绘制随机多边形

    QThread::sleep(2); // 让线程暂停2秒钟,模拟长时间的计算/绘图操作
    qDebug() << "绘图函数已经完成~"; // 输出调试信息

    emit newImage(img); // 发送newImage信号,传递绘制好的QImage对象给槽函数处理
}

示例-串口助手

需要安装组件,在【开始】那找到tools即可

ui界面

用到的:

  • 【文本框Plain Text Edit】
  • 右边放一个【Widget容器】,里面放 【标签Label】,【下拉框Combo Box】,【复选框CheckBox】,【按钮PushButton】,放一个【弹簧】
  • 下面部分放【按钮PushButton】,【文本框Plain Text Edit】,【行编辑框LineEdit】,【标签Label】,【复选框CheckBox】也用一个【Widget容器】包着
  • 然后摆好之后直接分别点击两个【widget容器】,点击栅格布局即可
  • 最后点击最外的框进行栅格布局即可
  • 修改一下细节,比如边距等