QT6学习笔记-3
前言
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,获取其模型索引的代码是:
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 的父节点是顶层节点,获取模型索引的代码是:
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());但是,节点 B 的父节点是节点 A,节点 B 的模型索引由下面的代码生成:
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
#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
#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
#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
#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
#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
#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
#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.cpp 和 ContactItem.h
ContactView.h 只需要包含头文件即可
#include"Contact.h"ContactView.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
#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
#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
[static] QThread *QThread::create(Function &&f, Args &&... args) //C++17【注意】
调用者获得返回的QThread实例的所有权。也就是说,需要自己释放线程对象
不要对返回的QThread实例多次调用start(); 这样做将导致未定义的行为
#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(),用来处理线程中的事件
/*
* 必须继承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() 函数可以在不破坏类结构的前提下依然可以在新线程中运行
#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
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}mainwindow.h
#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_Hmainwindow.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
#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_HMyWorker.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容器】,点击栅格布局即可
- 最后点击最外的框进行栅格布局即可
- 修改一下细节,比如边距等
