(四)Qt 那些事儿:GUI 编程
4. Qt GUI 编程
Qt GUI 编程是 Qt 框架的核心组成部分,提供了丰富的图形用户界面开发工具和组件。通过 widgets(控件)、stylesheet(样式表)、layout(布局)和 MVC(模型-视图-控制器)模式的协同工作,开发者可以构建出功能强大、界面美观、用户体验良好的桌面应用程序。
4.1 Widgets(控件)
Widgets 是 Qt GUI 编程的基础构建块,每个可视化界面元素都是一个 widget。Qt 提供了丰富的预定义控件,同时也支持自定义控件的开发。
4.1.1 基础控件类型
输入控件(Input Widgets):
-
QLineEdit:单行文本输入框,支持文本验证、占位符、密码模式等
QLineEdit *lineEdit = new QLineEdit(); lineEdit->setPlaceholderText("请输入用户名"); lineEdit->setMaxLength(20); lineEdit->setValidator(new QRegExpValidator(QRegExp("[A-Za-z0-9]+"))); -
QTextEdit:多行富文本编辑器,支持 HTML 格式、图片插入等
QTextEdit *textEdit = new QTextEdit(); textEdit->setHtml("<b>粗体文本</b> 和 <i>斜体文本</i>"); textEdit->setAcceptRichText(true); -
QSpinBox/QDoubleSpinBox:数值输入框,支持范围限制、步长设置
QSpinBox *spinBox = new QSpinBox(); spinBox->setRange(0, 100); spinBox->setSingleStep(5); spinBox->setSuffix(" %");
显示控件(Display Widgets):
-
QLabel:文本和图片显示控件
QLabel *label = new QLabel("Hello World"); label->setPixmap(QPixmap(":/images/icon.png")); label->setAlignment(Qt::AlignCenter); -
QProgressBar:进度条控件
QProgressBar *progressBar = new QProgressBar(); progressBar->setRange(0, 100); progressBar->setValue(50); progressBar->setFormat("%p% 完成");
按钮控件(Button Widgets):
- QPushButton:标准按钮
- QCheckBox:复选框
- QRadioButton:单选按钮
- QToolButton:工具按钮
QPushButton *button = new QPushButton("点击我");
QCheckBox *checkBox = new QCheckBox("启用功能");
QRadioButton *radioButton = new QRadioButton("选项A");
// 按钮组管理单选按钮
QButtonGroup *buttonGroup = new QButtonGroup();
buttonGroup->addButton(radioButton);
容器控件(Container Widgets):
- QGroupBox:分组框
- QTabWidget:选项卡容器
- QStackedWidget:堆叠容器
- QScrollArea:滚动区域
4.1.2 高级控件
列表和树控件:
- QListWidget:简单列表控件
- QTreeWidget:树形控件
- QTableWidget:表格控件
// 创建表格控件
QTableWidget *tableWidget = new QTableWidget(5, 3);
tableWidget->setHorizontalHeaderLabels({"姓名", "年龄", "职业"});
// 添加数据
QTableWidgetItem *item = new QTableWidgetItem("张三");
tableWidget->setItem(0, 0, item);
对话框类:
- QMessageBox:消息对话框
- QFileDialog:文件选择对话框
- QColorDialog:颜色选择对话框
- QFontDialog:字体选择对话框
// 显示消息框
QMessageBox::information(this, "信息", "操作完成!");
// 文件选择对话框
QString fileName = QFileDialog::getOpenFileName(this,
"选择文件", "", "文本文件 (*.txt);;所有文件 (*.*)");
4.1.3 自定义控件开发
继承现有控件:
class CustomButton : public QPushButton {
Q_OBJECT
public:
CustomButton(const QString &text, QWidget *parent = nullptr)
: QPushButton(text, parent) {
setupUI();
}
protected:
void paintEvent(QPaintEvent *event) override {
// 自定义绘制逻辑
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
// 绘制自定义外观
QRect rect = this->rect();
painter.setBrush(QColor(100, 150, 200));
painter.drawRoundedRect(rect, 5, 5);
// 调用基类绘制文本
QPushButton::paintEvent(event);
}
private:
void setupUI() {
setMinimumSize(100, 30);
setStyleSheet("border: none; color: white;");
}
};
完全自定义控件:
class CustomWidget : public QWidget {
Q_OBJECT
public:
CustomWidget(QWidget *parent = nullptr) : QWidget(parent) {
setAttribute(Qt::WA_StaticContents);
setMouseTracking(true);
}
protected:
void paintEvent(QPaintEvent *event) override {
QPainter painter(this);
painter.fillRect(rect(), Qt::white);
// 自定义绘制内容
painter.setPen(QPen(Qt::black, 2));
painter.drawLine(10, 10, width()-10, height()-10);
}
void mousePressEvent(QMouseEvent *event) override {
if (event->button() == Qt::LeftButton) {
emit clicked(event->pos());
}
QWidget::mousePressEvent(event);
}
signals:
void clicked(const QPoint &position);
};
4.1.4 特殊控件和界面组件
QDockWidget(停靠窗口): QDockWidget是一种可停靠的窗口部件,它可以在主窗口的边缘停靠,并且可以通过拖拽来重新排列位置。
class MainWindow : public QMainWindow {
public:
MainWindow() {
// 创建停靠窗口
QDockWidget *dockWidget = new QDockWidget("工具面板", this);
// 创建停靠窗口的内容
QListWidget *listWidget = new QListWidget();
listWidget->addItems({"工具1", "工具2", "工具3"});
dockWidget->setWidget(listWidget);
// 设置停靠区域
dockWidget->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
// 添加到主窗口
addDockWidget(Qt::LeftDockWidgetArea, dockWidget);
// 设置停靠窗口特性
dockWidget->setFeatures(QDockWidget::DockWidgetMovable |
QDockWidget::DockWidgetFloatable |
QDockWidget::DockWidgetClosable);
}
};
QMdiArea(多文档接口): MDI窗口(Multiple Document Interface,多文档接口窗口)是一种用户界面策略,允许应用程序在一个主窗口中同时打开和处理多个子窗口。
class MDIApplication : public QMainWindow {
private:
QMdiArea *mdiArea;
public:
MDIApplication() {
// 创建MDI区域
mdiArea = new QMdiArea();
setCentralWidget(mdiArea);
// 创建菜单
createMenus();
}
private slots:
void newDocument() {
// 创建新的子窗口
QTextEdit *textEdit = new QTextEdit();
QMdiSubWindow *subWindow = mdiArea->addSubWindow(textEdit);
subWindow->setWindowTitle("新文档");
subWindow->show();
}
void cascadeWindows() {
mdiArea->cascadeSubWindows();
}
void tileWindows() {
mdiArea->tileSubWindows();
}
private:
void createMenus() {
QMenu *fileMenu = menuBar()->addMenu("文件");
fileMenu->addAction("新建", this, &MDIApplication::newDocument);
QMenu *windowMenu = menuBar()->addMenu("窗口");
windowMenu->addAction("层叠", this, &MDIApplication::cascadeWindows);
windowMenu->addAction("平铺", this, &MDIApplication::tileWindows);
}
};
QGroupBox(分组框): QGroupBox是一个容器控件,用于将相关的控件组织在一起,并提供一个可选的标题。
// 创建分组框
QGroupBox *personalInfoGroup = new QGroupBox("个人信息");
QFormLayout *formLayout = new QFormLayout();
formLayout->addRow("姓名:", new QLineEdit());
formLayout->addRow("年龄:", new QSpinBox());
formLayout->addRow("性别:", new QComboBox());
personalInfoGroup->setLayout(formLayout);
// 可选择的分组框
QGroupBox *optionsGroup = new QGroupBox("选项");
optionsGroup->setCheckable(true); // 使分组框可选择
optionsGroup->setChecked(true); // 默认选中
QVBoxLayout *optionsLayout = new QVBoxLayout();
optionsLayout->addWidget(new QCheckBox("选项1"));
optionsLayout->addWidget(new QCheckBox("选项2"));
optionsGroup->setLayout(optionsLayout);
4.1.5 控件更新和重绘机制
update() vs repaint(): Qt 提供了两种不同的控件更新机制:
class CustomWidget : public QWidget {
private:
int value;
public:
void setValue(int newValue) {
if (value != newValue) {
value = newValue;
// update() - 延迟更新,在下次事件循环中重绘
update(); // 推荐使用,性能更好
// repaint() - 立即重绘,不等待事件循环
// repaint(); // 只在需要立即更新时使用
}
}
protected:
void paintEvent(QPaintEvent *event) override {
QPainter painter(this);
painter.drawText(rect(), Qt::AlignCenter, QString::number(value));
}
};
// 使用场景示例
void demonstrateUpdateMethods() {
CustomWidget *widget = new CustomWidget();
// 批量更新时使用 update()
for (int i = 0; i < 100; ++i) {
widget->setValue(i); // 每次调用 update()
}
// Qt 会合并这些更新请求,只进行一次重绘
// 需要立即更新时使用 repaint()
widget->setValue(200);
widget->repaint(); // 立即重绘,不等待事件循环
}
几何属性更新:
class ResizableWidget : public QWidget {
private:
QString customText;
public:
void setCustomText(const QString &text) {
customText = text;
// 通知布局系统重新计算
updateGeometry();
// 请求重绘
update();
}
QSize sizeHint() const override {
QFontMetrics fm(font());
return fm.size(Qt::TextSingleLine, customText) + QSize(20, 10);
}
protected:
void paintEvent(QPaintEvent *event) override {
QPainter painter(this);
painter.drawText(rect(), Qt::AlignCenter, customText);
}
};
// updateGeometry() 使用场景
void whenToUseUpdateGeometry() {
ResizableWidget *widget = new ResizableWidget();
// 当控件内容变化影响尺寸提示时
widget->setCustomText("This is a much longer text");
// 当尺寸策略变化时
widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
widget->updateGeometry();
// 当最小/最大尺寸变化时
widget->setMinimumSize(200, 50);
widget->updateGeometry();
}
4.1.6 窗口显示方法
Qt 提供了不同的窗口显示方法,适用于不同的场景:
// show() - 非模态显示
QWidget *window = new QWidget();
window->show(); // 窗口显示,程序继续执行
// exec() - 模态显示
QDialog *dialog = new QDialog();
int result = dialog->exec(); // 阻塞执行,直到对话框关闭
if (result == QDialog::Accepted) {
// 用户点击了确定
}
// 模态窗口示例
void showModalDialog() {
QMessageBox msgBox;
msgBox.setText("这是一个模态对话框");
msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
int ret = msgBox.exec(); // 阻塞执行
if (ret == QMessageBox::Ok) {
qDebug() << "用户点击了确定";
}
}
// 非模态窗口示例
void showModelessDialog() {
QDialog *dialog = new QDialog(this);
dialog->setWindowTitle("非模态对话框");
dialog->setAttribute(Qt::WA_DeleteOnClose); // 关闭时自动删除
dialog->show(); // 非阻塞显示
}
4.2 Stylesheet(样式表)
Qt 样式表(QSS)是 Qt 提供的强大样式定制机制,语法类似 CSS,允许开发者精确控制界面外观。
4.2.1 样式表基础语法
选择器类型:
/* 类型选择器 - 影响所有 QPushButton */
QPushButton {
background-color: #4caf50;
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
}
/* ID选择器 - 影响特定objectName的控件 */
QPushButton#submitButton {
background-color: #f44336;
}
/* 类选择器 - 影响继承关系 */
.QPushButton {
font-weight: bold;
}
/* 状态选择器 - 控件状态 */
QPushButton:hover {
background-color: #45a049;
}
QPushButton:pressed {
background-color: #3d8b40;
}
QPushButton:disabled {
background-color: #cccccc;
color: #666666;
}
属性选择器:
/* 属性选择器 */
QLineEdit[readOnly="true"] {
background-color: #f0f0f0;
color: #888888;
}
/* 复合选择器 */
QDialog QPushButton {
min-width: 80px;
min-height: 25px;
}
4.2.2 常用样式属性
背景和边框:
QWidget {
background-color: #ffffff;
background-image: url(:/images/background.png);
background-repeat: no-repeat;
background-position: center;
border: 2px solid #cccccc;
border-radius: 8px;
border-top-left-radius: 10px;
border-bottom-right-radius: 0px;
}
字体和文本:
QLabel {
font-family: "Arial", "Microsoft YaHei";
font-size: 14px;
font-weight: bold;
font-style: italic;
color: #333333;
text-align: center;
}
布局和间距:
QPushButton {
padding: 10px 20px;
margin: 5px;
min-width: 100px;
max-width: 200px;
min-height: 30px;
}
4.2.3 高级样式技巧
复杂控件样式化:
/* QTableView 样式 */
QTableView {
gridline-color: #d0d0d0;
background-color: #ffffff;
alternate-background-color: #f5f5f5;
}
QTableView::item {
padding: 5px;
border-bottom: 1px solid #e0e0e0;
}
QTableView::item:selected {
background-color: #3daee9;
color: white;
}
/* QScrollBar 样式 */
QScrollBar:vertical {
background: #f0f0f0;
width: 12px;
border: none;
}
QScrollBar::handle:vertical {
background: #c0c0c0;
min-height: 20px;
border-radius: 6px;
}
QScrollBar::handle:vertical:hover {
background: #a0a0a0;
}
主题切换实现:
class ThemeManager {
public:
enum Theme { Light, Dark };
static void applyTheme(Theme theme) {
QString styleSheet;
if (theme == Light) {
styleSheet = loadStyleSheet(":/styles/light.qss");
} else {
styleSheet = loadStyleSheet(":/styles/dark.qss");
}
qApp->setStyleSheet(styleSheet);
}
private:
static QString loadStyleSheet(const QString &path) {
QFile file(path);
if (file.open(QIODevice::ReadOnly)) {
return file.readAll();
}
return QString();
}
};
4.3 Layout(布局)
Qt 的布局管理系统提供了自动化的控件位置和尺寸管理,确保界面在不同窗口大小和分辨率下都能正确显示。
4.3.1 基础布局类型
水平布局(QHBoxLayout):
QHBoxLayout *horizontalLayout = new QHBoxLayout();
horizontalLayout->addWidget(new QPushButton("按钮1"));
horizontalLayout->addWidget(new QPushButton("按钮2"));
horizontalLayout->addWidget(new QPushButton("按钮3"));
// 设置间距和边距
horizontalLayout->setSpacing(10);
horizontalLayout->setContentsMargins(5, 5, 5, 5);
// 设置伸缩因子
horizontalLayout->addWidget(button1, 1); // 占1份
horizontalLayout->addWidget(button2, 2); // 占2份
垂直布局(QVBoxLayout):
QVBoxLayout *verticalLayout = new QVBoxLayout();
verticalLayout->addWidget(new QLabel("标题"));
verticalLayout->addWidget(new QLineEdit());
verticalLayout->addWidget(new QPushButton("确定"));
// 添加弹性空间
verticalLayout->addStretch(); // 末尾添加弹性空间
verticalLayout->insertStretch(1, 2); // 在索引1处添加2倍弹性空间
网格布局(QGridLayout):
QGridLayout *gridLayout = new QGridLayout();
// 添加控件到指定位置 (行, 列)
gridLayout->addWidget(new QLabel("用户名:"), 0, 0);
gridLayout->addWidget(new QLineEdit(), 0, 1);
gridLayout->addWidget(new QLabel("密码:"), 1, 0);
gridLayout->addWidget(new QLineEdit(), 1, 1);
// 跨行跨列添加控件 (行, 列, 行跨度, 列跨度)
gridLayout->addWidget(new QPushButton("登录"), 2, 0, 1, 2);
// 设置行列伸缩
gridLayout->setRowStretch(0, 1); // 第0行占1份
gridLayout->setColumnStretch(1, 2); // 第1列占2份
表单布局(QFormLayout):
QFormLayout *formLayout = new QFormLayout();
formLayout->addRow("姓名:", new QLineEdit());
formLayout->addRow("年龄:", new QSpinBox());
formLayout->addRow("邮箱:", new QLineEdit());
// 添加分隔符
formLayout->addRow(new QLabel(""));
formLayout->addRow("备注:", new QTextEdit());
// 设置标签对齐方式
formLayout->setLabelAlignment(Qt::AlignRight);
4.3.2 高级布局技巧
布局嵌套:
// 创建主布局
QVBoxLayout *mainLayout = new QVBoxLayout();
// 创建顶部水平布局
QHBoxLayout *topLayout = new QHBoxLayout();
topLayout->addWidget(new QLabel("搜索:"));
topLayout->addWidget(new QLineEdit());
topLayout->addWidget(new QPushButton("搜索"));
// 创建中间内容区域
QHBoxLayout *contentLayout = new QHBoxLayout();
contentLayout->addWidget(new QListWidget());
contentLayout->addWidget(new QTextEdit());
// 创建底部按钮布局
QHBoxLayout *buttonLayout = new QHBoxLayout();
buttonLayout->addStretch(); // 左侧弹性空间
buttonLayout->addWidget(new QPushButton("取消"));
buttonLayout->addWidget(new QPushButton("确定"));
// 组装主布局
mainLayout->addLayout(topLayout);
mainLayout->addLayout(contentLayout);
mainLayout->addLayout(buttonLayout);
widget->setLayout(mainLayout);
尺寸策略控制:
// 设置控件的尺寸策略
QPushButton *button = new QPushButton("按钮");
button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
QLineEdit *lineEdit = new QLineEdit();
lineEdit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
// 设置最小/最大尺寸
button->setMinimumSize(80, 25);
button->setMaximumWidth(120);
lineEdit->setMinimumWidth(200);
动态布局调整:
class DynamicLayoutWidget : public QWidget {
public:
DynamicLayoutWidget() {
setupUI();
}
void addItem(const QString &text) {
QPushButton *button = new QPushButton(text);
connect(button, &QPushButton::clicked, [this, button]() {
removeItem(button);
});
layout()->addWidget(button);
}
void removeItem(QWidget *widget) {
layout()->removeWidget(widget);
widget->deleteLater();
}
private:
void setupUI() {
setLayout(new QVBoxLayout());
layout()->setAlignment(Qt::AlignTop);
}
};
4.4 MVC(模型-视图-控制器)
4.4.0 MVC 模式的意义与价值
引入MVC模式的核心意义:
-
职责分离原则:MVC模式将应用程序分解为三个核心组件,每个组件都有明确的职责边界:
- Model(模型):负责数据管理、业务逻辑和数据验证
- View(视图):负责数据展示和用户界面交互
- Controller(控制器):负责协调模型和视图,处理用户输入
-
可维护性提升:通过职责分离,代码结构更加清晰,修改某一层的代码不会影响其他层,大大提高了代码的可维护性。
-
可测试性增强:业务逻辑与UI分离后,可以独立测试业务逻辑,无需依赖GUI环境。
-
可复用性提高:同一个模型可以被多个视图使用,同一个视图也可以展示不同的模型数据。
-
团队协作优化:不同的开发人员可以并行开发模型、视图和控制器,提高开发效率。
Qt中MVC模式的特点:
Qt采用了简化的Model-View架构(没有显式的Controller),通过信号槽机制和委托(Delegate)来实现MVC的职责分离:
// 传统的紧耦合方式(不推荐)
class BadDesignWidget : public QWidget {
private:
QTableWidget *table;
QVector<Person> persons; // 数据直接存储在UI类中
QLineEdit *nameEdit;
public:
void addPerson() {
// UI逻辑和业务逻辑混合在一起
QString name = nameEdit->text();
if (name.isEmpty()) {
QMessageBox::warning(this, "错误", "姓名不能为空");
return;
}
// 业务逻辑:数据验证和处理
if (name.length() > 50) {
QMessageBox::warning(this, "错误", "姓名过长");
return;
}
// 直接操作UI和数据
Person person;
person.name = name;
persons.append(person);
// 手动更新UI
int row = table->rowCount();
table->insertRow(row);
table->setItem(row, 0, new QTableWidgetItem(name));
nameEdit->clear();
}
};
4.4.1 UI与业务逻辑的解耦分离
分离策略和实现方法:
1. 数据层分离:
// 业务数据模型 - 纯业务逻辑,不依赖UI
class PersonBusinessModel : public QObject {
Q_OBJECT
private:
QVector<Person> persons;
PersonValidator validator; // 业务规则验证器
PersonRepository repository; // 数据持久化
public:
// 纯业务方法,不涉及UI
bool addPerson(const QString &name, int age, const QString &occupation) {
// 业务规则验证
if (!validator.validateName(name)) {
emit errorOccurred("姓名格式不正确");
return false;
}
if (!validator.validateAge(age)) {
emit errorOccurred("年龄范围无效");
return false;
}
// 检查重复
if (findPersonByName(name) != -1) {
emit errorOccurred("姓名已存在");
return false;
}
// 执行业务操作
Person person{name, age, occupation};
persons.append(person);
// 持久化数据
repository.save(person);
emit personAdded(person);
return true;
}
bool removePerson(int index) {
if (index < 0 || index >= persons.size()) {
return false;
}
Person person = persons[index];
persons.removeAt(index);
repository.remove(person.id);
emit personRemoved(index, person);
return true;
}
// 查询方法
QVector<Person> getAllPersons() const { return persons; }
Person getPersonAt(int index) const { return persons.value(index); }
signals:
void personAdded(const Person &person);
void personRemoved(int index, const Person &person);
void errorOccurred(const QString &message);
void dataChanged();
};
2. 视图适配层:
// Qt Model适配器 - 将业务模型适配为Qt View可用的接口
class PersonTableModel : public QAbstractTableModel {
Q_OBJECT
private:
PersonBusinessModel *businessModel; // 引用业务模型
public:
PersonTableModel(PersonBusinessModel *business, QObject *parent = nullptr)
: QAbstractTableModel(parent), businessModel(business) {
// 监听业务模型变化
connect(businessModel, &PersonBusinessModel::personAdded,
this, &PersonTableModel::onPersonAdded);
connect(businessModel, &PersonBusinessModel::personRemoved,
this, &PersonTableModel::onPersonRemoved);
}
// Qt Model接口实现
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return businessModel->getAllPersons().size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return 3;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid()) return QVariant();
Person person = businessModel->getPersonAt(index.row());
if (role == Qt::DisplayRole) {
switch (index.column()) {
case 0: return person.name;
case 1: return person.age;
case 2: return person.occupation;
}
}
return QVariant();
}
// 编辑支持
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || role != Qt::EditRole) return false;
Person person = businessModel->getPersonAt(index.row());
// 根据列更新对应字段
switch (index.column()) {
case 0: person.name = value.toString(); break;
case 1: person.age = value.toInt(); break;
case 2: person.occupation = value.toString(); break;
default: return false;
}
// 通过业务模型更新(经过业务验证)
if (businessModel->updatePerson(index.row(), person)) {
emit dataChanged(index, index, {Qt::DisplayRole});
return true;
}
return false;
}
private slots:
void onPersonAdded(const Person &person) {
int row = businessModel->getAllPersons().size() - 1;
beginInsertRows(QModelIndex(), row, row);
endInsertRows();
}
void onPersonRemoved(int index, const Person &person) {
beginRemoveRows(QModelIndex(), index, index);
endRemoveRows();
}
};
3. 控制器层(协调层):
// 应用控制器 - 协调UI和业务逻辑
class PersonController : public QObject {
Q_OBJECT
private:
PersonBusinessModel *businessModel;
PersonTableModel *tableModel;
QTableView *view;
QLineEdit *nameEdit;
QSpinBox *ageSpinBox;
QLineEdit *occupationEdit;
public:
PersonController(QObject *parent = nullptr) : QObject(parent) {
// 创建业务模型
businessModel = new PersonBusinessModel(this);
// 创建适配模型
tableModel = new PersonTableModel(businessModel, this);
// 连接业务模型的错误信号
connect(businessModel, &PersonBusinessModel::errorOccurred,
this, &PersonController::handleBusinessError);
}
void setView(QTableView *v) {
view = v;
view->setModel(tableModel);
}
void setInputControls(QLineEdit *name, QSpinBox *age, QLineEdit *occupation) {
nameEdit = name;
ageSpinBox = age;
occupationEdit = occupation;
}
public slots:
// UI事件处理 - 只负责收集UI数据,委托给业务层处理
void addPersonRequested() {
QString name = nameEdit->text().trimmed();
int age = ageSpinBox->value();
QString occupation = occupationEdit->text().trimmed();
// 委托给业务层,不在这里做业务验证
if (businessModel->addPerson(name, age, occupation)) {
// 成功后清空UI
clearInputs();
}
// 错误处理由 handleBusinessError 统一处理
}
void removePersonRequested() {
QModelIndexList selected = view->selectionModel()->selectedRows();
if (!selected.isEmpty()) {
int row = selected.first().row();
businessModel->removePerson(row);
}
}
void importFromFile(const QString &fileName) {
// 委托给业务层处理文件导入
businessModel->importFromFile(fileName);
}
void exportToFile(const QString &fileName) {
// 委托给业务层处理文件导出
businessModel->exportToFile(fileName);
}
private slots:
void handleBusinessError(const QString &message) {
// 统一的错误处理 - 只负责UI展示
QMessageBox::warning(qobject_cast<QWidget*>(parent()), "业务错误", message);
}
private:
void clearInputs() {
nameEdit->clear();
ageSpinBox->setValue(18);
occupationEdit->clear();
}
};
4. 完整的分层应用示例:
// 主窗口 - 只负责UI组装和事件连接
class MainWindow : public QMainWindow {
Q_OBJECT
private:
PersonController *controller;
QTableView *tableView;
QLineEdit *nameEdit;
QSpinBox *ageSpinBox;
QLineEdit *occupationEdit;
QPushButton *addButton;
QPushButton *removeButton;
public:
MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
setupUI();
setupController();
connectSignals();
}
private:
void setupUI() {
auto *centralWidget = new QWidget();
setCentralWidget(centralWidget);
// 创建UI控件
tableView = new QTableView();
nameEdit = new QLineEdit();
ageSpinBox = new QSpinBox();
ageSpinBox->setRange(18, 100);
occupationEdit = new QLineEdit();
addButton = new QPushButton("添加");
removeButton = new QPushButton("删除");
// 布局设置
auto *layout = new QVBoxLayout(centralWidget);
auto *formLayout = new QFormLayout();
formLayout->addRow("姓名:", nameEdit);
formLayout->addRow("年龄:", ageSpinBox);
formLayout->addRow("职业:", occupationEdit);
auto *buttonLayout = new QHBoxLayout();
buttonLayout->addWidget(addButton);
buttonLayout->addWidget(removeButton);
buttonLayout->addStretch();
layout->addLayout(formLayout);
layout->addLayout(buttonLayout);
layout->addWidget(tableView);
// 创建菜单
createMenus();
}
void setupController() {
controller = new PersonController(this);
controller->setView(tableView);
controller->setInputControls(nameEdit, ageSpinBox, occupationEdit);
}
void connectSignals() {
// UI事件直接连接到控制器
connect(addButton, &QPushButton::clicked,
controller, &PersonController::addPersonRequested);
connect(removeButton, &QPushButton::clicked,
controller, &PersonController::removePersonRequested);
// 回车键添加
connect(nameEdit, &QLineEdit::returnPressed,
controller, &PersonController::addPersonRequested);
connect(occupationEdit, &QLineEdit::returnPressed,
controller, &PersonController::addPersonRequested);
}
void createMenus() {
auto *fileMenu = menuBar()->addMenu("文件");
auto *importAction = fileMenu->addAction("导入");
connect(importAction, &QAction::triggered, [this]() {
QString fileName = QFileDialog::getOpenFileName(this, "导入文件", "", "CSV Files (*.csv)");
if (!fileName.isEmpty()) {
controller->importFromFile(fileName);
}
});
auto *exportAction = fileMenu->addAction("导出");
connect(exportAction, &QAction::triggered, [this]() {
QString fileName = QFileDialog::getSaveFileName(this, "导出文件", "", "CSV Files (*.csv)");
if (!fileName.isEmpty()) {
controller->exportToFile(fileName);
}
});
}
};
解耦分离的核心优势:
-
业务逻辑独立性:
- 业务模型(PersonBusinessModel)完全独立于UI,可以在控制台应用、Web应用或移动应用中复用
- 业务规则集中管理,修改业务逻辑不影响UI代码
-
UI灵活性:
- UI可以独立变化,从TableView改为ListView或自定义控件都不影响业务逻辑
- 支持多种UI呈现方式(详细视图、简化视图、图表视图等)
-
测试便利性:
// 业务逻辑单元测试 - 无需GUI环境 void testPersonBusinessModel() { PersonBusinessModel model; // 测试添加功能 bool result = model.addPerson("张三", 25, "工程师"); QVERIFY(result == true); QVERIFY(model.getAllPersons().size() == 1); // 测试重复添加 result = model.addPerson("张三", 30, "设计师"); QVERIFY(result == false); // 应该失败 // 测试无效数据 result = model.addPerson("", 25, "工程师"); QVERIFY(result == false); // 应该失败 } -
维护便利性:
- 各层职责清晰,bug定位更容易
- 可以独立升级某一层而不影响其他层
- 代码结构标准化,新团队成员更容易理解
QT中ui和business logic是如何耦合的?
在传统Qt应用中,UI和业务逻辑常常耦合在一起,但通过MVC模式可以实现有效解耦:
在 Qt 框架中,Model-View 设计模式用于分离数据和用户界面。这个模式主要由三个组件组成:Model(模型)、View(视图)和 Delegate(委托)。下面是对 Model 和 View 关系的详细解释:
-
分离关注点:
- 模型和视图的分离使得数据和显示逻辑独立。模型专注于数据管理,而视图专注于数据展示。
- 这种分离使得同一个模型可以被多个视图共享,从而实现数据的一致性和代码的重用。
-
数据流动:
- 视图通过模型接口获取数据,并在界面上显示。
- 当用户在视图中进行操作(如编辑数据),视图会通过模型接口将更改传递给模型。
-
信号和槽机制:
- 模型和视图之间通过信号和槽进行通信。例如,当模型中的数据发生变化时,会发出
dataChanged信号,视图接收到信号后会更新显示。 - 视图中的用户操作(如选择、编辑)也会通过信号和槽机制通知模型。
- 模型和视图之间通过信号和槽进行通信。例如,当模型中的数据发生变化时,会发出
4.4.1 Model(模型)信号类型
数据变化信号:
// QAbstractItemModel 的核心信号
class CustomModel : public QAbstractItemModel {
Q_OBJECT
public:
// 当数据发生变化时发出
void updateData(const QModelIndex &index, const QVariant &value) {
setData(index, value, Qt::DisplayRole);
emit dataChanged(index, index, {Qt::DisplayRole});
}
// 当行即将被插入时发出
void insertRows(int row, int count) {
beginInsertRows(QModelIndex(), row, row + count - 1);
// 实际插入逻辑
endInsertRows(); // 这会触发 rowsInserted 信号
}
// 当行即将被删除时发出
void removeRows(int row, int count) {
beginRemoveRows(QModelIndex(), row, row + count - 1);
// 实际删除逻辑
endRemoveRows(); // 这会触发 rowsRemoved 信号
}
signals:
// 数据变化信号
void dataChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight,
const QVector<int> &roles);
// 结构变化信号
void rowsAboutToBeInserted(const QModelIndex &parent, int first, int last);
void rowsInserted(const QModelIndex &parent, int first, int last);
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
void rowsRemoved(const QModelIndex &parent, int first, int last);
void columnsAboutToBeInserted(const QModelIndex &parent, int first, int last);
void columnsInserted(const QModelIndex &parent, int first, int last);
void columnsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
void columnsRemoved(const QModelIndex &parent, int first, int last);
// 布局变化信号
void layoutAboutToBeChanged();
void layoutChanged();
// 模型重置信号
void modelAboutToBeReset();
void modelReset();
};
4.4.2 View(视图)信号类型
选择和交互信号:
// QAbstractItemView 的核心信号
class CustomView : public QTableView {
Q_OBJECT
public:
CustomView(QWidget *parent = nullptr) : QTableView(parent) {
// 连接信号
connect(this, &QAbstractItemView::clicked,
this, &CustomView::onItemClicked);
connect(this, &QAbstractItemView::doubleClicked,
this, &CustomView::onItemDoubleClicked);
}
private slots:
void onItemClicked(const QModelIndex &index) {
qDebug() << "点击项目:" << index.row() << index.column();
}
void onItemDoubleClicked(const QModelIndex &index) {
qDebug() << "双击项目:" << index.row() << index.column();
// 进入编辑模式
edit(index);
}
signals:
// 鼠标交互信号
void clicked(const QModelIndex &index);
void doubleClicked(const QModelIndex &index);
void pressed(const QModelIndex &index);
// 激活信号
void activated(const QModelIndex &index);
// 进入/退出编辑信号
void entered(const QModelIndex &index);
void viewportEntered();
// 选择变化信号(通过 QItemSelectionModel)
void selectionChanged(const QItemSelection &selected,
const QItemSelection &deselected);
void currentChanged(const QModelIndex ¤t,
const QModelIndex &previous);
};
4.4.3 完整的 MVC 示例
// 自定义模型
class PersonModel : public QAbstractTableModel {
Q_OBJECT
private:
struct Person {
QString name;
int age;
QString occupation;
};
QVector<Person> persons;
public:
PersonModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {
// 初始化数据
persons = {
{"张三", 25, "工程师"},
{"李四", 30, "设计师"},
{"王五", 28, "产品经理"}
};
}
// 必须实现的纯虚函数
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
Q_UNUSED(parent)
return persons.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
Q_UNUSED(parent)
return 3; // 姓名、年龄、职业
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.row() >= persons.size()) {
return QVariant();
}
const Person &person = persons[index.row()];
if (role == Qt::DisplayRole) {
switch (index.column()) {
case 0: return person.name;
case 1: return person.age;
case 2: return person.occupation;
}
} else if (role == Qt::BackgroundRole) {
// 偶数行使用不同背景色
if (index.row() % 2 == 0) {
return QBrush(QColor(240, 240, 240));
}
}
return QVariant();
}
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override {
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section) {
case 0: return "姓名";
case 1: return "年龄";
case 2: return "职业";
}
}
return QVariant();
}
// 支持编辑
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override {
if (!index.isValid() || index.row() >= persons.size() || role != Qt::EditRole) {
return false;
}
Person &person = persons[index.row()];
switch (index.column()) {
case 0:
person.name = value.toString();
break;
case 1:
person.age = value.toInt();
break;
case 2:
person.occupation = value.toString();
break;
default:
return false;
}
emit dataChanged(index, index, {Qt::DisplayRole});
return true;
}
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
// 添加新人员
void addPerson(const QString &name, int age, const QString &occupation) {
int row = persons.size();
beginInsertRows(QModelIndex(), row, row);
persons.append({name, age, occupation});
endInsertRows();
}
// 删除人员
void removePerson(int row) {
if (row < 0 || row >= persons.size()) {
return;
}
beginRemoveRows(QModelIndex(), row, row);
persons.removeAt(row);
endRemoveRows();
}
};
// 主窗口类
class MainWindow : public QMainWindow {
Q_OBJECT
private:
PersonModel *model;
QTableView *view;
QLineEdit *nameEdit;
QSpinBox *ageSpinBox;
QLineEdit *occupationEdit;
public:
MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) {
setupUI();
setupConnections();
}
private:
void setupUI() {
auto *centralWidget = new QWidget();
setCentralWidget(centralWidget);
// 创建模型和视图
model = new PersonModel(this);
view = new QTableView();
view->setModel(model);
view->setSelectionBehavior(QAbstractItemView::SelectRows);
// 创建输入控件
nameEdit = new QLineEdit();
ageSpinBox = new QSpinBox();
ageSpinBox->setRange(18, 100);
occupationEdit = new QLineEdit();
auto *addButton = new QPushButton("添加");
auto *removeButton = new QPushButton("删除");
// 布局
auto *mainLayout = new QVBoxLayout(centralWidget);
// 输入表单
auto *formLayout = new QFormLayout();
formLayout->addRow("姓名:", nameEdit);
formLayout->addRow("年龄:", ageSpinBox);
formLayout->addRow("职业:", occupationEdit);
auto *buttonLayout = new QHBoxLayout();
buttonLayout->addWidget(addButton);
buttonLayout->addWidget(removeButton);
buttonLayout->addStretch();
mainLayout->addLayout(formLayout);
mainLayout->addLayout(buttonLayout);
mainLayout->addWidget(view);
// 连接按钮信号
connect(addButton, &QPushButton::clicked, this, &MainWindow::addPerson);
connect(removeButton, &QPushButton::clicked, this, &MainWindow::removePerson);
}
void setupConnections() {
// 监听视图选择变化
connect(view->selectionModel(), &QItemSelectionModel::currentRowChanged,
this, &MainWindow::onCurrentRowChanged);
// 监听模型数据变化
connect(model, &QAbstractItemModel::dataChanged,
this, &MainWindow::onDataChanged);
}
private slots:
void addPerson() {
QString name = nameEdit->text().trimmed();
int age = ageSpinBox->value();
QString occupation = occupationEdit->text().trimmed();
if (!name.isEmpty() && !occupation.isEmpty()) {
model->addPerson(name, age, occupation);
// 清空输入框
nameEdit->clear();
ageSpinBox->setValue(18);
occupationEdit->clear();
}
}
void removePerson() {
QModelIndex current = view->currentIndex();
if (current.isValid()) {
model->removePerson(current.row());
}
}
void onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex &previous) {
Q_UNUSED(previous)
if (current.isValid()) {
// 将选中行的数据填入编辑框
QString name = model->data(model->index(current.row(), 0)).toString();
int age = model->data(model->index(current.row(), 1)).toInt();
QString occupation = model->data(model->index(current.row(), 2)).toString();
nameEdit->setText(name);
ageSpinBox->setValue(age);
occupationEdit->setText(occupation);
}
}
void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) {
qDebug() << "数据变化:" << topLeft.row() << topLeft.column()
<< "到" << bottomRight.row() << bottomRight.column();
}
};
#include "main.moc"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}
Model 和 View 信号绑定总结:
| 组件 | 信号类型 | 主要信号 | 用途 |
|---|---|---|---|
| Model | 数据变化 | dataChanged | 通知视图数据已修改 |
| 结构变化 | rowsInserted, rowsRemoved | 通知视图行结构变化 | |
| 布局变化 | layoutChanged | 通知视图重新布局 | |
| 重置 | modelReset | 通知视图模型完全重置 | |
| View | 用户交互 | clicked, doubleClicked | 响应用户点击操作 |
| 选择变化 | selectionChanged | 响应选择项变化 | |
| 激活 | activated | 响应项目激活(回车/双击) | |
| 编辑 | entered, viewportEntered | 响应编辑状态变化 |
这种信号槽机制确保了 Model 和 View 之间的松耦合,当数据发生变化时,所有相关的视图都会自动更新,而当用户在视图中进行操作时,也会通过适当的信号通知到相关组件。
Enjoy Reading This Article?
Here are some more articles you might like to read next: