Qt - 模型/视图(Model/View)

注意:本教程假设读者已经具备基本的Qt知识,包括信号和槽的使用。

  1. 模型/视图架构简介
    模型/视图架构是一种用于将数据存储和用户界面分离的设计模式。在这种架构中,模型负责管理数据,视图负责显示数据,而委托负责处理如何渲染和编辑数据项。
    这种分离使得我们可以使用不同的视图来显示同一模型的数据,而无需改变模型本身。

  2. 为什么使用模型/视图架构

  • 分离数据存储和显示,使得代码更清晰、更易于维护。

  • 同一个模型可以用于多个视图,保持数据的一致性。

  • 标准化的接口使得自定义显示和编辑数据变得容易。

  1. 模型(Model)的角色
    模型是数据的抽象,它提供了视图和委托访问数据的接口。所有的模型都继承自QAbstractItemModel。模型中的数据被组织成层次结构,但最常见的列表和表格结构分别由QAbstractListModel和QAbstractTableModel提供。

  2. 视图(View)的角色
    视图是显示数据的组件,例如QListView、QTableView和QTreeView。它们从模型获取数据并显示给用户。

  3. 委托(Delegate)的角色
    委托负责绘制数据项,并在需要编辑时提供编辑器。默认的委托是QStyledItemDelegate,它可以处理常见数据类型的显示和编辑。

  4. 一个简单的示例:使用QStringListModel和QListView
    我们将创建一个简单的字符串列表,并使用QListView来显示。

步骤:
a. 创建一个QStringListModel,并设置字符串列表数据。
b. 创建一个QListView,并设置模型。
c. 显示视图。

  1. 一个更复杂的示例:自定义模型
    我们将创建一个自定义的表格模型,继承自QAbstractTableModel,并实现必要的虚函数。

  2. 总结

下面我们将通过代码来演示这两个示例。

注意:为了简洁,示例代码将不包括完整的项目设置,只展示关键部分。我们将使用C++和Qt5。

首先,我们来看第一个示例:使用QStringListModel和QListView。

 

一、模型/视图架构概述

1.1 什么是模型/视图架构

Qt的模型/视图架构是一种用于分离数据存储和数据显示的设计模式,它解决了传统部件(如QListWidget、QTableWidget)中数据与显示耦合的问题。

核心组件:

  • 模型(Model):管理数据,提供数据访问接口

  • 视图(View):显示数据,处理用户交互

  • 委托(Delegate):控制数据项的渲染和编辑方式

1.2 模型/视图与传统部件的对比

 
 
特性 传统部件(QListWidget等) 模型/视图架构
数据存储 部件内部存储 外部模型管理
大数据集 性能差 性能优异
多视图同步 困难 简单(共享模型)
自定义数据 有限 高度灵活

二、基础模型类

2.1 标准模型类

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QStringListModel>
#include <QStandardItemModel>
#include <QListView>
#include <QTableView>
#include <QVBoxLayout>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    void setupUI();  // 创建UI
    void setupModels();  // 设置模型

    QListView *listView;
    QTableView *tableView;
    QStringListModel *listModel;// 1. QStringListModel - 列表数据模型
    QStandardItemModel *standardModel;// 2. QStandardItemModel - 通用模型
    QVBoxLayout *mainLayout;
};
#endif // WIDGET_H
#include "widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , listView(new QListView(this))
    , tableView(new QTableView(this))
    , listModel(new QStringListModel(this))
    , standardModel(new QStandardItemModel(this))
    , mainLayout(new QVBoxLayout(this))
{
    setupUI();
    setupModels();
}

Widget::~Widget()
{
    // Qt的对象树会自动管理内存,通常不需要手动删除
}

void Widget::setupUI()
{
    // 设置主布局
    setLayout(mainLayout);

    // 添加视图到布局
    mainLayout->addWidget(listView);
    mainLayout->addWidget(tableView);

    // 设置窗口属性
    setWindowTitle("Model/View Example");
    resize(600, 400);
}

void Widget::setupModels()
{
    // 1. 设置列表数据
    QStringList listData;
    listData << "Apple" << "Banana" << "Cherry" << "Date"
             << "Elderberry" << "Fig" << "Grape" << "Honeydew";

    listModel->setStringList(listData);
    listView->setModel(listModel);

    // 2. 设置表格数据
    // 设置表头
    standardModel->setHorizontalHeaderLabels({"Name", "Age", "Department", "Salary"});

    // 添加数据
    QList<QStandardItem*> row1;
    row1 << new QStandardItem("John Doe")
         << new QStandardItem("28")
         << new QStandardItem("Engineering")
         << new QStandardItem("$75,000");
    standardModel->appendRow(row1);

    QList<QStandardItem*> row2;
    row2 << new QStandardItem("Jane Smith")
         << new QStandardItem("32")
         << new QStandardItem("Marketing")
         << new QStandardItem("$65,000");
    standardModel->appendRow(row2);

    // 更多数据...

    tableView->setModel(standardModel);

    // 设置表格属性
    tableView->setAlternatingRowColors(true);
    //tableView->horizontalHeader()->setStretchLastSection(true);
}

