(四)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模式的核心意义

  1. 职责分离原则:MVC模式将应用程序分解为三个核心组件,每个组件都有明确的职责边界:

    • Model(模型):负责数据管理、业务逻辑和数据验证
    • View(视图):负责数据展示和用户界面交互
    • Controller(控制器):负责协调模型和视图,处理用户输入
  2. 可维护性提升:通过职责分离,代码结构更加清晰,修改某一层的代码不会影响其他层,大大提高了代码的可维护性。

  3. 可测试性增强:业务逻辑与UI分离后,可以独立测试业务逻辑,无需依赖GUI环境。

  4. 可复用性提高:同一个模型可以被多个视图使用,同一个视图也可以展示不同的模型数据。

  5. 团队协作优化:不同的开发人员可以并行开发模型、视图和控制器,提高开发效率。

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);
            }
        });
    }
};

解耦分离的核心优势

  1. 业务逻辑独立性

    • 业务模型(PersonBusinessModel)完全独立于UI,可以在控制台应用、Web应用或移动应用中复用
    • 业务规则集中管理,修改业务逻辑不影响UI代码
  2. UI灵活性

    • UI可以独立变化,从TableView改为ListView或自定义控件都不影响业务逻辑
    • 支持多种UI呈现方式(详细视图、简化视图、图表视图等)
  3. 测试便利性

    // 业务逻辑单元测试 - 无需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);  // 应该失败
    }
    
  4. 维护便利性

    • 各层职责清晰,bug定位更容易
    • 可以独立升级某一层而不影响其他层
    • 代码结构标准化,新团队成员更容易理解

QT中ui和business logic是如何耦合的?

在传统Qt应用中,UI和业务逻辑常常耦合在一起,但通过MVC模式可以实现有效解耦:

在 Qt 框架中,Model-View 设计模式用于分离数据和用户界面。这个模式主要由三个组件组成:Model(模型)、View(视图)和 Delegate(委托)。下面是对 Model 和 View 关系的详细解释:

  1. 分离关注点

    • 模型和视图的分离使得数据和显示逻辑独立。模型专注于数据管理,而视图专注于数据展示。
    • 这种分离使得同一个模型可以被多个视图共享,从而实现数据的一致性和代码的重用。
  2. 数据流动

    • 视图通过模型接口获取数据,并在界面上显示。
    • 当用户在视图中进行操作(如编辑数据),视图会通过模型接口将更改传递给模型。
  3. 信号和槽机制

    • 模型和视图之间通过信号和槽进行通信。例如,当模型中的数据发生变化时,会发出 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 &current,
                       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 &current, 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:

  • (七)内核那些事儿:操作系统对网络包的处理
  • (六)内核那些事儿:文件系统
  • (五)内核那些事儿:系统和程序的交互
  • (四)内核那些事儿:设备管理与驱动开发
  • (三)内核那些事儿:CPU中断和信号