运行效果:

image
 

2.2 自定义模型基础

#include <QAbstractTableModel>
#include <QVector>

class CustomTableModel : public QAbstractTableModel {
    Q_OBJECT

public:
    explicit CustomTableModel(QObject *parent = nullptr)
        : QAbstractTableModel(parent) {
        // 初始化示例数据
        m_data = {
            {"Alice", "25", "Sales"},
            {"Bob", "30", "Marketing"},
            {"Charlie", "35", "Engineering"}
        };
    }

    // 必须实现的纯虚函数
    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        Q_UNUSED(parent);
        return m_data.size();
    }

    int columnCount(const QModelIndex &parent = QModelIndex()) const override {
        Q_UNUSED(parent);
        return m_data.isEmpty() ? 0 : m_data[0].size();
    }

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
        if (!index.isValid() ||
            index.row() >= m_data.size() ||
            index.column() >= m_data[0].size()) {
            return QVariant();
        }

        if (role == Qt::DisplayRole || role == Qt::EditRole) {
            return m_data[index.row()][index.column()];
        }

        if (role == Qt::TextAlignmentRole) {
            return Qt::AlignCenter;
        }

        return QVariant();
    }

    QVariant headerData(int section, Qt::Orientation orientation,
                       int role = Qt::DisplayRole) const override {
        if (role != Qt::DisplayRole) return QVariant();

        if (orientation == Qt::Horizontal) {
            QStringList headers = {"Name", "Age", "Department"};
            return section < headers.size() ? headers[section] : QVariant();
        }

        return section + 1;  // 行号从1开始
    }

    // 可选:实现可编辑模型
    Qt::ItemFlags flags(const QModelIndex &index) const override {
        Qt::ItemFlags flags = QAbstractTableModel::flags(index);
        if (index.isValid()) {
            flags |= Qt::ItemIsEditable;
        }
        return flags;
    }

    bool setData(const QModelIndex &index, const QVariant &value,
                int role = Qt::EditRole) override {
        if (!index.isValid() || role != Qt::EditRole) {
            return false;
        }

        if (index.row() < m_data.size() && index.column() < m_data[0].size()) {
            m_data[index.row()][index.column()] = value.toString();
            emit dataChanged(index, index, {role});
            return true;
        }

        return false;
    }

    // 添加数据方法
    void addData(const QStringList &rowData)
    {
        if (rowData.size() < 3) {
            //qWarning() << "Invalid row data, expected 3 elements but got" << rowData.size();
            return;
        }

        beginInsertRows(QModelIndex(), m_data.size(), m_data.size());

        // 根据你的数据结构进行适配
        // 假设 m_data 是 QVector<QVector<QString>> 类型
        QVector<QString> newRow;
        newRow.append(rowData[0]);  // Name
        newRow.append(rowData[1]);  // Age
        newRow.append(rowData[2]);  // Department

        m_data.append(newRow);

        endInsertRows();
    }

    // 添加员工方法
    void addEmployee(const QString &name, const QString &age, const QString &department)
    {
        QStringList rowData;
        rowData << name << age << department;
        addData(rowData);
    }

    // 删除行
    void removeRow(int row)
    {
        if (row < 0 || row >= m_data.size()) {
            //qWarning() << "Invalid row index:" << row;
            return;
        }

        beginRemoveRows(QModelIndex(), row, row);
        m_data.remove(row);
        endRemoveRows();
    }

    // 删除员工(别名方法)
    void removeEmployee(int row)
    {
        removeRow(row);
    }

    // 清空所有数据
    void clearAll()
    {
        if (m_data.isEmpty()) return;

        beginRemoveRows(QModelIndex(), 0, m_data.size() - 1);
        m_data.clear();
        endRemoveRows();
    }

private:
    QVector<QVector<QString>> m_data;
};

使用自定义模型:

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QStringListModel>
#include <QStandardItemModel>
#include <QListView>
#include <QTableView>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include "CustomTableModel.h"

// Widget类
class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void onAddButtonClicked();
    void onRemoveButtonClicked();
    void onClearButtonClicked();
    void onTableViewDoubleClicked(const QModelIndex &index);

private:
    void setupUI();
    void setupModels();
    void setupConnections();

    // 界面控件
    QTableView *tableView;
    QPushButton *addButton;
    QPushButton *removeButton;
    QPushButton *clearButton;
    QLabel *statusLabel;
    QVBoxLayout *mainLayout;

    // 模型
    CustomTableModel *customModel;
};

#endif // WIDGET_H
#include "widget.h"
#include <QMessageBox>
#include <QInputDialog>
#include <QHeaderView>
#include <QHBoxLayout>

// Widget的实现
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , tableView(new QTableView(this))
    , addButton(new QPushButton("Add Employee", this))
    , removeButton(new QPushButton("Remove Selected", this))
    , clearButton(new QPushButton("Clear All", this))
    , statusLabel(new QLabel("Ready", this))
    , mainLayout(new QVBoxLayout(this))
    , customModel(new CustomTableModel(this))
{
    setupUI();
    setupModels();
    setupConnections();
}

Widget::~Widget()
{
    // 使用Qt对象树管理,不需要手动删除
}

void Widget::setupUI()
{
    // 设置窗口标题和大小
    setWindowTitle("Custom Table Model Demo");
    resize(800, 600);

    // 创建按钮布局
    QHBoxLayout *buttonLayout = new QHBoxLayout();
    buttonLayout->addWidget(addButton);
    buttonLayout->addWidget(removeButton);
    buttonLayout->addWidget(clearButton);
    buttonLayout->addStretch();  // 添加弹性空间

    // 设置状态标签
    statusLabel->setAlignment(Qt::AlignCenter);
    statusLabel->setStyleSheet("QLabel { padding: 5px; background-color: #f0f0f0; }");

    // 将控件添加到主布局
    mainLayout->addLayout(buttonLayout);
    mainLayout->addWidget(tableView);
    mainLayout->addWidget(statusLabel);

    // 设置主窗口布局
    setLayout(mainLayout);
}

void Widget::setupModels()
{
    // 设置自定义模型到QTableView
    tableView->setModel(customModel);

    // 设置表格属性
    tableView->setAlternatingRowColors(true);
    tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    tableView->setSelectionMode(QAbstractItemView::SingleSelection);

    // 设置列宽
    tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
    tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
    tableView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);

    // 设置行高
    tableView->verticalHeader()->setDefaultSectionSize(30);

    // 启用排序
    tableView->setSortingEnabled(true);

    // 设置表格样式
    tableView->setStyleSheet(
        "QTableView {"
        "    gridline-color: #d0d0d0;"
        "    selection-background-color: #4da6ff;"
        "}"
        "QHeaderView::section {"
        "    background-color: #e0e0e0;"
        "    padding: 4px;"
        "    border: 1px solid #d0d0d0;"
        "    font-weight: bold;"
        "}"
    );
}

void Widget::setupConnections()
{
    // 连接按钮信号
    connect(addButton, &QPushButton::clicked, this, &Widget::onAddButtonClicked);
    connect(removeButton, &QPushButton::clicked, this, &Widget::onRemoveButtonClicked);
    connect(clearButton, &QPushButton::clicked, this, &Widget::onClearButtonClicked);

    // 连接表格双击信号
    connect(tableView, &QTableView::doubleClicked, this, &Widget::onTableViewDoubleClicked);
}

void Widget::onAddButtonClicked()
{
    bool ok;
    QString name = QInputDialog::getText(this, "Add Employee",
                                         "Enter name:", QLineEdit::Normal, "", &ok);
    if (!ok || name.isEmpty()) return;

    QString age = QInputDialog::getText(this, "Add Employee",
                                        "Enter age:", QLineEdit::Normal, "", &ok);
    if (!ok || age.isEmpty()) return;

    QString department = QInputDialog::getText(this, "Add Employee",
                                               "Enter department:", QLineEdit::Normal, "", &ok);
    if (!ok || department.isEmpty()) return;

    // 添加数据到模型
    QStringList newRow = {name, age, department};

    // 这里需要确保CustomTableModel有addData方法
    // 如果没有,可能需要修改CustomTableModel或使用其他方法
    customModel->addData(newRow);

    statusLabel->setText(QString("Added: %1, %2, %3").arg(name).arg(age).arg(department));
}

void Widget::onRemoveButtonClicked()
{
    QModelIndex currentIndex = tableView->currentIndex();
    if (!currentIndex.isValid()) {
        QMessageBox::warning(this, "Warning", "Please select a row to remove.");
        return;
    }

    int row = currentIndex.row();
    QString name = customModel->data(customModel->index(row, 0)).toString();

    QMessageBox::StandardButton reply;
    reply = QMessageBox::question(this, "Confirm Removal",
                                  QString("Remove %1?").arg(name),
                                  QMessageBox::Yes | QMessageBox::No);

    if (reply == QMessageBox::Yes) {
        // 这里需要确保CustomTableModel有removeRow方法
        customModel->removeRow(row);
        statusLabel->setText(QString("Removed: %1").arg(name));
    }
}

void Widget::onClearButtonClicked()
{
    if (customModel->rowCount() == 0) return;

    QMessageBox::StandardButton reply;
    reply = QMessageBox::question(this, "Confirm Clear",
                                  "Clear all data?",
                                  QMessageBox::Yes | QMessageBox::No);

    if (reply == QMessageBox::Yes) {
        // 这里需要确保CustomTableModel有clearAll方法
        customModel->clearAll();
        statusLabel->setText("All data cleared.");
    }
}

void Widget::onTableViewDoubleClicked(const QModelIndex &index)
{
    if (!index.isValid()) return;

    int row = index.row();
    int col = index.column();

    QString name = customModel->data(customModel->index(row, 0)).toString();
    QString age = customModel->data(customModel->index(row, 1)).toString();
    QString dept = customModel->data(customModel->index(row, 2)).toString();

    QString message = QString("Double clicked on:\n"
                              "Row: %1, Column: %2\n"
                              "Name: %3\n"
                              "Age: %4\n"
                              "Department: %5")
                      .arg(row + 1).arg(col + 1).arg(name).arg(age).arg(dept);

    QMessageBox::information(this, "Item Details", message);
}

运行效果:

image

 

三、视图组件详解

3.1 常用视图部件

void viewComponentsDemo() {
    // 创建数据模型
    QStandardItemModel model(4, 3);
    for (int row = 0; row < 4; ++row) {
        for (int col = 0; col < 3; ++col) {
            QStandardItem *item = new QStandardItem(
                QString("Row %1, Col %2").arg(row).arg(col));
            model.setItem(row, col, item);
        }
    }
    
    // 1. QListView - 列表视图
    QListView listView;
    listView.setModel(&model);
    listView.setViewMode(QListView::IconMode); // 图标模式
    listView.setDragEnabled(true); // 启用拖放
    listView.show();
    
    // 2. QTableView - 表格视图
    QTableView tableView;
    tableView.setModel(&model);
    tableView.setSortingEnabled(true); // 启用排序
    tableView.horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    tableView.show();
    
    // 3. QTreeView - 树形视图
    QStandardItemModel treeModel;
    QStandardItem *root = new QStandardItem("Root");
    
    for (int i = 0; i < 3; ++i) {
        QStandardItem *child = new QStandardItem(QString("Child %1").arg(i));
        for (int j = 0; j < 2; ++j) {
            QStandardItem *grandChild = new QStandardItem(
                QString("Grandchild %1-%2").arg(i).arg(j));
            child->appendRow(grandChild);
        }
        root->appendRow(child);
    }
    
    treeModel.appendRow(root);
    
    QTreeView treeView;
    treeView.setModel(&treeModel);
    treeView.expandAll(); // 展开所有节点
    treeView.show();
}

3.2 视图选择和导航

void viewSelectionDemo() {
    QStandardItemModel model(5, 3);
    
    // 填充数据
    for (int row = 0; row < 5; ++row) {
        for (int col = 0; col < 3; ++col) {
            model.setData(model.index(row, col), 
                         QString("Item %1-%2").arg(row).arg(col));
        }
    }
    
    QTableView view;
    view.setModel(&model);
    
    // 设置选择模式
    view.setSelectionMode(QAbstractItemView::ExtendedSelection); // 多选
    view.setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选择
    
    // 获取选择模型
    QItemSelectionModel *selectionModel = view.selectionModel();
    
    // 连接选择变化的信号
    QObject::connect(selectionModel, &QItemSelectionModel::selectionChanged,
        [](const QItemSelection &selected, const QItemSelection &deselected) {
            qDebug() << "Selection changed";
            qDebug() << "Selected items:" << selected.indexes().size();
        });
    
    view.show();
}

四、委托(Delegate)系统

委托就是在视图组件上为编辑数据提供编辑器,如在表格组件中编辑一个单元格的数据时,缺省是使用一个 QLineEdit 编辑框。代理负责从数据模型获取相应的数据,然后显示在编辑器里,修改数据后,又将其保存到数据模型中。

QAbstractltemDelegate 是所有代理类的基类,作为抽象类,它不能直接使用。它的一个子类 QStyledltemDelegate,是 Qt 的视图组件缺省使用的代理类。

对于一些特殊的数据编辑需求,例如只允许输入整型数,使用一个 QSpinBox 作为代理组件更恰当,从列表中选择数据时使用一个 QComboBox 作为代理组件更好。这时,就可以从 QStyledltemDelegate 继承创建自定义代理类。

4.1 自定义委托

#ifndef CUSTOMDELEGATE_H
#define CUSTOMDELEGATE_H


#include <QStyledItemDelegate>
#include <QSpinBox>
#include <QComboBox>
#include <QPainter>

class CustomDelegate : public QStyledItemDelegate {
    Q_OBJECT

public:
    explicit CustomDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}

    // 创建编辑器
    QWidget *createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        // 根据列返回不同的编辑器
        if (index.column() == 1) { // 第二列使用SpinBox
            QSpinBox *editor = new QSpinBox(parent);
            editor->setRange(0, 100);
            editor->setSingleStep(1);
            return editor;
        } else if (index.column() == 2) { // 第三列使用ComboBox
            QComboBox *editor = new QComboBox(parent);
            editor->addItems({"Engineering", "Sales", "Marketing", "HR"});
            return editor;
        }

        // 其他列使用默认编辑器
        return QStyledItemDelegate::createEditor(parent, option, index);
    }

    // 设置编辑器数据
    void setEditorData(QWidget *editor,
                      const QModelIndex &index) const override {
        if (index.column() == 1) {
            QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
            int value = index.model()->data(index, Qt::EditRole).toInt();
            spinBox->setValue(value);
        } else if (index.column() == 2) {
            QComboBox *comboBox = static_cast<QComboBox*>(editor);
            QString value = index.model()->data(index, Qt::EditRole).toString();
            comboBox->setCurrentText(value);
        } else {
            QStyledItemDelegate::setEditorData(editor, index);
        }
    }

    // 将编辑器数据保存到模型
    void setModelData(QWidget *editor, QAbstractItemModel *model,
                     const QModelIndex &index) const override {
        if (index.column() == 1) {
            QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
            model->setData(index, spinBox->value(), Qt::EditRole);
        } else if (index.column() == 2) {
            QComboBox *comboBox = static_cast<QComboBox*>(editor);
            model->setData(index, comboBox->currentText(), Qt::EditRole);
        } else {
            QStyledItemDelegate::setModelData(editor, model, index);
        }
    }

    // 自定义绘制
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const override
    {
        // 高亮特定条件的单元格
        if (index.column() == 1)
        {
            int age = index.data().toInt();
            if (age > 60)
            {
                painter->fillRect(option.rect, QColor(255, 200, 200));
            }
        }

        QStyledItemDelegate::paint(painter, option, index);
    }
};

#endif // CUSTOMDELEGATE_H

4.2 使用自定义委托

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QStringListModel>
#include <QStandardItemModel>
#include <QListView>
#include <QTableView>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include "CustomTableModel.h"
#include "CustomDelegate.h"  // 添加委托类头文件

// Widget类
class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void onAddButtonClicked();
    void onRemoveButtonClicked();
    void onClearButtonClicked();
    void onTableViewDoubleClicked(const QModelIndex &index);

private:
    void setupUI();
    void setupModels();
    void setupConnections();
    void setupDelegate();  // 新增:设置委托的方法

    // 界面控件
    QTableView *tableView;
    QPushButton *addButton;
    QPushButton *removeButton;
    QPushButton *clearButton;
    QLabel *statusLabel;
    QVBoxLayout *mainLayout;

    // 模型和委托
    CustomTableModel *customModel;
    CustomDelegate *customDelegate;  // 自定义委托对象
};

#endif // WIDGET_H
#include "widget.h"
#include <QMessageBox>
#include <QInputDialog>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QApplication>
#include <QDesktopWidget>

// Widget的实现
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , tableView(new QTableView(this))
    , addButton(new QPushButton("Add Employee", this))
    , removeButton(new QPushButton("Remove Selected", this))
    , clearButton(new QPushButton("Clear All", this))
    , statusLabel(new QLabel("Ready", this))
    , mainLayout(new QVBoxLayout(this))
    , customModel(new CustomTableModel(this))
    , customDelegate(new CustomDelegate(this))  // 初始化委托对象
{
    setupUI();
    setupModels();
    setupDelegate();  // 设置委托
    setupConnections();
}

Widget::~Widget()
{
    // 使用Qt对象树管理,不需要手动删除
}

void Widget::setupUI()
{
    // 设置窗口标题和大小
    setWindowTitle("Custom Table Model with Delegate Demo");

    // 根据屏幕大小调整窗口尺寸
    QRect screenGeometry = QApplication::desktop()->screenGeometry();
    int width = screenGeometry.width() * 2 / 3;
    int height = screenGeometry.height() * 2 / 3;
    resize(width, height);

    // 创建按钮布局
    QHBoxLayout *buttonLayout = new QHBoxLayout();
    buttonLayout->addWidget(addButton);
    buttonLayout->addWidget(removeButton);
    buttonLayout->addWidget(clearButton);
    buttonLayout->addStretch();  // 添加弹性空间

    // 添加功能说明标签
    QLabel *hintLabel = new QLabel("提示:双击单元格可编辑,年龄列使用SpinBox,部门列使用下拉框", this);
    hintLabel->setStyleSheet("QLabel { color: #666666; font-style: italic; padding: 5px; }");

    // 设置状态标签
    statusLabel->setAlignment(Qt::AlignCenter);
    statusLabel->setStyleSheet("QLabel { padding: 8px; background-color: #f5f5f5; border: 1px solid #ddd; }");

    // 将控件添加到主布局
    mainLayout->addLayout(buttonLayout);
    mainLayout->addWidget(hintLabel);
    mainLayout->addWidget(tableView);
    mainLayout->addWidget(statusLabel);

    // 设置主窗口布局
    setLayout(mainLayout);
}

void Widget::setupModels()
{
    // 设置自定义模型到QTableView
    tableView->setModel(customModel);

    // 设置表格属性
    tableView->setAlternatingRowColors(true);
    tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);  // 允许多选

    // 设置列宽
    tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Interactive);
    tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
    tableView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);

    // 设置默认列宽
    tableView->setColumnWidth(0, 200);  // 姓名列宽
    tableView->setColumnWidth(1, 100);  // 年龄列宽

    // 设置行高
    tableView->verticalHeader()->setDefaultSectionSize(35);
    tableView->verticalHeader()->setVisible(true);  // 显示行号

    // 启用排序
    tableView->setSortingEnabled(true);

    // 设置表格样式
    tableView->setStyleSheet(
        "QTableView {"
        "    background-color: white;"
        "    gridline-color: #e0e0e0;"
        "    selection-background-color: #b3d9ff;"
        "    selection-color: black;"
        "}"
        "QTableView::item {"
        "    padding: 5px;"
        "    border-bottom: 1px solid #f0f0f0;"
        "}"
        "QTableView::item:selected {"
        "    background-color: #b3d9ff;"
        "}"
        "QHeaderView::section {"
        "    background-color: #4a86e8;"
        "    color: white;"
        "    padding: 8px;"
        "    border: 1px solid #357ae8;"
        "    font-weight: bold;"
        "}"
        "QHeaderView::section:checked {"
        "    background-color: #3c78d8;"
        "}"
    );
}

void Widget::setupDelegate()
{
    // 为整个表格设置自定义委托
    tableView->setItemDelegate(customDelegate);

    // 或者可以为特定列设置不同的委托(如果需要)
    // tableView->setItemDelegateForColumn(1, customDelegate); // 只对年龄列设置
    // tableView->setItemDelegateForColumn(2, customDelegate); // 只对部门列设置

    // 启用编辑触发方式
    tableView->setEditTriggers(
        QAbstractItemView::DoubleClicked |    // 双击编辑
        QAbstractItemView::SelectedClicked |  // 选中后单击编辑
        QAbstractItemView::EditKeyPressed |   // 按F2键编辑
        QAbstractItemView::AnyKeyPressed      // 输入任意字符开始编辑
    );

    statusLabel->setText("自定义委托已启用:年龄列使用SpinBox,部门列使用下拉框");
}

void Widget::setupConnections()
{
    // 连接按钮信号
    connect(addButton, &QPushButton::clicked, this, &Widget::onAddButtonClicked);
    connect(removeButton, &QPushButton::clicked, this, &Widget::onRemoveButtonClicked);
    connect(clearButton, &QPushButton::clicked, this, &Widget::onClearButtonClicked);

    // 连接表格双击信号
    connect(tableView, &QTableView::doubleClicked, this, &Widget::onTableViewDoubleClicked);

    // 连接模型数据变化信号
    connect(customModel, &CustomTableModel::dataChanged, this, [this]() {
        statusLabel->setText(QString("数据已更新,当前行数:%1").arg(customModel->rowCount()));
    });
}

void Widget::onAddButtonClicked()
{
    bool ok;
    QString name = QInputDialog::getText(this, "Add Employee",
                                         "Enter name:", QLineEdit::Normal, "", &ok);
    if (!ok || name.isEmpty()) return;

    // 使用SpinBox输入年龄
    int age = QInputDialog::getInt(this, "Add Employee",
                                   "Enter age:", 25, 18, 65, 1, &ok);
    if (!ok) return;

    // 使用ComboBox选择部门
    QStringList departments = {"Engineering", "Sales", "Marketing", "HR", "Finance", "Operations"};
    QString department = QInputDialog::getItem(this, "Add Employee",
                                               "Select department:",
                                               departments, 0, false, &ok);
    if (!ok || department.isEmpty()) return;

    // 添加数据到模型
    // 根据你的CustomTableModel的实际方法名调用
    // 如果方法名是addEmployee,则:
    customModel->addEmployee(name, QString::number(age), department);
    // 或者如果方法名是addData,则:
    // customModel->addData(QStringList() << name << QString::number(age) << department);

    statusLabel->setText(QString("Added: %1, %2, %3").arg(name).arg(age).arg(department));
}

void Widget::onRemoveButtonClicked()
{
    QModelIndexList selectedIndexes = tableView->selectionModel()->selectedRows();
    if (selectedIndexes.isEmpty()) {
        QMessageBox::warning(this, "Warning", "Please select row(s) to remove.");
        return;
    }

    QStringList names;
    QList<int> rows;
    for (const QModelIndex &index : selectedIndexes) {
        int row = index.row();
        QString name = customModel->data(customModel->index(row, 0)).toString();
        names.append(name);
        rows.append(row);
    }

    QMessageBox::StandardButton reply;
    reply = QMessageBox::question(this, "Confirm Removal",
                                  QString("Remove %1 selected employee(s)?").arg(names.size()),
                                  QMessageBox::Yes | QMessageBox::No);

    if (reply == QMessageBox::Yes) {
        // 从后往前删除,避免索引变化
        std::sort(rows.begin(), rows.end(), std::greater<int>());
        for (int row : rows) {
            customModel->removeRow(row);
        }
        statusLabel->setText(QString("Removed %1 employee(s)").arg(rows.size()));
    }
}

void Widget::onClearButtonClicked()
{
    if (customModel->rowCount() == 0) return;

    QMessageBox::StandardButton reply;
    reply = QMessageBox::question(this, "Confirm Clear",
                                  "Clear all data?",
                                  QMessageBox::Yes | QMessageBox::No);

    if (reply == QMessageBox::Yes) {
        customModel->clearAll();
        statusLabel->setText("All data cleared.");
    }
}

void Widget::onTableViewDoubleClicked(const QModelIndex &index)
{
    if (!index.isValid()) return;

    int row = index.row();
    int col = index.column();

    QString name = customModel->data(customModel->index(row, 0)).toString();
    QString age = customModel->data(customModel->index(row, 1)).toString();
    QString dept = customModel->data(customModel->index(row, 2)).toString();

    QString columnName;
    switch (col) {
        case 0: columnName = "Name"; break;
        case 1: columnName = "Age"; break;
        case 2: columnName = "Department"; break;
        default: columnName = "Unknown"; break;
    }

    QString message = QString("Double clicked on:\n"
                              "Row: %1, Column: %2 (%3)\n"
                              "Name: %4\n"
                              "Age: %5\n"
                              "Department: %6")
                      .arg(row + 1)
                      .arg(col + 1)
                      .arg(columnName)
                      .arg(name)
                      .arg(age)
                      .arg(dept);

    QMessageBox::information(this, "Item Details", message);
}

运行效果:

image

 

五、实战案例:联系人管理系统

5.1 完整示例代码

#include <QApplication>
#include <QMainWindow>
#include <QTableView>
#include <QHeaderView>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QMessageBox>
#include <QFileDialog>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>

class ContactModel : public QAbstractTableModel {
    Q_OBJECT
    
public:
    enum Columns {
        NameColumn = 0,
        PhoneColumn,
        EmailColumn,
        DepartmentColumn,
        ColumnCount
    };
    
    explicit ContactModel(QObject *parent = nullptr)
        : QAbstractTableModel(parent) {}
    
    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        return parent.isValid() ? 0 : m_contacts.size();
    }
    
    int columnCount(const QModelIndex &parent = QModelIndex()) const override {
        return parent.isValid() ? 0 : ColumnCount;
    }
    
    QVariant data(const QModelIndex &index, int role) const override {
        if (!index.isValid() || index.row() >= m_contacts.size()) {
            return QVariant();
        }
        
        const Contact &contact = m_contacts.at(index.row());
        
        switch (role) {
        case Qt::DisplayRole:
        case Qt::EditRole:
            switch (index.column()) {
            case NameColumn: return contact.name;
            case PhoneColumn: return contact.phone;
            case EmailColumn: return contact.email;
            case DepartmentColumn: return contact.department;
            }
            break;
            
        case Qt::TextAlignmentRole:
            return Qt::AlignLeft + Qt::AlignVCenter;
            
        case Qt::ToolTipRole:
            return QString("Phone: %1\nEmail: %2").arg(contact.phone).arg(contact.email);
        }
        
        return QVariant();
    }
    
    QVariant headerData(int section, Qt::Orientation orientation, 
                       int role) const override {
        if (role != Qt::DisplayRole) return QVariant();
        
        if (orientation == Qt::Horizontal) {
            switch (section) {
            case NameColumn: return "Name";
            case PhoneColumn: return "Phone";
            case EmailColumn: return "Email";
            case DepartmentColumn: return "Department";
            }
        }
        
        return section + 1;
    }
    
    bool setData(const QModelIndex &index, const QVariant &value, 
                int role = Qt::EditRole) override {
        if (!index.isValid() || role != Qt::EditRole) {
            return false;
        }
        
        if (index.row() < m_contacts.size()) {
            Contact &contact = m_contacts[index.row()];
            
            switch (index.column()) {
            case NameColumn: contact.name = value.toString(); break;
            case PhoneColumn: contact.phone = value.toString(); break;
            case EmailColumn: contact.email = value.toString(); break;
            case DepartmentColumn: contact.department = value.toString(); break;
            default: return false;
            }
            
            emit dataChanged(index, index, {role});
            return true;
        }
        
        return false;
    }
    
    Qt::ItemFlags flags(const QModelIndex &index) const override {
        Qt::ItemFlags flags = QAbstractTableModel::flags(index);
        if (index.isValid()) {
            flags |= Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
        }
        return flags;
    }
    
    bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
        if (row < 0 || row > m_contacts.size()) return false;
        
        beginInsertRows(parent, row, row + count - 1);
        for (int i = 0; i < count; ++i) {
            m_contacts.insert(row + i, Contact());
        }
        endInsertRows();
        
        return true;
    }
    
    bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
        if (row < 0 || row + count > m_contacts.size()) return false;
        
        beginRemoveRows(parent, row, row + count - 1);
        for (int i = 0; i < count; ++i) {
            m_contacts.removeAt(row);
        }
        endRemoveRows();
        
        return true;
    }
    
    // 添加联系人
    void addContact(const QString &name, const QString &phone, 
                   const QString &email, const QString &department) {
        int row = m_contacts.size();
        beginInsertRows(QModelIndex(), row, row);
        m_contacts.append(Contact{name, phone, email, department});
        endInsertRows();
    }
    
    // 保存到文件
    bool saveToFile(const QString &filename) {
        QJsonArray contactsArray;
        
        for (const Contact &contact : m_contacts) {
            QJsonObject obj;
            obj["name"] = contact.name;
            obj["phone"] = contact.phone;
            obj["email"] = contact.email;
            obj["department"] = contact.department;
            contactsArray.append(obj);
        }
        
        QJsonDocument doc(contactsArray);
        QFile file(filename);
        if (!file.open(QIODevice::WriteOnly)) {
            return false;
        }
        
        file.write(doc.toJson());
        file.close();
        return true;
    }
    
    // 从文件加载
    bool loadFromFile(const QString &filename) {
        QFile file(filename);
        if (!file.open(QIODevice::ReadOnly)) {
            return false;
        }
        
        QByteArray data = file.readAll();
        file.close();
        
        QJsonDocument doc = QJsonDocument::fromJson(data);
        if (!doc.isArray()) {
            return false;
        }
        
        beginResetModel();
        m_contacts.clear();
        
        QJsonArray array = doc.array();
        for (const QJsonValue &value : array) {
            QJsonObject obj = value.toObject();
            m_contacts.append(Contact{
                obj["name"].toString(),
                obj["phone"].toString(),
                obj["email"].toString(),
                obj["department"].toString()
            });
        }
        
        endResetModel();
        return true;
    }
    
private:
    struct Contact {
        QString name;
        QString phone;
        QString email;
        QString department;
    };
    
    QVector<Contact> m_contacts;
};

class MainWindow : public QMainWindow {
    Q_OBJECT
    
public:
    MainWindow() {
        setupUI();
        setupConnections();
        
        // 添加示例数据
        m_model.addContact("John Doe", "123-456-7890", 
                          "john@example.com", "Engineering");
        m_model.addContact("Jane Smith", "987-654-3210", 
                          "jane@example.com", "Marketing");
    }
    
private:
    void setupUI() {
        // 创建中央部件
        QWidget *centralWidget = new QWidget(this);
        QVBoxLayout *layout = new QVBoxLayout(centralWidget);
        
        // 创建表格视图
        m_tableView = new QTableView;
        m_tableView->setModel(&m_model);
        m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);
        m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_tableView->horizontalHeader()->setStretchLastSection(true);
        m_tableView->setAlternatingRowColors(true);
        
        layout->addWidget(m_tableView);
        setCentralWidget(centralWidget);
        
        // 创建菜单
        QMenu *fileMenu = menuBar()->addMenu("File");
        fileMenu->addAction("Add Contact", this, &MainWindow::addContact);
        fileMenu->addAction("Remove Contact", this, &MainWindow::removeContact);
        fileMenu->addSeparator();
        fileMenu->addAction("Save", this, &MainWindow::saveContacts);
        fileMenu->addAction("Load", this, &MainWindow::loadContacts);
        fileMenu->addSeparator();
        fileMenu->addAction("Exit", qApp, &QApplication::quit);
        
        // 创建工具栏
        QToolBar *toolBar = addToolBar("Tools");
        toolBar->addAction("Add Contact", this, &MainWindow::addContact);
        toolBar->addAction("Remove Contact", this, &MainWindow::removeContact);
        
        // 状态栏
        statusBar()->showMessage("Ready");
        
        setWindowTitle("Contact Manager");
        resize(800, 600);
    }
    
    void setupConnections() {
        connect(m_tableView->selectionModel(), &QItemSelectionModel::selectionChanged,
                this, &MainWindow::updateStatusBar);
    }
    
private slots:
    void addContact() {
        int row = m_model.rowCount();
        if (m_model.insertRow(row)) {
            QModelIndex index = m_model.index(row, 0);
            m_tableView->setCurrentIndex(index);
            m_tableView->edit(index);
        }
    }
    
    void removeContact() {
        QModelIndexList selected = m_tableView->selectionModel()->selectedRows();
        if (selected.isEmpty()) {
            QMessageBox::warning(this, "Warning", "Please select a contact to remove");
            return;
        }
        
        int row = selected.first().row();
        m_model.removeRow(row);
    }
    
    void saveContacts() {
        QString filename = QFileDialog::getSaveFileName(this, "Save Contacts",
                                                       "", "JSON Files (*.json)");
        if (!filename.isEmpty()) {
            if (m_model.saveToFile(filename)) {
                statusBar()->showMessage("Contacts saved successfully", 3000);
            } else {
                QMessageBox::critical(this, "Error", "Failed to save contacts");
            }
        }
    }
    
    void loadContacts() {
        QString filename = QFileDialog::getOpenFileName(this, "Load Contacts",
                                                       "", "JSON Files (*.json)");
        if (!filename.isEmpty()) {
            if (m_model.loadFromFile(filename)) {
                statusBar()->showMessage("Contacts loaded successfully", 3000);
            } else {
                QMessageBox::critical(this, "Error", "Failed to load contacts");
            }
        }
    }
    
    void updateStatusBar() {
        int selectedCount = m_tableView->selectionModel()->selectedRows().size();
        int totalCount = m_model.rowCount();
        statusBar()->showMessage(
            QString("Total: %1 | Selected: %2").arg(totalCount).arg(selectedCount));
    }
    
private:
    ContactModel m_model;
    QTableView *m_tableView;
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    MainWindow window;
    window.show();
    
    return app.exec();
}

运行效果:

 

posted @ 2022-07-17 09:34  [BORUTO]  阅读(329)  评论(0)    收藏  举报