Qt Model/View架构详解(二):内置视图与模型

Qt Model/View架构详解

重要程度 : ⭐⭐⭐⭐⭐
实战价值 : 处理复杂数据展示(表格、树形结构、列表)
学习目标 : 掌握Qt的Model/View设计模式,能够自定义Model和Delegate处理复杂数据展示需求
本篇要点: 了解和会使用Qt自带的Model和View。


📚 目录

第二部分:内置视图与模型 (第4-5章)

第4章 视图类详解

  • 4.1 QListView - 列表视图
  • 4.2 QTableView - 表格视图
  • 4.3 QTreeView - 树形视图
  • 4.4 视图的通用属性和方法

第5章 便捷模型类

  • 5.1 QStringListModel - 字符串列表模型
  • 5.2 QStandardItemModel - 标准项模型
  • 5.3 QFileSystemModel - 文件系统模型
  • 5.4 QSqlTableModel 和 QSqlQueryModel

第4章 标准视图类的使用

4.1 QListView - 列表视图

QListView 是用于显示列表数据的视图类,支持单列或图标模式显示。

4.1.1 QListView基本用法

最简单的使用

cpp 复制代码
#include <QApplication>
#include <QListView>
#include <QStringListModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建模型
    QStringListModel *model = new QStringListModel;
    QStringList list;
    list << "Item 1" << "Item 2" << "Item 3" << "Item 4" << "Item 5";
    model->setStringList(list);
    
    // 创建视图
    QListView *view = new QListView;
    view->setModel(model);
    
    view->show();
    
    return app.exec();
}

常用属性设置

cpp 复制代码
QListView *view = new QListView;

// 设置是否允许编辑
view->setEditTriggers(QAbstractItemView::DoubleClicked | 
                      QAbstractItemView::EditKeyPressed);

// 设置间距
view->setSpacing(5);

// 设置是否允许拖动
view->setDragEnabled(true);
view->setAcceptDrops(true);
view->setDropIndicatorShown(true);

// 设置是否显示网格
view->setGridSize(QSize(100, 100));

// 设置是否自动换行
view->setWordWrap(true);

// 设置统一的项目大小(性能优化)
view->setUniformItemSizes(true);

// 设置滚动模式
view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);  // 像素级滚动
// view->setVerticalScrollMode(QAbstractItemView::ScrollPerItem);  // 项级滚动

4.1.2 视图模式:ListMode vs IconMode

ListMode - 列表模式(默认):

cpp 复制代码
view->setViewMode(QListView::ListMode);
view->setFlow(QListView::TopToBottom);  // 从上到下排列
// view->setFlow(QListView::LeftToRight);  // 从左到右排列

IconMode - 图标模式

cpp 复制代码
view->setViewMode(QListView::IconMode);
view->setIconSize(QSize(64, 64));
view->setGridSize(QSize(100, 100));
view->setSpacing(10);
view->setMovement(QListView::Free);  // 可以自由移动图标
// view->setMovement(QListView::Static);  // 图标位置固定
// view->setMovement(QListView::Snap);    // 图标吸附到网格
view->setResizeMode(QListView::Adjust);  // 自动调整布局

完整示例:切换视图模式

cpp 复制代码
#include <QApplication>
#include <QListView>
#include <QStandardItemModel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建模型
    QStandardItemModel *model = new QStandardItemModel;
    for (int i = 0; i < 20; ++i) {
        QStandardItem *item = new QStandardItem(QIcon(":/icons/file.png"), 
                                                QString("Item %1").arg(i));
        model->appendRow(item);
    }
    
    // 创建视图
    QListView *view = new QListView;
    view->setModel(model);
    
    // 切换按钮
    QPushButton *listModeBtn = new QPushButton("列表模式");
    QPushButton *iconModeBtn = new QPushButton("图标模式");
    
    QObject::connect(listModeBtn, &QPushButton::clicked, [=]() {
        view->setViewMode(QListView::ListMode);
        view->setFlow(QListView::TopToBottom);
    });
    
    QObject::connect(iconModeBtn, &QPushButton::clicked, [=]() {
        view->setViewMode(QListView::IconMode);
        view->setIconSize(QSize(48, 48));
        view->setGridSize(QSize(80, 80));
        view->setMovement(QListView::Snap);
        view->setResizeMode(QListView::Adjust);
    });
    
    // 布局
    QWidget window;
    QVBoxLayout *mainLayout = new QVBoxLayout;
    QHBoxLayout *btnLayout = new QHBoxLayout;
    btnLayout->addWidget(listModeBtn);
    btnLayout->addWidget(iconModeBtn);
    
    mainLayout->addWidget(view);
    mainLayout->addLayout(btnLayout);
    window.setLayout(mainLayout);
    
    window.resize(400, 500);
    window.show();
    
    return app.exec();
}

4.1.3 选择模式设置

选择模式类型

cpp 复制代码
// 单选(默认)
view->setSelectionMode(QAbstractItemView::SingleSelection);

// 多选(连续)
view->setSelectionMode(QAbstractItemView::ContiguousSelection);

// 多选(扩展,Ctrl+点击)
view->setSelectionMode(QAbstractItemView::ExtendedSelection);

// 多选(任意)
view->setSelectionMode(QAbstractItemView::MultiSelection);

// 不可选
view->setSelectionMode(QAbstractItemView::NoSelection);

选择行为

cpp 复制代码
// 选择整个项(默认)
view->setSelectionBehavior(QAbstractItemView::SelectItems);

// 选择整行
view->setSelectionBehavior(QAbstractItemView::SelectRows);

// 选择整列
view->setSelectionBehavior(QAbstractItemView::SelectColumns);

获取选中项

cpp 复制代码
// 获取当前项
QModelIndex current = view->currentIndex();
if (current.isValid()) {
    QString text = model->data(current, Qt::DisplayRole).toString();
    qDebug() << "Current:" << text;
}

// 获取所有选中项
QModelIndexList selected = view->selectionModel()->selectedIndexes();
for (const QModelIndex &index : selected) {
    qDebug() << model->data(index).toString();
}

// 监听选择变化
QObject::connect(view->selectionModel(), 
                 &QItemSelectionModel::selectionChanged,
                 [=](const QItemSelection &selected, const QItemSelection &deselected) {
    qDebug() << "Selection changed!";
    qDebug() << "Selected count:" << selected.indexes().size();
});

4.1.4 拖放支持

启用拖放

cpp 复制代码
// 基本拖放设置
view->setDragEnabled(true);
view->setAcceptDrops(true);
view->setDropIndicatorShown(true);
view->setDragDropMode(QAbstractItemView::InternalMove);  // 只在内部移动

// 其他拖放模式
// view->setDragDropMode(QAbstractItemView::DragOnly);    // 只能拖出
// view->setDragDropMode(QAbstractItemView::DropOnly);    // 只能拖入
// view->setDragDropMode(QAbstractItemView::DragDrop);    // 可拖入拖出

模型需要支持拖放

cpp 复制代码
class DraggableListModel : public QStringListModel {
public:
    // 支持的拖放操作
    Qt::DropActions supportedDropActions() const override {
        return Qt::MoveAction | Qt::CopyAction;
    }
    
    Qt::ItemFlags flags(const QModelIndex &index) const override {
        Qt::ItemFlags defaultFlags = QStringListModel::flags(index);
        
        if (index.isValid())
            return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
        else
            return Qt::ItemIsDropEnabled | defaultFlags;
    }
    
    // 可选:自定义拖放的 MIME 类型
    QStringList mimeTypes() const override {
        return QStringList() << "application/x-qabstractitemmodeldatalist";
    }
};

完整拖放示例

cpp 复制代码
#include <QApplication>
#include <QListView>
#include <QStringListModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    QStringListModel *model = new QStringListModel;
    QStringList list = {"Item 1", "Item 2", "Item 3", "Item 4", "Item 5"};
    model->setStringList(list);
    
    QListView *view = new QListView;
    view->setModel(model);
    
    // 启用拖放
    view->setDragEnabled(true);
    view->setAcceptDrops(true);
    view->setDropIndicatorShown(true);
    view->setDragDropMode(QAbstractItemView::InternalMove);
    view->setDefaultDropAction(Qt::MoveAction);
    
    view->resize(300, 400);
    view->show();
    
    return app.exec();
}

4.1.5 实战:图片浏览器

这个示例实现一个简单的图片浏览器,支持缩略图显示。

cpp 复制代码
#include <QApplication>
#include <QListView>
#include <QStandardItemModel>
#include <QFileDialog>
#include <QPushButton>
#include <QVBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QStyledItemDelegate>
#include <QPainter>

// 自定义代理:绘制缩略图
class ThumbnailDelegate : public QStyledItemDelegate {
public:
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        if (index.data(Qt::UserRole).canConvert<QString>()) {
            QString imagePath = index.data(Qt::UserRole).toString();
            QPixmap pixmap(imagePath);
            
            if (!pixmap.isNull()) {
                painter->save();
                
                // 绘制背景
                if (option.state & QStyle::State_Selected) {
                    painter->fillRect(option.rect, option.palette.highlight());
                }
                
                // 缩放图片以适应单元格
                QPixmap scaled = pixmap.scaled(option.rect.size() - QSize(10, 30),
                                               Qt::KeepAspectRatio,
                                               Qt::SmoothTransformation);
                
                // 居中绘制图片
                int x = option.rect.x() + (option.rect.width() - scaled.width()) / 2;
                int y = option.rect.y() + 5;
                painter->drawPixmap(x, y, scaled);
                
                // 绘制文件名
                QString fileName = index.data(Qt::DisplayRole).toString();
                QRect textRect(option.rect.x(), option.rect.bottom() - 25,
                              option.rect.width(), 20);
                painter->setPen(option.state & QStyle::State_Selected ? 
                               Qt::white : Qt::black);
                painter->drawText(textRect, Qt::AlignCenter, fileName);
                
                painter->restore();
                return;
            }
        }
        
        QStyledItemDelegate::paint(painter, option, index);
    }
    
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        return QSize(150, 150);
    }
};

// 图片浏览器
class ImageBrowser : public QWidget {
    Q_OBJECT
    
private:
    QListView *m_listView;
    QStandardItemModel *m_model;
    QLabel *m_previewLabel;
    
public:
    ImageBrowser(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
    }
    
private:
    void setupUI() {
        // 创建模型和视图
        m_model = new QStandardItemModel(this);
        m_listView = new QListView;
        m_listView->setModel(m_model);
        m_listView->setViewMode(QListView::IconMode);
        m_listView->setIconSize(QSize(128, 128));
        m_listView->setGridSize(QSize(150, 150));
        m_listView->setResizeMode(QListView::Adjust);
        m_listView->setMovement(QListView::Static);
        m_listView->setItemDelegate(new ThumbnailDelegate);
        
        // 预览标签
        m_previewLabel = new QLabel("选择图片以预览");
        m_previewLabel->setAlignment(Qt::AlignCenter);
        m_previewLabel->setMinimumSize(300, 300);
        m_previewLabel->setStyleSheet("border: 1px solid gray;");
        m_previewLabel->setScaledContents(true);
        
        // 按钮
        QPushButton *loadBtn = new QPushButton("加载图片");
        QPushButton *clearBtn = new QPushButton("清空");
        
        connect(loadBtn, &QPushButton::clicked, this, &ImageBrowser::loadImages);
        connect(clearBtn, &QPushButton::clicked, [=]() {
            m_model->clear();
            m_previewLabel->clear();
            m_previewLabel->setText("选择图片以预览");
        });
        
        // 监听选择变化
        connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged,
                this, &ImageBrowser::onImageSelected);
        
        // 布局
        QVBoxLayout *leftLayout = new QVBoxLayout;
        leftLayout->addWidget(new QLabel("图片列表:"));
        leftLayout->addWidget(m_listView);
        
        QHBoxLayout *btnLayout = new QHBoxLayout;
        btnLayout->addWidget(loadBtn);
        btnLayout->addWidget(clearBtn);
        leftLayout->addLayout(btnLayout);
        
        QVBoxLayout *rightLayout = new QVBoxLayout;
        rightLayout->addWidget(new QLabel("预览:"));
        rightLayout->addWidget(m_previewLabel);
        
        QHBoxLayout *mainLayout = new QHBoxLayout;
        mainLayout->addLayout(leftLayout, 2);
        mainLayout->addLayout(rightLayout, 1);
        
        setLayout(mainLayout);
        resize(800, 500);
    }
    
private slots:
    void loadImages() {
        QStringList files = QFileDialog::getOpenFileNames(
            this, "选择图片", "",
            "Images (*.png *.jpg *.jpeg *.bmp *.gif)");
        
        for (const QString &file : files) {
            QFileInfo info(file);
            QStandardItem *item = new QStandardItem(info.fileName());
            item->setData(file, Qt::UserRole);  // 存储完整路径
            item->setToolTip(file);
            m_model->appendRow(item);
        }
    }
    
    void onImageSelected(const QModelIndex &current, const QModelIndex &previous) {
        Q_UNUSED(previous);
        
        if (!current.isValid()) {
            m_previewLabel->clear();
            return;
        }
        
        QString imagePath = current.data(Qt::UserRole).toString();
        QPixmap pixmap(imagePath);
        
        if (!pixmap.isNull()) {
            m_previewLabel->setPixmap(pixmap.scaled(m_previewLabel->size(),
                                                    Qt::KeepAspectRatio,
                                                    Qt::SmoothTransformation));
        }
    }
};

4.1.6 实战:音乐播放列表

这个示例实现一个音乐播放列表管理器。

cpp 复制代码
#include <QApplication>
#include <QListView>
#include <QStandardItemModel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTime>
#include <QMenu>

// 音乐信息结构
struct MusicInfo {
    QString title;
    QString artist;
    QString album;
    QTime duration;
    QString filePath;
};
Q_DECLARE_METATYPE(MusicInfo)

// 音乐播放列表
class MusicPlaylist : public QWidget {
    Q_OBJECT
    
private:
    QListView *m_listView;
    QStandardItemModel *m_model;
    int m_currentPlaying = -1;
    
public:
    MusicPlaylist(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        addSampleMusic();
    }
    
private:
    void setupUI() {
        m_model = new QStandardItemModel(this);
        
        m_listView = new QListView;
        m_listView->setModel(m_model);
        m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
        m_listView->setAlternatingRowColors(true);
        m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
        
        // 双击播放
        connect(m_listView, &QListView::doubleClicked,
                this, &MusicPlaylist::playMusic);
        
        // 右键菜单
        connect(m_listView, &QListView::customContextMenuRequested,
                this, &MusicPlaylist::showContextMenu);
        
        // 按钮
        QPushButton *playBtn = new QPushButton("播放");
        QPushButton *addBtn = new QPushButton("添加");
        QPushButton *removeBtn = new QPushButton("移除");
        
        connect(playBtn, &QPushButton::clicked, [=]() {
            QModelIndex current = m_listView->currentIndex();
            if (current.isValid())
                playMusic(current);
        });
        
        connect(addBtn, &QPushButton::clicked, this, &MusicPlaylist::addMusic);
        
        connect(removeBtn, &QPushButton::clicked, [=]() {
            QModelIndex current = m_listView->currentIndex();
            if (current.isValid())
                m_model->removeRow(current.row());
        });
        
        // 布局
        QVBoxLayout *mainLayout = new QVBoxLayout;
        mainLayout->addWidget(new QLabel("播放列表:"));
        mainLayout->addWidget(m_listView);
        
        QHBoxLayout *btnLayout = new QHBoxLayout;
        btnLayout->addWidget(playBtn);
        btnLayout->addWidget(addBtn);
        btnLayout->addWidget(removeBtn);
        mainLayout->addLayout(btnLayout);
        
        setLayout(mainLayout);
        resize(400, 500);
    }
    
    void addSampleMusic() {
        // 添加示例音乐
        QList<MusicInfo> samples = {
            {"Song 1", "Artist A", "Album X", QTime(0, 3, 45), "/path/song1.mp3"},
            {"Song 2", "Artist B", "Album Y", QTime(0, 4, 12), "/path/song2.mp3"},
            {"Song 3", "Artist A", "Album Z", QTime(0, 3, 28), "/path/song3.mp3"},
            {"Song 4", "Artist C", "Album X", QTime(0, 5, 03), "/path/song4.mp3"},
        };
        
        for (const MusicInfo &info : samples) {
            addMusicItem(info);
        }
    }
    
    void addMusicItem(const MusicInfo &info) {
        QStandardItem *item = new QStandardItem;
        
        // 显示文本
        QString displayText = QString("%1 - %2 [%3]")
            .arg(info.title)
            .arg(info.artist)
            .arg(info.duration.toString("mm:ss"));
        item->setText(displayText);
        
        // 存储完整信息
        item->setData(QVariant::fromValue(info), Qt::UserRole);
        
        // 工具提示
        QString tooltip = QString("标题: %1\n艺术家: %2\n专辑: %3\n时长: %4")
            .arg(info.title, info.artist, info.album)
            .arg(info.duration.toString("mm:ss"));
        item->setToolTip(tooltip);
        
        // 图标
        item->setIcon(QIcon(":/icons/music.png"));
        
        m_model->appendRow(item);
    }
    
private slots:
    void playMusic(const QModelIndex &index) {
        if (!index.isValid())
            return;
        
        // 清除之前的播放标记
        if (m_currentPlaying >= 0) {
            QStandardItem *prevItem = m_model->item(m_currentPlaying);
            QFont font = prevItem->font();
            font.setBold(false);
            prevItem->setFont(font);
            prevItem->setForeground(Qt::black);
        }
        
        // 标记当前播放
        m_currentPlaying = index.row();
        QStandardItem *item = m_model->item(m_currentPlaying);
        QFont font = item->font();
        font.setBold(true);
        item->setFont(font);
        item->setForeground(QColor(0, 120, 0));
        
        // 获取音乐信息
        MusicInfo info = index.data(Qt::UserRole).value<MusicInfo>();
        
        qDebug() << "正在播放:" << info.title << "-" << info.artist;
        // 这里可以调用实际的播放器 API
    }
    
    void addMusic() {
        // 简化版:实际应该打开文件对话框选择音乐文件
        MusicInfo newSong = {
            "New Song",
            "Unknown Artist",
            "Unknown Album",
            QTime(0, 3, 0),
            "/path/new.mp3"
        };
        addMusicItem(newSong);
    }
    
    void showContextMenu(const QPoint &pos) {
        QModelIndex index = m_listView->indexAt(pos);
        if (!index.isValid())
            return;
        
        QMenu menu;
        QAction *playAction = menu.addAction(QIcon(":/icons/play.png"), "播放");
        QAction *infoAction = menu.addAction(QIcon(":/icons/info.png"), "详细信息");
        menu.addSeparator();
        QAction *removeAction = menu.addAction(QIcon(":/icons/delete.png"), "移除");
        
        QAction *selected = menu.exec(m_listView->viewport()->mapToGlobal(pos));
        
        if (selected == playAction) {
            playMusic(index);
        } else if (selected == infoAction) {
            MusicInfo info = index.data(Qt::UserRole).value<MusicInfo>();
            QString msg = QString("标题: %1\n艺术家: %2\n专辑: %3\n时长: %4\n路径: %5")
                .arg(info.title, info.artist, info.album)
                .arg(info.duration.toString("mm:ss"))
                .arg(info.filePath);
            qDebug() << msg;
        } else if (selected == removeAction) {
            m_model->removeRow(index.row());
        }
    }
};

本节小结

QListView 支持列表和图标两种显示模式

选择模式 包括单选、多选、连续选择等

拖放支持 需要模型和视图配合实现

自定义代理 可以实现复杂的项目渲染

适用场景 图片浏览、音乐列表、文件管理等

  • QListView基本用法
  • 视图模式:ListMode vs IconMode
  • 选择模式设置
  • 拖放支持
  • 实战:图片浏览器
  • 实战:音乐播放列表

4.2 QTableView - 表格视图

QTableView 是用于显示二维表格数据的视图类,通常与 QAbstractTableModel 配合使用。

4.2.1 QTableView基本用法

最简单的使用

cpp 复制代码
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建模型
    QStandardItemModel *model = new QStandardItemModel(4, 3);  // 4行3列
    model->setHorizontalHeaderLabels({"姓名", "年龄", "城市"});
    
    // 填充数据
    model->setItem(0, 0, new QStandardItem("张三"));
    model->setItem(0, 1, new QStandardItem("25"));
    model->setItem(0, 2, new QStandardItem("北京"));
    
    model->setItem(1, 0, new QStandardItem("李四"));
    model->setItem(1, 1, new QStandardItem("30"));
    model->setItem(1, 2, new QStandardItem("上海"));
    
    // 创建视图
    QTableView *view = new QTableView;
    view->setModel(model);
    
    view->resize(500, 300);
    view->show();
    
    return app.exec();
}

常用属性设置

cpp 复制代码
QTableView *view = new QTableView;

// 设置是否显示网格线
view->setShowGrid(true);

// 设置网格线样式
view->setGridStyle(Qt::SolidLine);

// 设置交替行颜色
view->setAlternatingRowColors(true);

// 设置选择行为
view->setSelectionBehavior(QAbstractItemView::SelectRows);  // 选择整行
// view->setSelectionBehavior(QAbstractItemView::SelectColumns);  // 选择整列
// view->setSelectionBehavior(QAbstractItemView::SelectItems);    // 选择单元格

// 设置选择模式
view->setSelectionMode(QAbstractItemView::ExtendedSelection);  // 多选

// 设置编辑触发方式
view->setEditTriggers(QAbstractItemView::DoubleClicked | 
                      QAbstractItemView::EditKeyPressed);

// 设置排序启用
view->setSortingEnabled(true);

// 设置单元格间距
view->setWordWrap(false);

4.2.2 表头设置:QHeaderView

获取表头

cpp 复制代码
QHeaderView *horizontalHeader = view->horizontalHeader();
QHeaderView *verticalHeader = view->verticalHeader();

表头常用操作

cpp 复制代码
// 水平表头
QHeaderView *hHeader = view->horizontalHeader();

// 设置列宽调整模式
hHeader->setSectionResizeMode(QHeaderView::Stretch);  // 均匀拉伸
// hHeader->setSectionResizeMode(QHeaderView::ResizeToContents);  // 根据内容调整
// hHeader->setSectionResizeMode(QHeaderView::Interactive);  // 用户可调整

// 设置特定列的调整模式
hHeader->setSectionResizeMode(0, QHeaderView::Fixed);  // 第0列固定宽度
hHeader->resizeSection(0, 100);  // 设置第0列宽度为100像素

// 设置最后一列拉伸
hHeader->setStretchLastSection(true);

// 设置排序指示器
hHeader->setSortIndicatorShown(true);

// 隐藏表头
hHeader->setVisible(false);

// 设置表头高度
hHeader->setDefaultSectionSize(30);
hHeader->setMinimumSectionSize(20);

// 允许拖动列
hHeader->setSectionsMovable(true);

// 允许点击排序
hHeader->setSectionsClickable(true);

垂直表头

cpp 复制代码
QHeaderView *vHeader = view->verticalHeader();

// 隐藏行号
vHeader->setVisible(false);

// 设置行高
vHeader->setDefaultSectionSize(25);

// 根据内容自动调整行高
vHeader->setSectionResizeMode(QHeaderView::ResizeToContents);

自定义表头样式

cpp 复制代码
// 通过样式表
view->horizontalHeader()->setStyleSheet(
    "QHeaderView::section {"
    "    background-color: #4CAF50;"
    "    color: white;"
    "    padding: 5px;"
    "    border: 1px solid #45a049;"
    "    font-weight: bold;"
    "}"
);

4.2.3 行列调整:隐藏、调整大小、排序

隐藏行列

cpp 复制代码
// 隐藏第2行
view->setRowHidden(2, true);

// 隐藏第1列
view->setColumnHidden(1, true);

// 显示
view->setRowHidden(2, false);
view->setColumnHidden(1, false);

调整行列大小

cpp 复制代码
// 设置列宽
view->setColumnWidth(0, 150);
view->setColumnWidth(1, 100);

// 设置行高
view->setRowHeight(0, 30);

// 自动调整列宽以适应内容
view->resizeColumnsToContents();

// 自动调整行高以适应内容
view->resizeRowsToContents();

// 自动调整某一列
view->resizeColumnToContents(0);

// 自动调整某一行
view->resizeRowToContents(0);

排序

cpp 复制代码
// 启用排序
view->setSortingEnabled(true);

// 程序中排序(按第0列升序)
view->sortByColumn(0, Qt::AscendingOrder);

// 按第1列降序
view->sortByColumn(1, Qt::DescendingOrder);

// 监听排序变化
QObject::connect(view->horizontalHeader(), &QHeaderView::sortIndicatorChanged,
                 [](int logicalIndex, Qt::SortOrder order) {
    qDebug() << "Sorted by column" << logicalIndex 
             << (order == Qt::AscendingOrder ? "ASC" : "DESC");
});

4.2.4 单元格合并

合并单元格

cpp 复制代码
// 合并单元格:从(row, col)开始,跨rowSpan行,colSpan列
view->setSpan(0, 0, 2, 1);  // 合并第0行第0列,跨2行1列

// 示例:合并表头单元格
view->setSpan(0, 0, 1, 2);  // 第0行,合并第0-1列
view->setSpan(0, 2, 1, 2);  // 第0行,合并第2-3列

// 取消合并
view->setSpan(0, 0, 1, 1);

完整合并示例

cpp 复制代码
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    QStandardItemModel *model = new QStandardItemModel(5, 4);
    model->setHorizontalHeaderLabels({"A", "B", "C", "D"});
    
    QTableView *view = new QTableView;
    view->setModel(model);
    
    // 合并单元格
    view->setSpan(0, 0, 2, 2);  // 左上角 2x2
    view->setSpan(3, 1, 2, 3);  // 底部大区域
    
    // 在合并单元格中设置内容
    model->setItem(0, 0, new QStandardItem("合并区域 1"));
    model->setItem(3, 1, new QStandardItem("合并区域 2"));
    
    view->resize(400, 300);
    view->show();
    
    return app.exec();
}

4.2.5 选择行为:行选择、列选择、单元格选择

选择行为

cpp 复制代码
// 选择整行
view->setSelectionBehavior(QAbstractItemView::SelectRows);

// 选择整列
view->setSelectionBehavior(QAbstractItemView::SelectColumns);

// 选择单个单元格
view->setSelectionBehavior(QAbstractItemView::SelectItems);

获取选中的行/列/单元格

cpp 复制代码
// 获取选中的行(去重)
QSet<int> selectedRows;
QModelIndexList selected = view->selectionModel()->selectedIndexes();
for (const QModelIndex &index : selected) {
    selectedRows.insert(index.row());
}
qDebug() << "Selected rows:" << selectedRows;

// 获取选中的列(去重)
QSet<int> selectedCols;
for (const QModelIndex &index : selected) {
    selectedCols.insert(index.column());
}

// 获取选中的行对象
QModelIndexList selectedRows = view->selectionModel()->selectedRows();
for (const QModelIndex &index : selectedRows) {
    qDebug() << "Row" << index.row() << "selected";
}

// 获取选中的列对象
QModelIndexList selectedCols = view->selectionModel()->selectedColumns();

// 选中特定行
view->selectRow(0);  // 选中第0行

// 选中特定列
view->selectColumn(1);  // 选中第1列

// 清除选择
view->clearSelection();

监听选择变化

cpp 复制代码
QObject::connect(view->selectionModel(), &QItemSelectionModel::selectionChanged,
                 [=](const QItemSelection &selected, const QItemSelection &deselected) {
    qDebug() << "Selection changed";
    qDebug() << "Selected count:" << selected.indexes().size();
    qDebug() << "Deselected count:" << deselected.indexes().size();
});

4.2.6 实战:数据表格展示

这个示例实现一个通用的数据表格展示工具。

cpp 复制代码
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QLabel>
#include <QHeaderView>

class DataTableWidget : public QWidget {
    Q_OBJECT
    
private:
    QTableView *m_tableView;
    QStandardItemModel *m_model;
    QLineEdit *m_searchBox;
    
public:
    DataTableWidget(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        loadSampleData();
    }
    
private:
    void setupUI() {
        // 创建模型
        m_model = new QStandardItemModel(this);
        m_model->setHorizontalHeaderLabels({"ID", "姓名", "部门", "职位", "薪资", "入职日期"});
        
        // 创建视图
        m_tableView = new QTableView;
        m_tableView->setModel(m_model);
        m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
        m_tableView->setAlternatingRowColors(true);
        m_tableView->setSortingEnabled(true);
        m_tableView->setEditTriggers(QAbstractItemView::DoubleClicked);
        
        // 表头设置
        QHeaderView *header = m_tableView->horizontalHeader();
        header->setSectionResizeMode(QHeaderView::Interactive);
        header->setSectionResizeMode(1, QHeaderView::Stretch);  // 姓名列拉伸
        header->setStretchLastSection(true);
        
        m_tableView->setColumnWidth(0, 60);   // ID
        m_tableView->setColumnWidth(2, 100);  // 部门
        m_tableView->setColumnWidth(3, 100);  // 职位
        m_tableView->setColumnWidth(4, 80);   // 薪资
        m_tableView->setColumnWidth(5, 100);  // 日期
        
        // 垂直表头
        m_tableView->verticalHeader()->setDefaultSectionSize(30);
        
        // 搜索框
        m_searchBox = new QLineEdit;
        m_searchBox->setPlaceholderText("搜索姓名或部门...");
        connect(m_searchBox, &QLineEdit::textChanged, this, &DataTableWidget::searchData);
        
        // 按钮
        QPushButton *addBtn = new QPushButton("添加");
        QPushButton *deleteBtn = new QPushButton("删除选中");
        QPushButton *exportBtn = new QPushButton("导出");
        
        connect(addBtn, &QPushButton::clicked, this, &DataTableWidget::addRow);
        connect(deleteBtn, &QPushButton::clicked, this, &DataTableWidget::deleteSelected);
        connect(exportBtn, &QPushButton::clicked, this, &DataTableWidget::exportData);
        
        // 统计标签
        QLabel *statsLabel = new QLabel;
        connect(m_model, &QStandardItemModel::rowsInserted, [=]() {
            statsLabel->setText(QString("总计: %1 条记录").arg(m_model->rowCount()));
        });
        connect(m_model, &QStandardItemModel::rowsRemoved, [=]() {
            statsLabel->setText(QString("总计: %1 条记录").arg(m_model->rowCount()));
        });
        
        // 布局
        QVBoxLayout *mainLayout = new QVBoxLayout;
        
        QHBoxLayout *topLayout = new QHBoxLayout;
        topLayout->addWidget(new QLabel("搜索:"));
        topLayout->addWidget(m_searchBox);
        topLayout->addStretch();
        topLayout->addWidget(statsLabel);
        
        mainLayout->addLayout(topLayout);
        mainLayout->addWidget(m_tableView);
        
        QHBoxLayout *btnLayout = new QHBoxLayout;
        btnLayout->addWidget(addBtn);
        btnLayout->addWidget(deleteBtn);
        btnLayout->addWidget(exportBtn);
        btnLayout->addStretch();
        
        mainLayout->addLayout(btnLayout);
        
        setLayout(mainLayout);
        resize(800, 500);
    }
    
    void loadSampleData() {
        QList<QStringList> data = {
            {"1001", "张三", "技术部", "工程师", "8000", "2020-01-15"},
            {"1002", "李四", "销售部", "销售经理", "12000", "2019-06-10"},
            {"1003", "王五", "技术部", "高级工程师", "15000", "2018-03-20"},
            {"1004", "赵六", "人事部", "HR", "6000", "2021-09-01"},
            {"1005", "孙七", "财务部", "会计", "7000", "2020-05-15"}
        };
        
        for (const QStringList &row : data) {
            QList<QStandardItem*> items;
            for (const QString &text : row) {
                QStandardItem *item = new QStandardItem(text);
                
                // ID列和薪资列右对齐
                if (row.indexOf(text) == 0 || row.indexOf(text) == 4) {
                    item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
                }
                
                // ID列不可编辑
                if (row.indexOf(text) == 0) {
                    item->setFlags(item->flags() & ~Qt::ItemIsEditable);
                }
                
                items.append(item);
            }
            m_model->appendRow(items);
        }
    }
    
private slots:
    void addRow() {
        int nextId = m_model->rowCount() + 1001;
        QList<QStandardItem*> items;
        items << new QStandardItem(QString::number(nextId))
              << new QStandardItem("新员工")
              << new QStandardItem("未分配")
              << new QStandardItem("职位")
              << new QStandardItem("0")
              << new QStandardItem(QDate::currentDate().toString("yyyy-MM-dd"));
        
        items[0]->setFlags(items[0]->flags() & ~Qt::ItemIsEditable);
        items[0]->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
        items[4]->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
        
        m_model->appendRow(items);
        
        // 滚动到新行
        m_tableView->scrollToBottom();
    }
    
    void deleteSelected() {
        QModelIndexList selected = m_tableView->selectionModel()->selectedRows();
        
        // 从后往前删除(避免索引变化)
        QList<int> rows;
        for (const QModelIndex &index : selected) {
            rows.append(index.row());
        }
        std::sort(rows.begin(), rows.end(), std::greater<int>());
        
        for (int row : rows) {
            m_model->removeRow(row);
        }
    }
    
    void searchData(const QString &text) {
        for (int row = 0; row < m_model->rowCount(); ++row) {
            bool match = false;
            
            // 搜索姓名和部门列
            QString name = m_model->item(row, 1)->text();
            QString dept = m_model->item(row, 2)->text();
            
            if (name.contains(text, Qt::CaseInsensitive) || 
                dept.contains(text, Qt::CaseInsensitive)) {
                match = true;
            }
            
            m_tableView->setRowHidden(row, !match && !text.isEmpty());
        }
    }
    
    void exportData() {
        qDebug() << "导出数据功能 - 实际应用中可导出为 CSV/Excel";
        for (int row = 0; row < m_model->rowCount(); ++row) {
            QStringList rowData;
            for (int col = 0; col < m_model->columnCount(); ++col) {
                rowData << m_model->item(row, col)->text();
            }
            qDebug() << rowData.join(",");
        }
    }
};

4.2.7 实战:Excel风格数据编辑器

这个示例实现一个类似Excel的数据编辑器。

cpp 复制代码
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>
#include <QHeaderView>
#include <QKeyEvent>
#include <QClipboard>
#include <QMenu>

class ExcelStyleTable : public QTableView {
    Q_OBJECT
    
public:
    ExcelStyleTable(QWidget *parent = nullptr) : QTableView(parent) {
        setupView();
        createContextMenu();
    }
    
protected:
    void keyPressEvent(QKeyEvent *event) override {
        // Ctrl+C: 复制
        if (event->matches(QKeySequence::Copy)) {
            copySelection();
            return;
        }
        
        // Ctrl+V: 粘贴
        if (event->matches(QKeySequence::Paste)) {
            pasteSelection();
            return;
        }
        
        // Delete: 清除内容
        if (event->key() == Qt::Key_Delete) {
            clearSelection();
            return;
        }
        
        // Ctrl+X: 剪切
        if (event->matches(QKeySequence::Cut)) {
            copySelection();
            clearSelection();
            return;
        }
        
        QTableView::keyPressEvent(event);
    }
    
    void contextMenuEvent(QContextMenuEvent *event) override {
        m_contextMenu->exec(event->globalPos());
    }
    
private:
    QMenu *m_contextMenu;
    
    void setupView() {
        setSelectionMode(QAbstractItemView::ExtendedSelection);
        setSelectionBehavior(QAbstractItemView::SelectItems);
        setEditTriggers(QAbstractItemView::DoubleClicked | 
                       QAbstractItemView::EditKeyPressed |
                       QAbstractItemView::AnyKeyPressed);
        
        setAlternatingRowColors(true);
        setSortingEnabled(false);
        
        horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
        horizontalHeader()->setStretchLastSection(false);
        
        // Excel风格样式
        setStyleSheet(
            "QTableView {"
            "    gridline-color: #d0d0d0;"
            "    selection-background-color: #cce8ff;"
            "    selection-color: black;"
            "}"
            "QTableView::item {"
            "    padding: 3px;"
            "}"
            "QHeaderView::section {"
            "    background-color: #f0f0f0;"
            "    padding: 4px;"
            "    border: 1px solid #d0d0d0;"
            "    font-weight: bold;"
            "}"
        );
    }
    
    void createContextMenu() {
        m_contextMenu = new QMenu(this);
        
        QAction *copyAction = m_contextMenu->addAction("复制 (Ctrl+C)");
        QAction *cutAction = m_contextMenu->addAction("剪切 (Ctrl+X)");
        QAction *pasteAction = m_contextMenu->addAction("粘贴 (Ctrl+V)");
        m_contextMenu->addSeparator();
        QAction *clearAction = m_contextMenu->addAction("清除 (Delete)");
        m_contextMenu->addSeparator();
        QAction *insertRowAction = m_contextMenu->addAction("插入行");
        QAction *deleteRowAction = m_contextMenu->addAction("删除行");
        
        connect(copyAction, &QAction::triggered, this, &ExcelStyleTable::copySelection);
        connect(cutAction, &QAction::triggered, [this]() {
            copySelection();
            clearSelection();
        });
        connect(pasteAction, &QAction::triggered, this, &ExcelStyleTable::pasteSelection);
        connect(clearAction, &QAction::triggered, this, &ExcelStyleTable::clearSelection);
        connect(insertRowAction, &QAction::triggered, this, &ExcelStyleTable::insertRow);
        connect(deleteRowAction, &QAction::triggered, this, &ExcelStyleTable::deleteRow);
    }
    
private slots:
    void copySelection() {
        QModelIndexList selected = selectionModel()->selectedIndexes();
        if (selected.isEmpty())
            return;
        
        // 排序索引
        std::sort(selected.begin(), selected.end(), [](const QModelIndex &a, const QModelIndex &b) {
            if (a.row() != b.row())
                return a.row() < b.row();
            return a.column() < b.column();
        });
        
        // 构建文本
        QString text;
        int lastRow = selected.first().row();
        
        for (const QModelIndex &index : selected) {
            if (index.row() != lastRow) {
                text += "\n";
                lastRow = index.row();
            } else if (!text.isEmpty() && !text.endsWith('\n')) {
                text += "\t";
            }
            
            text += index.data().toString();
        }
        
        QApplication::clipboard()->setText(text);
        qDebug() << "已复制:" << selected.size() << "个单元格";
    }
    
    void pasteSelection() {
        QString text = QApplication::clipboard()->text();
        if (text.isEmpty())
            return;
        
        QModelIndex current = currentIndex();
        if (!current.isValid())
            return;
        
        QStringList rows = text.split('\n');
        int startRow = current.row();
        int startCol = current.column();
        
        for (int i = 0; i < rows.size(); ++i) {
            QStringList cols = rows[i].split('\t');
            for (int j = 0; j < cols.size(); ++j) {
                int row = startRow + i;
                int col = startCol + j;
                
                if (row < model()->rowCount() && col < model()->columnCount()) {
                    model()->setData(model()->index(row, col), cols[j]);
                }
            }
        }
        
        qDebug() << "已粘贴";
    }
    
    void clearSelection() {
        QModelIndexList selected = selectionModel()->selectedIndexes();
        for (const QModelIndex &index : selected) {
            model()->setData(index, "");
        }
    }
    
    void insertRow() {
        QModelIndex current = currentIndex();
        int row = current.isValid() ? current.row() : model()->rowCount();
        model()->insertRow(row);
    }
    
    void deleteRow() {
        QModelIndexList selected = selectionModel()->selectedRows();
        if (selected.isEmpty()) {
            QModelIndex current = currentIndex();
            if (current.isValid())
                model()->removeRow(current.row());
        } else {
            QList<int> rows;
            for (const QModelIndex &index : selected)
                rows.append(index.row());
            
            std::sort(rows.begin(), rows.end(), std::greater<int>());
            for (int row : rows)
                model()->removeRow(row);
        }
    }
};

// 使用示例
int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    QStandardItemModel *model = new QStandardItemModel(10, 6);
    
    // 设置表头(Excel风格:A, B, C...)
    QStringList headers;
    for (int i = 0; i < 6; ++i)
        headers << QString(QChar('A' + i));
    model->setHorizontalHeaderLabels(headers);
    
    // 填充一些示例数据
    for (int row = 0; row < 5; ++row) {
        for (int col = 0; col < 6; ++col) {
            model->setItem(row, col, new QStandardItem(
                QString("R%1C%2").arg(row + 1).arg(col + 1)));
        }
    }
    
    ExcelStyleTable *table = new ExcelStyleTable;
    table->setModel(model);
    table->resize(600, 400);
    table->show();
    
    return app.exec();
}

本节小结

QTableView 是强大的二维表格展示组件

QHeaderView 提供灵活的表头控制

行列操作 支持隐藏、调整、排序等

单元格合并 可实现复杂表格布局

选择模式 支持行、列、单元格多种选择

适用场景 数据展示、Excel风格编辑器、报表系统

  • QTableView基本用法
  • 表头设置:QHeaderView
  • 行列调整:隐藏、调整大小、排序
  • 单元格合并
  • 选择行为:行选择、列选择、单元格选择
  • 实战:数据表格展示
  • 实战:Excel风格数据编辑器

4.3 QTreeView - 树形视图

QTreeView 用于显示层级结构的树形数据,通常与树形模型配合使用。

4.3.1 QTreeView基本用法

最简单的使用

cpp 复制代码
#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建模型
    QStandardItemModel *model = new QStandardItemModel;
    model->setHorizontalHeaderLabels({"名称", "类型"});
    
    // 创建根项
    QStandardItem *rootItem = model->invisibleRootItem();
    
    // 添加第一层
    QStandardItem *item1 = new QStandardItem("文件夹 1");
    QStandardItem *item1Type = new QStandardItem("目录");
    rootItem->appendRow(QList<QStandardItem*>() << item1 << item1Type);
    
    // 添加子项
    QStandardItem *subItem1 = new QStandardItem("文件 1.1");
    QStandardItem *subItem1Type = new QStandardItem("文件");
    item1->appendRow(QList<QStandardItem*>() << subItem1 << subItem1Type);
    
    // 创建视图
    QTreeView *view = new QTreeView;
    view->setModel(model);
    
    view->resize(400, 300);
    view->show();
    
    return app.exec();
}

常用属性设置

cpp 复制代码
QTreeView *view = new QTreeView;

// 设置是否显示根节点的装饰
view->setRootIsDecorated(true);  // 显示根节点的展开/折叠图标

// 设置是否显示网格线
view->setIndentation(20);  // 设置缩进宽度

// 设置是否允许展开/折叠动画
view->setAnimated(true);

// 设置是否显示分支线
view->setStyleSheet("QTreeView::branch { background: palette(base); }");

// 设置是否允许排序
view->setSortingEnabled(true);

// 设置是否统一行高(性能优化)
view->setUniformRowHeights(true);

// 设置是否全部展开
view->expandAll();

// 设置是否全部折叠  
view->collapseAll();

4.3.2 展开/折叠控制

程序控制展开/折叠

cpp 复制代码
// 展开特定项
QModelIndex index = model->index(0, 0);
view->expand(index);

// 折叠特定项
view->collapse(index);

// 展开所有项
view->expandAll();

// 折叠所有项
view->collapseAll();

// 展开到指定层级
view->expandToDepth(2);  // 展开到第2层

// 判断是否展开
bool isExpanded = view->isExpanded(index);

// 设置是否可以展开
model->item(0)->setFlags(model->item(0)->flags() & ~Qt::ItemIsEnabled);

监听展开/折叠事件

cpp 复制代码
// 监听展开事件
QObject::connect(view, &QTreeView::expanded, [](const QModelIndex &index) {
    qDebug() << "Expanded:" << index.data().toString();
});

// 监听折叠事件
QObject::connect(view, &QTreeView::collapsed, [](const QModelIndex &index) {
    qDebug() << "Collapsed:" << index.data().toString();
});

记住展开状态

cpp 复制代码
// 保存展开状态
QSet<QString> expandedPaths;

void saveExpandState(QTreeView *view, QStandardItemModel *model) {
    expandedPaths.clear();
    for (int i = 0; i < model->rowCount(); ++i) {
        saveExpandStateRecursive(view, model->item(i), "");
    }
}

void saveExpandStateRecursive(QTreeView *view, QStandardItem *item, QString path) {
    QString itemPath = path + "/" + item->text();
    QModelIndex index = item->index();
    
    if (view->isExpanded(index)) {
        expandedPaths.insert(itemPath);
    }
    
    for (int i = 0; i < item->rowCount(); ++i) {
        saveExpandStateRecursive(view, item->child(i), itemPath);
    }
}

// 恢复展开状态
void restoreExpandState(QTreeView *view, QStandardItemModel *model) {
    for (int i = 0; i < model->rowCount(); ++i) {
        restoreExpandStateRecursive(view, model->item(i), "");
    }
}

void restoreExpandStateRecursive(QTreeView *view, QStandardItem *item, QString path) {
    QString itemPath = path + "/" + item->text();
    
    if (expandedPaths.contains(itemPath)) {
        view->expand(item->index());
    }
    
    for (int i = 0; i < item->rowCount(); ++i) {
        restoreExpandStateRecursive(view, item->child(i), itemPath);
    }
}

4.3.3 多列树形视图

创建多列树形视图

cpp 复制代码
QStandardItemModel *model = new QStandardItemModel;
model->setHorizontalHeaderLabels({"名称", "大小", "类型", "修改日期"});

QStandardItem *parent = model->invisibleRootItem();

// 添加多列数据
QList<QStandardItem*> row;
row << new QStandardItem("文件夹1");
row << new QStandardItem("");  // 文件夹无大小
row << new QStandardItem("目录");
row << new QStandardItem(QDateTime::currentDateTime().toString("yyyy-MM-dd"));
parent->appendRow(row);

// 添加子项(也是多列)
QList<QStandardItem*> subRow;
subRow << new QStandardItem("文件1.txt");
subRow << new QStandardItem("1.2 KB");
subRow << new QStandardItem("文本文件");
subRow << new QStandardItem(QDateTime::currentDateTime().toString("yyyy-MM-dd"));
row[0]->appendRow(subRow);

QTreeView *view = new QTreeView;
view->setModel(model);

// 设置列宽
view->setColumnWidth(0, 200);
view->setColumnWidth(1, 80);
view->setColumnWidth(2, 100);
view->setColumnWidth(3, 120);

4.3.4 排序和过滤

启用排序

cpp 复制代码
view->setSortingEnabled(true);

// 按特定列排序
view->sortByColumn(0, Qt::AscendingOrder);

使用排序代理模型过滤

cpp 复制代码
#include <QSortFilterProxyModel>

// 创建代理模型
QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel;
proxyModel->setSourceModel(sourceModel);

// 设置过滤
proxyModel->setFilterKeyColumn(0);  // 过滤第0列
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxyModel->setFilterRegularExpression("pattern");

// 设置排序
proxyModel->setSortRole(Qt::DisplayRole);

// 视图使用代理模型
view->setModel(proxyModel);

// 动态过滤
QLineEdit *searchBox = new QLineEdit;
QObject::connect(searchBox, &QLineEdit::textChanged, [=](const QString &text) {
    proxyModel->setFilterWildcard(text);
});

4.3.5 实战:文件浏览器

实现一个简单的文件系统浏览器。

cpp 复制代码
#include <QApplication>
#include <QTreeView>
#include <QFileSystemModel>
#include <QSplitter>
#include <QTextEdit>
#include <QVBoxLayout>

class FileBrowser : public QWidget {
    Q_OBJECT
    
private:
    QTreeView *m_treeView;
    QFileSystemModel *m_model;
    QTextEdit *m_infoText;
    
public:
    FileBrowser(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
    }
    
private:
    void setupUI() {
        // 创建文件系统模型
        m_model = new QFileSystemModel(this);
        m_model->setRootPath("");  // 设置根路径为系统根目录
        
        // 设置过滤器(只显示目录和某些文件)
        m_model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
        m_model->setNameFilters(QStringList() << "*.txt" << "*.cpp" << "*.h" << "*");
        m_model->setNameFilterDisables(false);
        
        // 创建树形视图
        m_treeView = new QTreeView;
        m_treeView->setModel(m_model);
        m_treeView->setRootIndex(m_model->index("C:/"));  // 设置显示的根目录
        
        // 隐藏不需要的列
        m_treeView->setColumnWidth(0, 250);
        m_treeView->hideColumn(1);  // 隐藏大小列
        m_treeView->hideColumn(2);  // 隐藏类型列
        
        // 设置视图属性
        m_treeView->setAnimated(true);
        m_treeView-> setSortingEnabled(true);
        m_treeView->setIndentation(20);
        
        // 信息显示区
        m_infoText = new QTextEdit;
        m_infoText->setReadOnly(true);
        m_infoText->setMaximumHeight(150);
        
        // 监听选择变化
        connect(m_treeView->selectionModel(), &QItemSelectionModel::currentChanged,
                this, &FileBrowser::onItemSelected);
        
        // 双击打开文件夹
        connect(m_treeView, &QTreeView::doubleClicked,
                this, &FileBrowser::onItemDoubleClicked);
        
        // 布局
        QSplitter *splitter = new QSplitter(Qt::Vertical);
        splitter->addWidget(m_treeView);
        splitter->addWidget(m_infoText);
        splitter->setStretchFactor(0, 3);
        splitter->setStretchFactor(1, 1);
        
        QVBoxLayout *layout = new QVBoxLayout;
        layout->addWidget(splitter);
        setLayout(layout);
        
        resize(600, 500);
    }
    
private slots:
    void onItemSelected(const QModelIndex &current, const QModelIndex &previous) {
        Q_UNUSED(previous);
        
        if (!current.isValid())
            return;
        
        QString path = m_model->filePath(current);
        QFileInfo info(path);
        
        QString infoHtml = "<b>路径:</b> " + path + "<br>";
        infoHtml += "<b>类型:</b> " + (info.isDir() ? "目录" : "文件") + "<br>";
        
        if (info.isFile()) {
            infoHtml += "<b>大小:</b> " + QString::number(info.size()) + " bytes<br>";
        }
        
        infoHtml += "<b>修改时间:</b> " + info.lastModified().toString("yyyy-MM-dd hh:mm:ss") + "<br>";
        infoHtml += "<b>可读:</b> " + (info.isReadable() ? "是" : "否") + "<br>";
        infoHtml += "<b>可写:</b> " + (info.isWritable() ? "是" : "否");
        
        m_infoText->setHtml(infoHtml);
    }
    
    void onItemDoubleClicked(const QModelIndex &index) {
        if (m_model->isDir(index)) {
            qDebug() << "打开目录:" << m_model->filePath(index);
        } else {
            qDebug() << "打开文件:" << m_model->filePath(index);
            // 实际应用中可以用 QDesktopServices::openUrl() 打开文件
        }
    }
};

4.3.6 实战:分类管理系统

实现一个商品分类管理系统。

cpp 复制代码
#include <QApplication>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QInputDialog>
#include <QMenu>

class CategoryManager : public QWidget {
    Q_OBJECT
    
private:
    QTreeView *m_treeView;
    QStandardItemModel *m_model;
    
public:
    CategoryManager(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        loadSampleData();
    }
    
private:
    void setupUI() {
        m_model = new QStandardItemModel(this);
        m_model->setHorizontalHeaderLabels({"分类名称", "商品数量", "描述"});
        
        m_treeView = new QTreeView;
        m_treeView->setModel(m_model);
        m_treeView->setEditTriggers(QAbstractItemView::DoubleClicked);
        m_treeView->setContextMenuPolicy(Qt::CustomContextMenu);
        m_treeView->setSortingEnabled(false);
        m_treeView->setColumnWidth(0, 200);
        m_treeView->setColumnWidth(1, 80);
        
        connect(m_treeView, &QTreeView::customContextMenuRequested,
                this, &CategoryManager::showContextMenu);
        
        // 按钮
        QPushButton *addRootBtn = new QPushButton("添加根分类");
        QPushButton *addChildBtn = new QPushButton("添加子分类");
        QPushButton *deleteBtn = new QPushButton("删除选中");
        QPushButton *expandBtn = new QPushButton("全部展开");
        QPushButton *collapseBtn = new QPushButton("全部折叠");
        
        connect(addRootBtn, &QPushButton::clicked, this, &CategoryManager::addRootCategory);
        connect(addChildBtn, &QPushButton::clicked, this, &CategoryManager::addChildCategory);
        connect(deleteBtn, &QPushButton::clicked, this, &CategoryManager::deleteCategory);
        connect(expandBtn, &QPushButton::clicked, m_treeView, &QTreeView::expandAll);
        connect(collapseBtn, &QPushButton::clicked, m_treeView, &QTreeView::collapseAll);
        
        // 布局
        QVBoxLayout *layout = new QVBoxLayout;
        layout->addWidget(m_treeView);
        
        QHBoxLayout *btnLayout = new QHBoxLayout;
        btnLayout->addWidget(addRootBtn);
        btnLayout->addWidget(addChildBtn);
        btnLayout->addWidget(deleteBtn);
        btnLayout->addStretch();
        btnLayout->addWidget(expandBtn);
        btnLayout->addWidget(collapseBtn);
        
        layout->addLayout(btnLayout);
        setLayout(layout);
        
        resize(600, 400);
    }
    
    void loadSampleData() {
        // 电子产品
        QList<QStandardItem*> electronics;
        electronics << new QStandardItem("电子产品");
        electronics << new QStandardItem("15");
        electronics << new QStandardItem("各类电子设备");
        m_model->appendRow(electronics);
        
        // 手机
        QList<QStandardItem*> phones;
        phones << new QStandardItem("手机");
        phones << new QStandardItem("8");
        phones << new QStandardItem("智能手机");
        electronics[0]->appendRow(phones);
        
        // 电脑
        QList<QStandardItem*> computers;
        computers << new QStandardItem("电脑");
        computers << new QStandardItem("7");
        computers << new QStandardItem("笔记本和台式机");
        electronics[0]->appendRow(computers);
        
        // 服装
        QList<QStandardItem*> clothing;
        clothing << new QStandardItem("服装");
        clothing << new QStandardItem("30");
        clothing << new QStandardItem("各类服饰");
        m_model->appendRow(clothing);
        
        // 男装
        QList<QStandardItem*> menClothing;
        menClothing << new QStandardItem("男装");
        menClothing << new QStandardItem("15");
        menClothing << new QStandardItem("男士服装");
        clothing[0]->appendRow(menClothing);
        
        // 女装
        QList<QStandardItem*> womenClothing;
        womenClothing << new QStandardItem("女装");
        womenClothing << new QStandardItem("15");
        womenClothing << new QStandardItem("女士服装");
        clothing[0]->appendRow(womenClothing);
        
        m_treeView->expandAll();
    }
    
private slots:
    void addRootCategory() {
        bool ok;
        QString name = QInputDialog::getText(this, "添加根分类", "分类名称:", 
                                             QLineEdit::Normal, "", &ok);
        if (ok && !name.isEmpty()) {
            QList<QStandardItem*> row;
            row << new QStandardItem(name);
            row << new QStandardItem("0");
            row << new QStandardItem("");
            m_model->appendRow(row);
        }
    }
    
    void addChildCategory() {
        QModelIndex current = m_treeView->currentIndex();
        if (!current.isValid()) {
            qDebug() << "请先选择父分类";
            return;
        }
        
        bool ok;
        QString name = QInputDialog::getText(this, "添加子分类", "分类名称:", 
                                             QLineEdit::Normal, "", &ok);
        if (ok && !name.isEmpty()) {
            QStandardItem *parentItem = m_model->itemFromIndex(current);
            
            QList<QStandardItem*> row;
            row << new QStandardItem(name);
            row << new QStandardItem("0");
            row << new QStandardItem("");
            parentItem->appendRow(row);
            
            m_treeView->expand(current);
        }
    }
    
    void deleteCategory() {
        QModelIndex current = m_treeView->currentIndex();
        if (!current.isValid())
            return;
        
        QStandardItem *item = m_model->itemFromIndex(current);
        
        if (item->hasChildren()) {
            qDebug() << "不能删除包含子分类的分类";
            return;
        }
        
        // 删除
        QStandardItem *parent = item->parent();
        if (parent) {
            parent->removeRow(current.row());
        } else {
            m_model->removeRow(current.row());
        }
    }
    
    void showContextMenu(const QPoint &pos) {
        QModelIndex index = m_treeView->indexAt(pos);
        
        QMenu menu;
        QAction *addChildAction = menu.addAction("添加子分类");
        menu.addSeparator();
        QAction *renameAction = menu.addAction("重命名");
        QAction *deleteAction = menu.addAction("删除");
        
        if (!index.isValid()) {
            addChildAction->setEnabled(false);
            renameAction->setEnabled(false);
            deleteAction->setEnabled(false);
        }
        
        QAction *selected = menu.exec(m_treeView->viewport()->mapToGlobal(pos));
        
        if (selected == addChildAction) {
            m_treeView->setCurrentIndex(index);
            addChildCategory();
        } else if (selected == renameAction) {
            m_treeView->edit(index);
        } else if (selected == deleteAction) {
            m_treeView->setCurrentIndex(index);
            deleteCategory();
        }
    }
};

本节小结

QTreeView 用于显示层级结构数据

展开/折叠 提供灵活的层级展示控制

多列支持 可显示复杂的树形表格

排序过滤 通过代理模型实现

适用场景 文件浏览器、分类管理、组织架构等

  • QTreeView基本用法
  • 展开/折叠控制
  • 多列树形视图
  • 排序和过滤
  • 实战:文件浏览器
  • 实战:分类管理系统

4.4 视图通用属性与方法

这些是所有视图类(QListView、QTableView、QTreeView)共享的常用属性和方法。

4.4.1 选择模型:QItemSelectionModel

选择模型的作用

QItemSelectionModel 管理视图中的选择状态,与模型和视图分离。

cpp 复制代码
// 获取选择模型
QItemSelectionModel *selectionModel = view->selectionModel();

// 选择项
selectionModel->select(index, QItemSelectionModel::Select);

// 取消选择
selectionModel->select(index, QItemSelectionModel::Deselect);

// 切换选择状态
selectionModel->select(index, QItemSelectionModel::Toggle);

// 清除所有选择
selectionModel->clear();

// 选择并使其成为当前项
selectionModel->setCurrentIndex(index, QItemSelectionModel::SelectCurrent);

选择标志

cpp 复制代码
// 选择标志可以组合使用
QItemSelectionModel::Select        // 选择
QItemSelectionModel::Deselect      // 取消选择
QItemSelectionModel::Toggle        // 切换
QItemSelectionModel::Current       // 设置为当前项
QItemSelectionModel::Rows          // 选择整行
QItemSelectionModel::Columns       // 选择整列
QItemSelectionModel::Clear         // 先清除现有选择

// 示例:选择整行并清除其他选择
selectionModel->select(index, QItemSelectionModel::Clear | 
                             QItemSelectionModel::Select | 
                             QItemSelectionModel::Rows);

4.4.2 当前选中项的获取
cpp 复制代码
// 获取当前项
QModelIndex current = view->currentIndex();

// 获取所有选中的索引
QModelIndexList selected = view->selectionModel()->selectedIndexes();

// 获取选中的行
QModelIndexList selectedRows = view->selectionModel()->selectedRows();

// 获取选中的列
QModelIndexList selectedCols = view->selectionModel()->selectedColumns();

// 判断是否有选择
bool hasSelection = view->selectionModel()->hasSelection();

// 获取选择的范围
QItemSelection selection = view->selectionModel()->selection();

4.4.3 编辑触发方式
cpp 复制代码
// 设置编辑触发方式
view->setEditTriggers(QAbstractItemView::NoEditTriggers);  // 不可编辑

// 常用组合
view->setEditTriggers(QAbstractItemView::DoubleClicked |   // 双击
                      QAbstractItemView::EditKeyPressed);  // 按F2

// 所有触发方式
QAbstractItemView::NoEditTriggers          // 不可编辑
QAbstractItemView::CurrentChanged          // 当前项改变时
QAbstractItemView::DoubleClicked           // 双击
QAbstractItemView::SelectedClicked         // 点击已选中项
QAbstractItemView::EditKeyPressed          // 按下编辑键(F2)
QAbstractItemView::AnyKeyPressed           // 按下任意键
QAbstractItemView::AllEditTriggers         // 所有方式

4.4.4 右键菜单实现

基本右键菜单

cpp 复制代码
class MyView : public QTableView {
    Q_OBJECT
    
public:
    MyView(QWidget *parent = nullptr) : QTableView(parent) {
        setContextMenuPolicy(Qt::CustomContextMenu);
        connect(this, &QTableView::customContextMenuRequested,
                this, &MyView::showContextMenu);
    }
    
private slots:
    void showContextMenu(const QPoint &pos) {
        QModelIndex index = indexAt(pos);
        
        QMenu menu(this);
        
        QAction *addAction = menu.addAction("添加");
        QAction *editAction = menu.addAction("编辑");
        QAction *deleteAction = menu.addAction("删除");
        menu.addSeparator();
        QAction *refreshAction = menu.addAction("刷新");
        
        // 根据是否选中项启用/禁用菜单
        if (!index.isValid()) {
            editAction->setEnabled(false);
            deleteAction->setEnabled(false);
        }
        
        QAction *selected = menu.exec(viewport()->mapToGlobal(pos));
        
        if (selected == addAction) {
            // 添加逻辑
        } else if (selected == editAction) {
            edit(index);
        } else if (selected == deleteAction) {
            model()->removeRow(index.row());
        } else if (selected == refreshAction) {
            // 刷新逻辑
        }
    }
};

4.4.5 自定义视图行为

常用自定义

cpp 复制代码
class CustomView : public QTableView {
protected:
    // 自定义键盘事件
    void keyPressEvent(QKeyEvent *event) override {
        if (event->key() == Qt::Key_Delete) {
            deleteSelectedRows();
            return;
        }
        QTableView::keyPressEvent(event);
    }
    
    // 自定义鼠标事件
    void mousePressEvent(QMouseEvent *event) override {
        if (event->button() == Qt::RightButton) {
            // 自定义右键处理
        }
        QTableView::mousePressEvent(event);
    }
    
    // 自定义双击事件
    void mouseDoubleClickEvent(QMouseEvent *event) override {
        QModelIndex index = indexAt(event->pos());
        if (index.isValid()) {
            // 自定义双击逻辑
        }
        QTableView::mouseDoubleClickEvent(event);
    }
    
private:
    void deleteSelectedRows() {
        QModelIndexList selected = selectionModel()->selectedRows();
        // 删除逻辑
    }
};

完整示例:增强型表格视图

cpp 复制代码
class EnhancedTableView : public QTableView {
    Q_OBJECT
    
public:
    EnhancedTableView(QWidget *parent = nullptr) : QTableView(parent) {
        setupView();
    }
    
protected:
    void keyPressEvent(QKeyEvent *event) override {
        // Ctrl+A: 全选
        if (event->matches(QKeySequence::SelectAll)) {
            selectAll();
            return;
        }
        
        // Ctrl+C: 复制
        if (event->matches(QKeySequence::Copy)) {
            copyToClipboard();
            return;
        }
        
        // Delete: 删除
        if (event->key() == Qt::Key_Delete) {
            emit deleteRequested();
            return;
        }
        
        // Enter: 编辑
        if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) {
            QModelIndex current = currentIndex();
            if (current.isValid()) {
                edit(current);
                return;
            }
        }
        
        QTableView::keyPressEvent(event);
    }
    
signals:
    void deleteRequested();
    
private:
    void setupView() {
        setSelectionBehavior(QAbstractItemView::SelectRows);
        setSelectionMode(QAbstractItemView::ExtendedSelection);
        setAlternatingRowColors(true);
        setSortingEnabled(true);
        
        horizontalHeader()->setStretchLastSection(true);
        verticalHeader()->setDefaultSectionSize(25);
    }
    
    void copyToClipboard() {
        QModelIndexList selected = selectionModel()->selectedIndexes();
        if (selected.isEmpty())
            return;
        
        // 排序
        std::sort(selected.begin(), selected.end());
        
        QString text;
        int lastRow = -1;
        
        for (const QModelIndex &index : selected) {
            if (index.row() != lastRow) {
                if (!text.isEmpty())
                    text += "\n";
                lastRow = index.row();
            } else {
                text += "\t";
            }
            text += index.data().toString();
        }
        
        QApplication::clipboard()->setText(text);
    }
};

本节小结

QItemSelectionModel 统一管理所有视图的选择

编辑触发 可灵活配置

右键菜单 提供便捷的上下文操作

自定义行为 通过重写事件处理实现

通用方法 适用于所有视图类型

  • 选择模型:QItemSelectionModel
  • 当前选中项的获取
  • 编辑触发方式
  • 右键菜单实现
  • 自定义视图行为

第4章总结

🎉 第4章 标准视图类的使用 已全部完成!

本章涵盖了:

  • ✅ QListView - 列表视图(ListMode和IconMode)
  • ✅ QTableView - 表格视图(表头、排序、单元格合并)
  • ✅ QTreeView - 树形视图(展开/折叠、多列)
  • ✅ 视图通用属性(选择模型、编辑、右键菜单)

通过本章学习,你已经掌握了:

  1. 三种主要视图类型的使用
  2. 如何配置视图的显示和交互行为
  3. 如何处理选择、编辑、排序等操作
  4. 如何实现复杂的实战应用(文件浏览器、数据表格、分类管理等)

接下来可以继续学习第5章"便捷模型类"!


第5章 便捷模型类

5.1 QStringListModel

QStringListModel 是 Qt 提供的最简单的便捷模型类,专门用于管理字符串列表数据。

5.1.1 基本用法

创建和使用

cpp 复制代码
#include <QApplication>
#include <QListView>
#include <QStringListModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建字符串列表
    QStringList data;
    data << "Apple" << "Banana" << "Cherry" << "Date" << "Elderberry";
    
    // 创建模型
    QStringListModel *model = new QStringListModel(data);
    
    // 创建视图
    QListView *view = new QListView;
    view->setModel(model);
    view->setEditTriggers(QAbstractItemView::DoubleClicked);
    
    view->resize(300, 400);
    view->show();
    
    return app.exec();
}

常用方法

cpp 复制代码
QStringListModel *model = new QStringListModel;

// 设置字符串列表
QStringList list = {"Item 1", "Item 2", "Item 3"};
model->setStringList(list);

// 获取字符串列表
QStringList currentList = model->stringList();

// 获取行数
int count = model->rowCount();

// 通过索引获取数据
QModelIndex index = model->index(0, 0);
QString text = model->data(index, Qt::DisplayRole).toString();

// 设置数据
model->setData(index, "New Value");

5.1.2 数据的增删改查

添加数据

cpp 复制代码
// 方法1:通过insertRows添加
model->insertRows(model->rowCount(), 1);  // 在末尾插入1行
QModelIndex newIndex = model->index(model->rowCount() - 1, 0);
model->setData(newIndex, "New Item");

// 方法2:直接修改字符串列表
QStringList list = model->stringList();
list << "New Item";
model->setStringList(list);

// 方法3:在特定位置插入
int position = 2;  // 在索引2处插入
model->insertRows(position, 1);
model->setData(model->index(position, 0), "Inserted Item");

删除数据

cpp 复制代码
// 删除特定行
int row = 1;
model->removeRows(row, 1);  // 删除第1行(1行)

// 删除多行
model->removeRows(0, 3);  // 从第0行开始删除3行

// 清空所有数据
model->removeRows(0, model->rowCount());
// 或者
model->setStringList(QStringList());

修改数据

cpp 复制代码
// 通过索引修改
QModelIndex index = model->index(row, 0);
model->setData(index, "Modified Text");

// 直接修改字符串列表
QStringList list = model->stringList();
list[row] = "Modified Text";
model->setStringList(list);

查询数据

cpp 复制代码
// 获取特定行的数据
QString text = model->data(model->index(row, 0)).toString();

// 获取所有数据
QStringList allData = model->stringList();

// 查找包含特定文本的项
QStringList list = model->stringList();
int foundIndex = -1;
for (int i = 0; i < list.size(); ++i) {
    if (list[i].contains("search text", Qt::CaseInsensitive)) {
        foundIndex = i;
        break;
    }
}

排序

cpp 复制代码
// 排序字符串列表
QStringList list = model->stringList();
list.sort();  // 升序排序
model->setStringList(list);

// 降序排序
list.sort();
std::reverse(list.begin(), list.end());
model->setStringList(list);

// 使用自定义比较
std::sort(list.begin(), list.end(), [](const QString &a, const QString &b) {
    return a.length() < b.length();  // 按长度排序
});
model->setStringList(list);

完整的增删改查示例

cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QListView>
#include <QStringListModel>
#include <QPushButton>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QMessageBox>

class StringListDemo : public QWidget {
    Q_OBJECT
    
private:
    QStringListModel *m_model;
    QListView *m_view;
    QLineEdit *m_searchBox;
    
public:
    StringListDemo(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
    }
    
private:
    void setupUI() {
        // 创建模型和视图
        QStringList initialData = {"Apple", "Banana", "Cherry", "Date", "Elderberry"};
        m_model = new QStringListModel(initialData, this);
        
        m_view = new QListView;
        m_view->setModel(m_model);
        m_view->setEditTriggers(QAbstractItemView::DoubleClicked);
        
        // 搜索框
        m_searchBox = new QLineEdit;
        m_searchBox->setPlaceholderText("搜索...");
        connect(m_searchBox, &QLineEdit::textChanged, this, &StringListDemo::highlightSearch);
        
        // 按钮
        QPushButton *addBtn = new QPushButton("添加");
        QPushButton *insertBtn = new QPushButton("插入");
        QPushButton *deleteBtn = new QPushButton("删除");
        QPushButton *editBtn = new QPushButton("编辑");
        QPushButton *sortBtn = new QPushButton("排序");
        QPushButton *clearBtn = new QPushButton("清空");
        
        connect(addBtn, &QPushButton::clicked, this, &StringListDemo::addItem);
        connect(insertBtn, &QPushButton::clicked, this, &StringListDemo::insertItem);
        connect(deleteBtn, &QPushButton::clicked, this, &StringListDemo::deleteItem);
        connect(editBtn, &QPushButton::clicked, this, &StringListDemo::editItem);
        connect(sortBtn, &QPushButton::clicked, this, &StringListDemo::sortItems);
        connect(clearBtn, &QPushButton::clicked, this, &StringListDemo::clearItems);
        
        // 布局
        QVBoxLayout *mainLayout = new QVBoxLayout;
        
        QHBoxLayout *searchLayout = new QHBoxLayout;
        searchLayout->addWidget(m_searchBox);
        mainLayout->addLayout(searchLayout);
        
        mainLayout->addWidget(m_view);
        
        QHBoxLayout *btnLayout1 = new QHBoxLayout;
        btnLayout1->addWidget(addBtn);
        btnLayout1->addWidget(insertBtn);
        btnLayout1->addWidget(deleteBtn);
        
        QHBoxLayout *btnLayout2 = new QHBoxLayout;
        btnLayout2->addWidget(editBtn);
        btnLayout2->addWidget(sortBtn);
        btnLayout2->addWidget(clearBtn);
        
        mainLayout->addLayout(btnLayout1);
        mainLayout->addLayout(btnLayout2);
        
        setLayout(mainLayout);
        resize(400, 500);
    }
    
private slots:
    void addItem() {
        bool ok;
        QString text = QInputDialog::getText(this, "添加项", "输入文本:",
                                             QLineEdit::Normal, "", &ok);
        if (ok && !text.isEmpty()) {
            int row = m_model->rowCount();
            m_model->insertRows(row, 1);
            m_model->setData(m_model->index(row, 0), text);
        }
    }
    
    void insertItem() {
        QModelIndex current = m_view->currentIndex();
        int row = current.isValid() ? current.row() : 0;
        
        bool ok;
        QString text = QInputDialog::getText(this, "插入项", "输入文本:",
                                             QLineEdit::Normal, "", &ok);
        if (ok && !text.isEmpty()) {
            m_model->insertRows(row, 1);
            m_model->setData(m_model->index(row, 0), text);
        }
    }
    
    void deleteItem() {
        QModelIndex current = m_view->currentIndex();
        if (current.isValid()) {
            m_model->removeRows(current.row(), 1);
        }
    }
    
    void editItem() {
        QModelIndex current = m_view->currentIndex();
        if (current.isValid()) {
            QString oldText = m_model->data(current).toString();
            bool ok;
            QString text = QInputDialog::getText(this, "编辑项", "修改文本:",
                                                 QLineEdit::Normal, oldText, &ok);
            if (ok && !text.isEmpty()) {
                m_model->setData(current, text);
            }
        }
    }
    
    void sortItems() {
        QStringList list = m_model->stringList();
        list.sort();
        m_model->setStringList(list);
    }
    
    void clearItems() {
        QMessageBox::StandardButton reply = QMessageBox::question(
            this, "确认", "确定要清空所有项吗?",
            QMessageBox::Yes | QMessageBox::No);
        
        if (reply == QMessageBox::Yes) {
            m_model->setStringList(QStringList());
        }
    }
    
    void highlightSearch(const QString &text) {
        if (text.isEmpty()) {
            return;
        }
        
        // 查找匹配项
        QStringList list = m_model->stringList();
        for (int i = 0; i < list.size(); ++i) {
            if (list[i].contains(text, Qt::CaseInsensitive)) {
                m_view->setCurrentIndex(m_model->index(i, 0));
                break;
            }
        }
    }
};

5.1.3 实战示例:待办事项列表

这个示例实现一个功能完整的待办事项管理应用。

cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QListView>
#include <QStringListModel>
#include <QPushButton>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QFile>
#include <QTextStream>
#include <QFileDialog>
#include <QCheckBox>
#include <QStyledItemDelegate>
#include <QPainter>

// 自定义代理:显示复选框
class TodoItemDelegate : public QStyledItemDelegate {
public:
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        QStyleOptionViewItem opt = option;
        
        // 检查是否已完成(以[x]开头)
        QString text = index.data().toString();
        bool isCompleted = text.startsWith("[x] ");
        
        if (isCompleted) {
            // 已完成项:删除线和灰色
            opt.font.setStrikeOut(true);
            opt.palette.setColor(QPalette::Text, QColor(100, 100, 100));
        }
        
        QStyledItemDelegate::paint(painter, opt, index);
    }
};

class TodoListApp : public QWidget {
    Q_OBJECT
    
private:
    QStringListModel *m_model;
    QListView *m_view;
    QLineEdit *m_inputBox;
    QCheckBox *m_showCompletedCheck;
    QStringList m_allTodos;  // 存储所有任务(包括已完成)
    
public:
    TodoListApp(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        setWindowTitle("待办事项列表");
    }
    
private:
    void setupUI() {
        // 创建模型和视图
        m_model = new QStringListModel(this);
        
        m_view = new QListView;
        m_view->setModel(m_model);
        m_view->setItemDelegate(new TodoItemDelegate);
        m_view->setAlternatingRowColors(true);
        
        // 输入框
        m_inputBox = new QLineEdit;
        m_inputBox->setPlaceholderText("输入新的待办事项...");
        connect(m_inputBox, &QLineEdit::returnPressed, this, &TodoListApp::addTodo);
        
        // 显示已完成项复选框
        m_showCompletedCheck = new QCheckBox("显示已完成项");
        m_showCompletedCheck->setChecked(true);
        connect(m_showCompletedCheck, &QCheckBox::toggled, this, &TodoListApp::updateDisplay);
        
        // 按钮
        QPushButton *addBtn = new QPushButton("添加");
        QPushButton *completeBtn = new QPushButton("标记完成");
        QPushButton *deleteBtn = new QPushButton("删除");
        QPushButton *clearCompletedBtn = new QPushButton("清除已完成");
        QPushButton *saveBtn = new QPushButton("保存");
        QPushButton *loadBtn = new QPushButton("加载");
        
        connect(addBtn, &QPushButton::clicked, this, &TodoListApp::addTodo);
        connect(completeBtn, &QPushButton::clicked, this, &TodoListApp::toggleComplete);
        connect(deleteBtn, &QPushButton::clicked, this, &TodoListApp::deleteTodo);
        connect(clearCompletedBtn, &QPushButton::clicked, this, &TodoListApp::clearCompleted);
        connect(saveBtn, &QPushButton::clicked, this, &TodoListApp::saveTodos);
        connect(loadBtn, &QPushButton::clicked, this, &TodoListApp::loadTodos);
        
        // 统计标签
        m_statsLabel = new QLabel;
        updateStats();
        
        // 布局
        QVBoxLayout *mainLayout = new QVBoxLayout;
        
        QHBoxLayout *inputLayout = new QHBoxLayout;
        inputLayout->addWidget(m_inputBox);
        inputLayout->addWidget(addBtn);
        mainLayout->addLayout(inputLayout);
        
        mainLayout->addWidget(m_view);
        
        QHBoxLayout *optionsLayout = new QHBoxLayout;
        optionsLayout->addWidget(m_showCompletedCheck);
        optionsLayout->addWidget(m_statsLabel);
        optionsLayout->addStretch();
        mainLayout->addLayout(optionsLayout);
        
        QHBoxLayout *btnLayout1 = new QHBoxLayout;
        btnLayout1->addWidget(completeBtn);
        btnLayout1->addWidget(deleteBtn);
        btnLayout1->addWidget(clearCompletedBtn);
        
        QHBoxLayout *btnLayout2 = new QHBoxLayout;
        btnLayout2->addWidget(saveBtn);
        btnLayout2->addWidget(loadBtn);
        btnLayout2->addStretch();
        
        mainLayout->addLayout(btnLayout1);
        mainLayout->addLayout(btnLayout2);
        
        setLayout(mainLayout);
        resize(500, 600);
    }
    
    QLabel *m_statsLabel;
    
    void updateStats() {
        int total = m_allTodos.size();
        int completed = 0;
        
        for (const QString &todo : m_allTodos) {
            if (todo.startsWith("[x] ")) {
                completed++;
            }
        }
        
        int pending = total - completed;
        m_statsLabel->setText(QString("总计: %1 | 待办: %2 | 已完成: %3")
                              .arg(total).arg(pending).arg(completed));
    }
    
    void updateDisplay() {
        bool showCompleted = m_showCompletedCheck->isChecked();
        
        if (showCompleted) {
            // 显示所有任务
            m_model->setStringList(m_allTodos);
        } else {
            // 只显示未完成任务
            QStringList pending;
            for (const QString &todo : m_allTodos) {
                if (!todo.startsWith("[x] ")) {
                    pending << todo;
                }
            }
            m_model->setStringList(pending);
        }
    }
    
private slots:
    void addTodo() {
        QString text = m_inputBox->text().trimmed();
        if (text.isEmpty())
            return;
        
        // 添加到列表
        m_allTodos << "[ ] " + text;
        updateDisplay();
        updateStats();
        
        m_inputBox->clear();
        m_inputBox->setFocus();
    }
    
    void toggleComplete() {
        QModelIndex current = m_view->currentIndex();
        if (!current.isValid())
            return;
        
        QString text = m_model->data(current).toString();
        
        // 切换完成状态
        if (text.startsWith("[x] ")) {
            // 已完成 -> 未完成
            text = "[ ] " + text.mid(4);
        } else if (text.startsWith("[ ] ")) {
            // 未完成 -> 已完成
            text = "[x] " + text.mid(4);
        }
        
        // 在全部列表中更新
        int actualIndex = findInAllTodos(m_model->data(current).toString());
        if (actualIndex >= 0) {
            m_allTodos[actualIndex] = text;
        }
        
        updateDisplay();
        updateStats();
    }
    
    void deleteTodo() {
        QModelIndex current = m_view->currentIndex();
        if (!current.isValid())
            return;
        
        QString text = m_model->data(current).toString();
        
        // 从全部列表中删除
        int actualIndex = findInAllTodos(text);
        if (actualIndex >= 0) {
            m_allTodos.removeAt(actualIndex);
        }
        
        updateDisplay();
        updateStats();
    }
    
    void clearCompleted() {
        QMessageBox::StandardButton reply = QMessageBox::question(
            this, "确认", "确定要清除所有已完成项吗?",
            QMessageBox::Yes | QMessageBox::No);
        
        if (reply == QMessageBox::Yes) {
            // 保留未完成的任务
            QStringList pending;
            for (const QString &todo : m_allTodos) {
                if (!todo.startsWith("[x] ")) {
                    pending << todo;
                }
            }
            m_allTodos = pending;
            
            updateDisplay();
            updateStats();
        }
    }
    
    void saveTodos() {
        QString fileName = QFileDialog::getSaveFileName(
            this, "保存待办事项", "", "文本文件 (*.txt)");
        
        if (fileName.isEmpty())
            return;
        
        QFile file(fileName);
        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            QMessageBox::warning(this, "错误", "无法保存文件");
            return;
        }
        
        QTextStream out(&file);
        for (const QString &todo : m_allTodos) {
            out << todo << "\n";
        }
        
        file.close();
        QMessageBox::information(this, "成功", "待办事项已保存");
    }
    
    void loadTodos() {
        QString fileName = QFileDialog::getOpenFileName(
            this, "加载待办事项", "", "文本文件 (*.txt)");
        
        if (fileName.isEmpty())
            return;
        
        QFile file(fileName);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QMessageBox::warning(this, "错误", "无法打开文件");
            return;
        }
        
        m_allTodos.clear();
        QTextStream in(&file);
        while (!in.atEnd()) {
            QString line = in.readLine().trimmed();
            if (!line.isEmpty()) {
                m_allTodos << line;
            }
        }
        
        file.close();
        updateDisplay();
        updateStats();
        
        QMessageBox::information(this, "成功", "待办事项已加载");
    }
    
    int findInAllTodos(const QString &text) {
        return m_allTodos.indexOf(text);
    }
};

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

本节小结

QStringListModel 是管理字符串列表的最简单模型

增删改查 通过 insertRows、removeRows、setData 等方法实现

适用场景 简单列表、待办事项、标签管理等

优点 使用简单、API清晰、适合快速开发

  • 基本用法
  • 数据的增删改查
  • 实战示例:待办事项列表

5.2 QStandardItemModel

QStandardItemModel 是 Qt 提供的功能最强大、最灵活的便捷模型类,支持列表、表格和树形结构。

5.2.1 QStandardItem和QStandardItemModel

QStandardItem 基本概念

QStandardItem 是模型中的一个项,可以存储数据、图标、字体等多种信息。

cpp 复制代码
#include <QStandardItem>

// 创建简单项
QStandardItem *item = new QStandardItem("Item Text");

// 创建带图标的项
QStandardItem *iconItem = new QStandardItem(QIcon(":/icon.png"), "Icon Item");

// 设置各种属性
item->setText("New Text");
item->setIcon(QIcon(":/icon.png"));
item->setToolTip("This is a tooltip");
item->setEditable(false);  // 设置为不可编辑
item->setCheckable(true);  // 可以显示复选框
item->setCheckState(Qt::Checked);  // 设置选中状态

// 设置数据(支持多个角色)
item->setData("Custom Data", Qt::UserRole);
item->setData(QColor(Qt::red), Qt::BackgroundRole);
item->setData(QFont("Arial", 12, QFont::Bold), Qt::FontRole);

// 获取数据
QString text = item->text();
QVariant customData = item->data(Qt::UserRole);

QStandardItemModel 基本使用

cpp 复制代码
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建模型
    QStandardItemModel *model = new QStandardItemModel(4, 3);  // 4行3列
    model->setHorizontalHeaderLabels({"列1", "列2", "列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);
        }
    }
    
    // 创建视图
    QTableView *view = new QTableView;
    view->setModel(model);
    
    view->resize(500, 300);
    view->show();
    
    return app.exec();
}

5.2.2 复杂数据的添加

添加单个项

cpp 复制代码
QStandardItemModel *model = new QStandardItemModel;

// 方法1:通过setItem添加
QStandardItem *item = new QStandardItem("Item");
model->setItem(0, 0, item);

// 方法2:通过appendRow添加整行
QStandardItem *item1 = new QStandardItem("Column 1");
QStandardItem *item2 = new QStandardItem("Column 2");
QStandardItem *item3 = new QStandardItem("Column 3");
model->appendRow(QList<QStandardItem*>() << item1 << item2 << item3);

// 方法3:通过insertRow插入行
QList<QStandardItem*> rowItems;
rowItems << new QStandardItem("A") << new QStandardItem("B");
model->insertRow(0, rowItems);

批量添加数据

cpp 复制代码
QStandardItemModel *model = new QStandardItemModel;
model->setColumnCount(3);
model->setHorizontalHeaderLabels({"姓名", "年龄", "城市"});

// 准备数据
QList<QList<QString>> data = {
    {"张三", "25", "北京"},
    {"李四", "30", "上海"},
    {"王五", "28", "广州"}
};

// 批量添加
for (const QList<QString> &row : data) {
    QList<QStandardItem*> items;
    for (const QString &text : row) {
        items << new QStandardItem(text);
    }
    model->appendRow(items);
}

添加复杂格式的项

cpp 复制代码
// 创建一个富格式的项
QStandardItem *item = new QStandardItem;

// 设置文本和图标
item->setText("重要项");
item->setIcon(QIcon(":/star.png"));

// 设置颜色
item->setForeground(QBrush(Qt::red));  // 前景色(文字颜色)
item->setBackground(QBrush(QColor(255, 255, 200)));  // 背景色

// 设置字体
QFont font("Arial", 12, QFont::Bold);
item->setFont(font);

// 设置对齐方式
item->setTextAlignment(Qt::AlignCenter);

// 设置提示文本
item->setToolTip("这是一个重要项");
item->setStatusTip("状态栏提示");

// 设置复选框
item->setCheckable(true);
item->setCheckState(Qt::Checked);

// 设置自定义数据
item->setData(123, Qt::UserRole);
item->setData("metadata", Qt::UserRole + 1);

model->setItem(0, 0, item);

5.2.3 树形结构的构建

创建树形结构

cpp 复制代码
QStandardItemModel *model = new QStandardItemModel;
model->setHorizontalHeaderLabels({"名称", "类型"});

// 创建根项
QStandardItem *rootItem = model->invisibleRootItem();

// 添加第一层
QStandardItem *folder1 = new QStandardItem(QIcon(":/folder.png"), "文件夹1");
QStandardItem *folder1Type = new QStandardItem("目录");
rootItem->appendRow(QList<QStandardItem*>() << folder1 << folder1Type);

// 添加子项到folder1
QStandardItem *file1 = new QStandardItem(QIcon(":/file.png"), "文件1.txt");
QStandardItem *file1Type = new QStandardItem("文本文件");
folder1->appendRow(QList<QStandardItem*>() << file1 << file1Type);

QStandardItem *file2 = new QStandardItem(QIcon(":/file.png"), "文件2.txt");
QStandardItem *file2Type = new QStandardItem("文本文件");
folder1->appendRow(QList<QStandardItem*>() << file2 << file2Type);

// 添加子文件夹
QStandardItem *subfolder = new QStandardItem(QIcon(":/folder.png"), "子文件夹");
QStandardItem *subfolderType = new QStandardItem("目录");
folder1->appendRow(QList<QStandardItem*>() << subfolder << subfolderType);

// 添加第二层
QStandardItem *folder2 = new QStandardItem(QIcon(":/folder.png"), "文件夹2");
QStandardItem *folder2Type = new QStandardItem("目录");
rootItem->appendRow(QList<QStandardItem*>() << folder2 << folder2Type);

递归构建树形结构

cpp 复制代码
// 数据结构
struct TreeNode {
    QString name;
    QString type;
    QList<TreeNode> children;
};

// 递归构建函数
void buildTree(QStandardItem *parent, const TreeNode &node) {
    // 创建当前节点的项
    QStandardItem *nameItem = new QStandardItem(node.name);
    QStandardItem *typeItem = new QStandardItem(node.type);
    
    parent->appendRow(QList<QStandardItem*>() << nameItem << typeItem);
    
    // 递归添加子节点
    for (const TreeNode &child : node.children) {
        buildTree(nameItem, child);
    }
}

// 使用示例
TreeNode root = {
    "根", "root", {
        {"分支1", "branch", {
            {"叶子1", "leaf", {}},
            {"叶子2", "leaf", {}}
        }},
        {"分支2", "branch", {
            {"叶子3", "leaf", {}}
        }}
    }
};

QStandardItemModel *model = new QStandardItemModel;
QStandardItem *rootItem = model->invisibleRootItem();

for (const TreeNode &child : root.children) {
    buildTree(rootItem, child);
}

5.2.4 查找功能:findItems()

基本查找

cpp 复制代码
QStandardItemModel *model = new QStandardItemModel;

// 查找包含特定文本的项(第一列)
QList<QStandardItem*> items = model->findItems("搜索文本");

// 遍历结果
for (QStandardItem *item : items) {
    qDebug() << "Found:" << item->text() << "at row" << item->row();
}

高级查找选项

cpp 复制代码
// 精确匹配
QList<QStandardItem*> items = model->findItems(
    "精确文本", 
    Qt::MatchExactly  // 精确匹配
);

// 包含匹配(默认)
items = model->findItems(
    "部分文本",
    Qt::MatchContains  // 包含即可
);

// 通配符匹配
items = model->findItems(
    "file*.txt",
    Qt::MatchWildcard  // 支持 * 和 ? 通配符
);

// 正则表达式匹配
items = model->findItems(
    "^[A-Z].*",
    Qt::MatchRegularExpression  // 正则表达式
);

// 区分大小写
items = model->findItems(
    "Text",
    Qt::MatchExactly | Qt::MatchCaseSensitive
);

// 递归查找(包括所有层级)
items = model->findItems(
    "搜索文本",
    Qt::MatchContains | Qt::MatchRecursive
);

// 在特定列查找
items = model->findItems(
    "搜索文本",
    Qt::MatchContains,
    1  // 在第1列搜索
);

自定义查找

cpp 复制代码
// 查找满足特定条件的项
QList<QStandardItem*> findItemsCustom(QStandardItemModel *model,
                                      std::function<bool(QStandardItem*)> predicate) {
    QList<QStandardItem*> results;
    
    for (int row = 0; row < model->rowCount(); ++row) {
        for (int col = 0; col < model->columnCount(); ++col) {
            QStandardItem *item = model->item(row, col);
            if (item && predicate(item)) {
                results.append(item);
            }
        }
    }
    
    return results;
}

// 使用示例:查找所有可编辑的项
auto editableItems = findItemsCustom(model, [](QStandardItem *item) {
    return item->isEditable();
});

// 查找所有带复选框的项
auto checkableItems = findItemsCustom(model, [](QStandardItem *item) {
    return item->isCheckable();
});

// 查找所有包含自定义数据的项
auto customDataItems = findItemsCustom(model, [](QStandardItem *item) {
    return item->data(Qt::UserRole).isValid();
});

5.2.5 实战:通用数据表格

实现一个通用的数据表格管理工具。

cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QTableView>
#include <QStandardItemModel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QHeaderView>
#include <QMenu>
#include <QFileDialog>
#include <QFile>
#include <QTextStream>

class DataTableManager : public QWidget {
    Q_OBJECT
    
private:
    QStandardItemModel *m_model;
    QTableView *m_view;
    
public:
    DataTableManager(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        setWindowTitle("通用数据表格管理器");
        resize(800, 600);
    }
    
private:
    void setupUI() {
        // 创建模型
        m_model = new QStandardItemModel(this);
        m_model->setHorizontalHeaderLabels({"编号", "名称", "数值", "状态", "备注"});
        
        // 添加示例数据
        loadSampleData();
        
        // 创建视图
        m_view = new QTableView;
        m_view->setModel(m_model);
        m_view->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
        m_view->setAlternatingRowColors(true);
        m_view->setSortingEnabled(true);
        m_view->setContextMenuPolicy(Qt::CustomContextMenu);
        
        // 设置列宽
        m_view->setColumnWidth(0, 80);
        m_view->setColumnWidth(1, 150);
        m_view->setColumnWidth(2, 100);
        m_view->setColumnWidth(3, 100);
        m_view->horizontalHeader()->setStretchLastSection(true);
        
        connect(m_view, &QTableView::customContextMenuRequested,
                this, &DataTableManager::showContextMenu);
        
        // 按钮
        QPushButton *addRowBtn = new QPushButton("添加行");
        QPushButton *deleteRowBtn = new QPushButton("删除行");
        QPushButton *addColBtn = new QPushButton("添加列");
        QPushButton *deleteColBtn = new QPushButton("删除列");
        QPushButton *importBtn = new QPushButton("导入CSV");
        QPushButton *exportBtn = new QPushButton("导出CSV");
        QPushButton *searchBtn = new QPushButton("查找");
        
        connect(addRowBtn, &QPushButton::clicked, this, &DataTableManager::addRow);
        connect(deleteRowBtn, &QPushButton::clicked, this, &DataTableManager::deleteRow);
        connect(addColBtn, &QPushButton::clicked, this, &DataTableManager::addColumn);
        connect(deleteColBtn, &QPushButton::clicked, this, &DataTableManager::deleteColumn);
        connect(importBtn, &QPushButton::clicked, this, &DataTableManager::importCSV);
        connect(exportBtn, &QPushButton::clicked, this, &DataTableManager::exportCSV);
        connect(searchBtn, &QPushButton::clicked, this, &DataTableManager::searchData);
        
        // 布局
        QVBoxLayout *mainLayout = new QVBoxLayout;
        mainLayout->addWidget(m_view);
        
        QHBoxLayout *btnLayout1 = new QHBoxLayout;
        btnLayout1->addWidget(addRowBtn);
        btnLayout1->addWidget(deleteRowBtn);
        btnLayout1->addWidget(addColBtn);
        btnLayout1->addWidget(deleteColBtn);
        
        QHBoxLayout *btnLayout2 = new QHBoxLayout;
        btnLayout2->addWidget(importBtn);
        btnLayout2->addWidget(exportBtn);
        btnLayout2->addWidget(searchBtn);
        btnLayout2->addStretch();
        
        mainLayout->addLayout(btnLayout1);
        mainLayout->addLayout(btnLayout2);
        
        setLayout(mainLayout);
    }
    
    void loadSampleData() {
        QList<QList<QString>> data = {
            {"001", "项目A", "100", "进行中", "重要项目"},
            {"002", "项目B", "200", "已完成", "已结束"},
            {"003", "项目C", "150", "暂停", "等待审批"},
            {"004", "项目D", "300", "进行中", "高优先级"},
            {"005", "项目E", "80", "计划中", "准备启动"}
        };
        
        for (const QList<QString> &row : data) {
            QList<QStandardItem*> items;
            for (int i = 0; i < row.size(); ++i) {
                QStandardItem *item = new QStandardItem(row[i]);
                
                // 第一列不可编辑
                if (i == 0) {
                    item->setEditable(false);
                }
                
                // 第三列右对齐
                if (i == 2) {
                    item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
                }
                
                // 根据状态设置颜色
                if (i == 3) {
                    if (row[i] == "进行中") {
                        item->setForeground(QBrush(QColor(0, 120, 0)));
                    } else if (row[i] == "已完成") {
                        item->setForeground(QBrush(Qt::gray));
                    } else if (row[i] == "暂停") {
                        item->setForeground(QBrush(Qt::red));
                    }
                }
                
                items << item;
            }
            m_model->appendRow(items);
        }
    }
    
private slots:
    void addRow() {
        int newId = m_model->rowCount() + 1;
        
        QList<QStandardItem*> items;
        QStandardItem *idItem = new QStandardItem(QString::number(newId, 10).rightJustified(3, '0'));
        idItem->setEditable(false);
        items << idItem;
        items << new QStandardItem("新项目");
        
        QStandardItem *valueItem = new QStandardItem("0");
        valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
        items << valueItem;
        
        items << new QStandardItem("计划中");
        items << new QStandardItem("");
        
        m_model->appendRow(items);
        
        // 滚动到新行
        m_view->scrollToBottom();
    }
    
    void deleteRow() {
        QModelIndexList selected = m_view->selectionModel()->selectedRows();
        if (selected.isEmpty())
            return;
        
        // 从后往前删除
        QList<int> rows;
        for (const QModelIndex &index : selected) {
            rows.append(index.row());
        }
        std::sort(rows.begin(), rows.end(), std::greater<int>());
        
        for (int row : rows) {
            m_model->removeRow(row);
        }
    }
    
    void addColumn() {
        bool ok;
        QString name = QInputDialog::getText(this, "添加列", "列名:",
                                             QLineEdit::Normal, "", &ok);
        if (ok && !name.isEmpty()) {
            int col = m_model->columnCount();
            m_model->setHorizontalHeaderItem(col, new QStandardItem(name));
            
            // 为现有行添加空项
            for (int row = 0; row < m_model->rowCount(); ++row) {
                m_model->setItem(row, col, new QStandardItem(""));
            }
        }
    }
    
    void deleteColumn() {
        QModelIndex current = m_view->currentIndex();
        if (current.isValid()) {
            m_model->removeColumn(current.column());
        }
    }
    
    void importCSV() {
        QString fileName = QFileDialog::getOpenFileName(
            this, "导入CSV", "", "CSV文件 (*.csv)");
        
        if (fileName.isEmpty())
            return;
        
        QFile file(fileName);
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
            return;
        
        // 清空现有数据
        m_model->removeRows(0, m_model->rowCount());
        
        QTextStream in(&file);
        bool firstLine = true;
        
        while (!in.atEnd()) {
            QString line = in.readLine();
            QStringList fields = line.split(',');
            
            if (firstLine) {
                // 第一行作为表头
                QList<QStandardItem*> headers;
                for (const QString &field : fields) {
                    headers << new QStandardItem(field.trimmed());
                }
                m_model->setHorizontalHeaderLabels(
                    fields.replaceInStrings(QRegularExpression("^\\s+|\\s+$"), ""));
                firstLine = false;
            } else {
                // 数据行
                QList<QStandardItem*> items;
                for (const QString &field : fields) {
                    items << new QStandardItem(field.trimmed());
                }
                m_model->appendRow(items);
            }
        }
        
        file.close();
    }
    
    void exportCSV() {
        QString fileName = QFileDialog::getSaveFileName(
            this, "导出CSV", "", "CSV文件 (*.csv)");
        
        if (fileName.isEmpty())
            return;
        
        QFile file(fileName);
        if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
            return;
        
        QTextStream out(&file);
        
        // 写入表头
        QStringList headers;
        for (int col = 0; col < m_model->columnCount(); ++col) {
            headers << m_model->horizontalHeaderItem(col)->text();
        }
        out << headers.join(',') << "\n";
        
        // 写入数据
        for (int row = 0; row < m_model->rowCount(); ++row) {
            QStringList rowData;
            for (int col = 0; col < m_model->columnCount(); ++col) {
                QStandardItem *item = m_model->item(row, col);
                rowData << (item ? item->text() : "");
            }
            out << rowData.join(',') << "\n";
        }
        
        file.close();
    }
    
    void searchData() {
        bool ok;
        QString searchText = QInputDialog::getText(this, "查找", "输入搜索文本:",
                                                   QLineEdit::Normal, "", &ok);
        if (!ok || searchText.isEmpty())
            return;
        
        QList<QStandardItem*> found = m_model->findItems(
            searchText, Qt::MatchContains | Qt::MatchRecursive);
        
        if (found.isEmpty()) {
            qDebug() << "未找到匹配项";
            return;
        }
        
        // 选中第一个匹配项
        QStandardItem *first = found.first();
        QModelIndex index = m_model->indexFromItem(first);
        m_view->setCurrentIndex(index);
        m_view->scrollTo(index);
        
        qDebug() << "找到" << found.size() << "个匹配项";
    }
    
    void showContextMenu(const QPoint &pos) {
        QMenu menu;
        QAction *highlightAction = menu.addAction("高亮行");
        QAction *clearHighlightAction = menu.addAction("清除高亮");
        menu.addSeparator();
        QAction *copyAction = menu.addAction("复制");
        
        QAction *selected = menu.exec(m_view->viewport()->mapToGlobal(pos));
        
        if (selected == highlightAction) {
            QModelIndex current = m_view->currentIndex();
            if (current.isValid()) {
                for (int col = 0; col < m_model->columnCount(); ++col) {
                    QStandardItem *item = m_model->item(current.row(), col);
                    if (item) {
                        item->setBackground(QBrush(QColor(255, 255, 200)));
                    }
                }
            }
        } else if (selected == clearHighlightAction) {
            for (int row = 0; row < m_model->rowCount(); ++row) {
                for (int col = 0; col < m_model->columnCount(); ++col) {
                    QStandardItem *item = m_model->item(row, col);
                    if (item) {
                        item->setBackground(QBrush());
                    }
                }
            }
        }
    }
};

5.2.6 实战:产品分类树

实现一个产品分类管理系统。

cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QTreeView>
#include <QStandardItemModel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QInputDialog>
#include <QMessageBox>

class ProductCategoryTree : public QWidget {
    Q_OBJECT
    
private:
    QStandardItemModel *m_model;
    QTreeView *m_view;
    
public:
    ProductCategoryTree(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        loadSampleData();
        setWindowTitle("产品分类管理");
        resize(600, 500);
    }
    
private:
    void setupUI() {
        m_model = new QStandardItemModel(this);
        m_model->setHorizontalHeaderLabels({"分类名称", "产品数", "描述"});
        
        m_view = new QTreeView;
        m_view->setModel(m_model);
        m_view->setEditTriggers(QAbstractItemView::DoubleClicked);
        m_view->setColumnWidth(0, 250);
        m_view->setColumnWidth(1, 80);
        m_view->expandAll();
        
        QPushButton *addRootBtn = new QPushButton("添加根分类");
        QPushButton *addChildBtn = new QPushButton("添加子分类");
        QPushButton *deleteBtn = new QPushButton("删除分类");
        QPushButton *searchBtn = new QPushButton("查找分类");
        QPushButton *expandBtn = new QPushButton("全部展开");
        QPushButton *collapseBtn = new QPushButton("全部折叠");
        
        connect(addRootBtn, &QPushButton::clicked, this, &ProductCategoryTree::addRootCategory);
        connect(addChildBtn, &QPushButton::clicked, this, &ProductCategoryTree::addChildCategory);
        connect(deleteBtn, &QPushButton::clicked, this, &ProductCategoryTree::deleteCategory);
        connect(searchBtn, &QPushButton::clicked, this, &ProductCategoryTree::searchCategory);
        connect(expandBtn, &QPushButton::clicked, m_view, &QTreeView::expandAll);
        connect(collapseBtn, &QPushButton::clicked, m_view, &QTreeView::collapseAll);
        
        QVBoxLayout *layout = new QVBoxLayout;
        layout->addWidget(m_view);
        
        QHBoxLayout *btnLayout1 = new QHBoxLayout;
        btnLayout1->addWidget(addRootBtn);
        btnLayout1->addWidget(addChildBtn);
        btnLayout1->addWidget(deleteBtn);
        
        QHBoxLayout *btnLayout2 = new QHBoxLayout;
        btnLayout2->addWidget(searchBtn);
        btnLayout2->addWidget(expandBtn);
        btnLayout2->addWidget(collapseBtn);
        btnLayout2->addStretch();
        
        layout->addLayout(btnLayout1);
        layout->addLayout(btnLayout2);
        
        setLayout(layout);
    }
    
    void loadSampleData() {
        // 电子产品
        QStandardItem *electronics = new QStandardItem(QIcon(":/folder.png"), "电子产品");
        QStandardItem *electronicsCount = new QStandardItem("25");
        QStandardItem *electronicsDesc = new QStandardItem("各类电子设备");
        m_model->appendRow(QList<QStandardItem*>() << electronics << electronicsCount << electronicsDesc);
        
        // 手机
        QStandardItem *phones = new QStandardItem(QIcon(":/folder.png"), "手机");
        QStandardItem *phonesCount = new QStandardItem("15");
        QStandardItem *phonesDesc = new QStandardItem("智能手机");
        electronics->appendRow(QList<QStandardItem*>() << phones << phonesCount << phonesDesc);
        
        // Android
        QStandardItem *android = new QStandardItem("Android");
        QStandardItem *androidCount = new QStandardItem("10");
        phones->appendRow(QList<QStandardItem*>() << android << androidCount << new QStandardItem(""));
        
        // iOS
        QStandardItem *ios = new QStandardItem("iOS");
        QStandardItem *iosCount = new QStandardItem("5");
        phones->appendRow(QList<QStandardItem*>() << ios << iosCount << new QStandardItem(""));
        
        // 电脑
        QStandardItem *computers = new QStandardItem(QIcon(":/folder.png"), "电脑");
        QStandardItem *computersCount = new QStandardItem("10");
        electronics->appendRow(QList<QStandardItem*>() << computers << computersCount << new QStandardItem(""));
        
        // 服装
        QStandardItem *clothing = new QStandardItem(QIcon(":/folder.png"), "服装");
        clothing->setForeground(QBrush(QColor(0, 100, 200)));
        m_model->appendRow(QList<QStandardItem*>() << clothing << new QStandardItem("50") << new QStandardItem("各类服饰"));
        
        // 男装
        QStandardItem *men = new QStandardItem("男装");
        clothing->appendRow(QList<QStandardItem*>() << men << new QStandardItem("25") << new QStandardItem(""));
        
        // 女装
        QStandardItem *women = new QStandardItem("女装");
        clothing->appendRow(QList<QStandardItem*>() << women << new QStandardItem("25") << new QStandardItem(""));
    }
    
private slots:
    void addRootCategory() {
        bool ok;
        QString name = QInputDialog::getText(this, "添加根分类", "分类名称:",
                                             QLineEdit::Normal, "", &ok);
        if (ok && !name.isEmpty()) {
            QStandardItem *item = new QStandardItem(QIcon(":/folder.png"), name);
            m_model->appendRow(QList<QStandardItem*>() 
                << item << new QStandardItem("0") << new QStandardItem(""));
        }
    }
    
    void addChildCategory() {
        QModelIndex current = m_view->currentIndex();
        if (!current.isValid()) {
            QMessageBox::warning(this, "提示", "请先选择父分类");
            return;
        }
        
        bool ok;
        QString name = QInputDialog::getText(this, "添加子分类", "分类名称:",
                                             QLineEdit::Normal, "", &ok);
        if (ok && !name.isEmpty()) {
            QStandardItem *parentItem = m_model->itemFromIndex(current);
            QStandardItem *item = new QStandardItem(name);
            parentItem->appendRow(QList<QStandardItem*>() 
                << item << new QStandardItem("0") << new QStandardItem(""));
            
            m_view->expand(current);
        }
    }
    
    void deleteCategory() {
        QModelIndex current = m_view->currentIndex();
        if (!current.isValid())
            return;
        
        QStandardItem *item = m_model->itemFromIndex(current);
        
        if (item->hasChildren()) {
            QMessageBox::warning(this, "提示", "不能删除包含子分类的分类");
            return;
        }
        
        QStandardItem *parent = item->parent();
        if (parent) {
            parent->removeRow(current.row());
        } else {
            m_model->removeRow(current.row());
        }
    }
    
    void searchCategory() {
        bool ok;
        QString searchText = QInputDialog::getText(this, "查找分类", "输入分类名称:",
                                                   QLineEdit::Normal, "", &ok);
        if (!ok || searchText.isEmpty())
            return;
        
        QList<QStandardItem*> found = m_model->findItems(
            searchText, Qt::MatchContains | Qt::MatchRecursive, 0);
        
        if (found.isEmpty()) {
            QMessageBox::information(this, "查找结果", "未找到匹配的分类");
            return;
        }
        
        // 展开并选中第一个匹配项
        QStandardItem *firstItem = found.first();
        QModelIndex index = m_model->indexFromItem(firstItem);
        
        // 展开父节点
        QStandardItem *parent = firstItem->parent();
        while (parent) {
            m_view->expand(m_model->indexFromItem(parent));
            parent = parent->parent();
        }
        
        m_view->setCurrentIndex(index);
        m_view->scrollTo(index);
        
        QMessageBox::information(this, "查找结果", 
            QString("找到 %1 个匹配项").arg(found.size()));
    }
};

本节小结

QStandardItemModel 是最强大灵活的便捷模型类

支持多种结构 列表、表格、树形都可以

QStandardItem 可存储丰富的数据和格式

findItems() 提供强大的查找功能

适用场景 通用数据管理、分类树、配置编辑器等

  • QStandardItem和QStandardItemModel
  • 复杂数据的添加
  • 树形结构的构建
  • 查找功能:findItems()
  • 实战:通用数据表格
  • 实战:产品分类树

5.3 QFileSystemModel

QFileSystemModel 是专门用于显示文件系统的便捷模型类,提供了完整的文件系统访问功能。

5.3.1 文件系统模型的使用

基本用法

cpp 复制代码
#include <QApplication>
#include <QTreeView>
#include <QFileSystemModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建文件系统模型
    QFileSystemModel *model = new QFileSystemModel;
    model->setRootPath("");  // 设置根路径(空字符串表示整个文件系统)
    
    // 创建树形视图
    QTreeView *view = new QTreeView;
    view->setModel(model);
    
    // 设置视图的根索引(决定显示哪个目录为根)
    view->setRootIndex(model->index("C:/"));  // Windows
    // view->setRootIndex(model->index("/home"));  // Linux/Mac
    
    view->resize(800, 600);
    view->show();
    
    return app.exec();
}

常用方法

cpp 复制代码
QFileSystemModel *model = new QFileSystemModel;

// 设置根路径
model->setRootPath(QDir::currentPath());  // 当前目录
model->setRootPath("C:/");  // 特定路径

// 获取文件信息
QModelIndex index = model->index("C:/path/to/file.txt");
QString fileName = model->fileName(index);  // 文件名
QString filePath = model->filePath(index);  // 完整路径
QFileInfo fileInfo = model->fileInfo(index);  // QFileInfo对象
qint64 fileSize = model->size(index);  // 文件大小
QString fileType = model->type(index);  // 文件类型
QDateTime lastModified = model->lastModified(index);  // 修改时间

// 判断是否是目录
bool isDir = model->isDir(index);

// 删除文件/目录
bool success = model->remove(index);

// 创建目录
QModelIndex parentIndex = model->index("C:/parent");
QModelIndex newDir = model->mkdir(parentIndex, "NewFolder");

// 重命名
model->setData(index, "newName.txt", Qt::EditRole);

5.3.2 过滤器设置

设置过滤器

cpp 复制代码
QFileSystemModel *model = new QFileSystemModel;

// 显示所有文件和目录
model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);

// 只显示目录
model->setFilter(QDir::Dirs | QDir::NoDotAndDotDot);

// 只显示文件
model->setFilter(QDir::Files);

// 显示隐藏文件
model->setFilter(QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot);

// 常用过滤器组合
QDir::AllEntries      // 所有条目
QDir::Dirs            // 只有目录
QDir::Files           // 只有文件
QDir::Drives          // 驱动器(Windows)
QDir::NoSymLinks      // 排除符号链接
QDir::NoDotAndDotDot  // 排除 . 和 ..
QDir::Hidden          // 包括隐藏文件
QDir::System          // 包括系统文件

名称过滤器

cpp 复制代码
// 只显示特定扩展名的文件
QStringList nameFilters;
nameFilters << "*.txt" << "*.doc" << "*.pdf";
model->setNameFilters(nameFilters);

// 名称过滤器是否禁用不匹配的项(false表示隐藏,true表示禁用)
model->setNameFilterDisables(false);  // 隐藏不匹配的文件
model->setNameFilterDisables(true);   // 禁用(变灰)不匹配的文件

5.3.3 文件信息获取

获取详细文件信息

cpp 复制代码
QFileSystemModel *model = new QFileSystemModel;
QModelIndex index = model->index("C:/path/to/file.txt");

// 获取QFileInfo对象
QFileInfo info = model->fileInfo(index);

// 文件基本信息
QString fileName = info.fileName();           // 文件名
QString baseName = info.baseName();          // 不含扩展名的文件名
QString suffix = info.suffix();              // 扩展名
QString absolutePath = info.absolutePath();  // 绝对路径
QString canonicalPath = info.canonicalPath(); // 规范路径

// 文件大小
qint64 size = info.size();
QString sizeStr = QString::number(size / 1024.0, 'f', 2) + " KB";

// 时间信息
QDateTime created = info.birthTime();        // 创建时间
QDateTime modified = info.lastModified();    // 修改时间
QDateTime accessed = info.lastRead();        // 访问时间

// 权限信息
bool isReadable = info.isReadable();
bool isWritable = info.isWritable();
bool isExecutable = info.isExecutable();

// 类型判断
bool isFile = info.isFile();
bool isDir = info.isDir();
bool isSymLink = info.isSymLink();
bool isHidden = info.isHidden();

格式化文件大小

cpp 复制代码
QString formatSize(qint64 bytes) {
    const qint64 KB = 1024;
    const qint64 MB = KB * 1024;
    const qint64 GB = MB * 1024;
    
    if (bytes >= GB) {
        return QString::number(bytes / (double)GB, 'f', 2) + " GB";
    } else if (bytes >= MB) {
        return QString::number(bytes / (double)MB, 'f', 2) + " MB";
    } else if (bytes >= KB) {
        return QString::number(bytes / (double)KB, 'f', 2) + " KB";
    } else {
        return QString::number(bytes) + " B";
    }
}

5.3.4 实战:简易文件浏览器

实现一个功能完整的文件浏览器。

cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QTreeView>
#include <QListView>
#include <QFileSystemModel>
#include <QSplitter>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>
#include <QTextEdit>
#include <QFileIconProvider>
#include <QMessageBox>
#include <QInputDialog>
#include <QMenu>
#include <QDesktopServices>
#include <QUrl>

class FileBrowser : public QWidget {
    Q_OBJECT
    
private:
    QFileSystemModel *m_model;
    QTreeView *m_treeView;
    QListView *m_listView;
    QLineEdit *m_pathEdit;
    QTextEdit *m_infoText;
    QString m_currentPath;
    
public:
    FileBrowser(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        setWindowTitle("文件浏览器");
        resize(1000, 700);
    }
    
private:
    void setupUI() {
        // 创建文件系统模型
        m_model = new QFileSystemModel(this);
        m_model->setRootPath("");
        m_model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
        
        // 树形视图(目录结构)
        m_treeView = new QTreeView;
        m_treeView->setModel(m_model);
        m_treeView->setRootIndex(m_model->index(QDir::currentPath()));
        m_treeView->hideColumn(1);  // 隐藏大小列
        m_treeView->hideColumn(2);  // 隐藏类型列
        m_treeView->hideColumn(3);  // 隐藏修改时间列
        m_treeView->setHeaderHidden(true);
        m_treeView->setAnimated(true);
        
        // 列表视图(当前目录内容)
        m_listView = new QListView;
        m_listView->setModel(m_model);
        m_listView->setRootIndex(m_model->index(QDir::currentPath()));
        m_listView->setViewMode(QListView::IconMode);
        m_listView->setIconSize(QSize(64, 64));
        m_listView->setGridSize(QSize(100, 100));
        m_listView->setResizeMode(QListView::Adjust);
        m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
        
        // 路径编辑框
        m_pathEdit = new QLineEdit;
        m_pathEdit->setText(QDir::currentPath());
        m_pathEdit->setReadOnly(true);
        
        QPushButton *upBtn = new QPushButton("上一级");
        QPushButton *homeBtn = new QPushButton("主目录");
        QPushButton *refreshBtn = new QPushButton("刷新");
        
        connect(upBtn, &QPushButton::clicked, this, &FileBrowser::navigateUp);
        connect(homeBtn, &QPushButton::clicked, this, &FileBrowser::navigateHome);
        connect(refreshBtn, &QPushButton::clicked, [=]() {
            m_model->setRootPath("");  // 刷新
        });
        
        // 信息显示区
        m_infoText = new QTextEdit;
        m_infoText->setReadOnly(true);
        m_infoText->setMaximumHeight(120);
        
        // 监听选择变化
        connect(m_treeView->selectionModel(), &QItemSelectionModel::currentChanged,
                this, &FileBrowser::onTreeSelectionChanged);
        connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged,
                this, &FileBrowser::onListSelectionChanged);
        connect(m_listView, &QListView::doubleClicked,
                this, &FileBrowser::onItemDoubleClicked);
        connect(m_listView, &QListView::customContextMenuRequested,
                this, &FileBrowser::showContextMenu);
        
        // 布局
        QHBoxLayout *pathLayout = new QHBoxLayout;
        pathLayout->addWidget(new QLabel("路径:"));
        pathLayout->addWidget(m_pathEdit);
        pathLayout->addWidget(upBtn);
        pathLayout->addWidget(homeBtn);
        pathLayout->addWidget(refreshBtn);
        
        QSplitter *mainSplitter = new QSplitter(Qt::Horizontal);
        mainSplitter->addWidget(m_treeView);
        mainSplitter->addWidget(m_listView);
        mainSplitter->setStretchFactor(0, 1);
        mainSplitter->setStretchFactor(1, 3);
        
        QVBoxLayout *mainLayout = new QVBoxLayout;
        mainLayout->addLayout(pathLayout);
        mainLayout->addWidget(mainSplitter);
        mainLayout->addWidget(m_infoText);
        
        setLayout(mainLayout);
    }
    
private slots:
    void onTreeSelectionChanged(const QModelIndex &current, const QModelIndex &previous) {
        Q_UNUSED(previous);
        
        if (!current.isValid())
            return;
        
        if (m_model->isDir(current)) {
            QString path = m_model->filePath(current);
            m_listView->setRootIndex(current);
            m_pathEdit->setText(path);
            m_currentPath = path;
            updateFileInfo(current);
        }
    }
    
    void onListSelectionChanged(const QModelIndex &current, const QModelIndex &previous) {
        Q_UNUSED(previous);
        
        if (current.isValid()) {
            updateFileInfo(current);
        }
    }
    
    void onItemDoubleClicked(const QModelIndex &index) {
        if (!index.isValid())
            return;
        
        if (m_model->isDir(index)) {
            // 进入目录
            m_listView->setRootIndex(index);
            m_pathEdit->setText(m_model->filePath(index));
            m_currentPath = m_model->filePath(index);
        } else {
            // 打开文件
            QString filePath = m_model->filePath(index);
            QDesktopServices::openUrl(QUrl::fromLocalFile(filePath));
        }
    }
    
    void navigateUp() {
        QModelIndex current = m_listView->rootIndex();
        QModelIndex parent = current.parent();
        
        if (parent.isValid()) {
            m_listView->setRootIndex(parent);
            m_pathEdit->setText(m_model->filePath(parent));
            m_currentPath = m_model->filePath(parent);
        }
    }
    
    void navigateHome() {
        QString homePath = QDir::homePath();
        QModelIndex homeIndex = m_model->index(homePath);
        m_listView->setRootIndex(homeIndex);
        m_pathEdit->setText(homePath);
        m_currentPath = homePath;
    }
    
    void updateFileInfo(const QModelIndex &index) {
        if (!index.isValid())
            return;
        
        QFileInfo info = m_model->fileInfo(index);
        
        QString html = "<b>名称:</b> " + info.fileName() + "<br>";
        html += "<b>路径:</b> " + info.absolutePath() + "<br>";
        html += "<b>类型:</b> " + (info.isDir() ? "文件夹" : m_model->type(index)) + "<br>";
        
        if (info.isFile()) {
            qint64 size = info.size();
            html += "<b>大小:</b> " + formatSize(size) + "<br>";
        }
        
        html += "<b>修改时间:</b> " + info.lastModified().toString("yyyy-MM-dd hh:mm:ss") + "<br>";
        html += "<b>可读:</b> " + (info.isReadable() ? "是" : "否") + " | ";
        html += "<b>可写:</b> " + (info.isWritable() ? "是" : "否") + " | ";
        html += "<b>可执行:</b> " + (info.isExecutable() ? "是" : "否");
        
        m_infoText->setHtml(html);
    }
    
    QString formatSize(qint64 bytes) {
        const qint64 KB = 1024;
        const qint64 MB = KB * 1024;
        const qint64 GB = MB * 1024;
        
        if (bytes >= GB) {
            return QString::number(bytes / (double)GB, 'f', 2) + " GB";
        } else if (bytes >= MB) {
            return QString::number(bytes / (double)MB, 'f', 2) + " MB";
        } else if (bytes >= KB) {
            return QString::number(bytes / (double)KB, 'f', 2) + " KB";
        } else {
            return QString::number(bytes) + " B";
        }
    }
    
    void showContextMenu(const QPoint &pos) {
        QModelIndex index = m_listView->indexAt(pos);
        
        QMenu menu;
        QAction *openAction = menu.addAction("打开");
        menu.addSeparator();
        QAction *newFolderAction = menu.addAction("新建文件夹");
        QAction *renameAction = menu.addAction("重命名");
        QAction *deleteAction = menu.addAction("删除");
        menu.addSeparator();
        QAction *propertiesAction = menu.addAction("属性");
        
        if (!index.isValid()) {
            openAction->setEnabled(false);
            renameAction->setEnabled(false);
            deleteAction->setEnabled(false);
            propertiesAction->setEnabled(false);
        }
        
        QAction *selected = menu.exec(m_listView->viewport()->mapToGlobal(pos));
        
        if (selected == openAction) {
            onItemDoubleClicked(index);
        } else if (selected == newFolderAction) {
            createNewFolder();
        } else if (selected == renameAction) {
            renameItem(index);
        } else if (selected == deleteAction) {
            deleteItem(index);
        } else if (selected == propertiesAction) {
            updateFileInfo(index);
        }
    }
    
    void createNewFolder() {
        bool ok;
        QString name = QInputDialog::getText(this, "新建文件夹", "文件夹名称:",
                                             QLineEdit::Normal, "新建文件夹", &ok);
        if (ok && !name.isEmpty()) {
            QModelIndex current = m_listView->rootIndex();
            m_model->mkdir(current, name);
        }
    }
    
    void renameItem(const QModelIndex &index) {
        if (!index.isValid())
            return;
        
        QString oldName = m_model->fileName(index);
        bool ok;
        QString newName = QInputDialog::getText(this, "重命名", "新名称:",
                                                QLineEdit::Normal, oldName, &ok);
        if (ok && !newName.isEmpty() && newName != oldName) {
            m_model->setData(index, newName, Qt::EditRole);
        }
    }
    
    void deleteItem(const QModelIndex &index) {
        if (!index.isValid())
            return;
        
        QString fileName = m_model->fileName(index);
        QMessageBox::StandardButton reply = QMessageBox::question(
            this, "确认删除",
            QString("确定要删除 '%1' 吗?").arg(fileName),
            QMessageBox::Yes | QMessageBox::No);
        
        if (reply == QMessageBox::Yes) {
            if (!m_model->remove(index)) {
                QMessageBox::warning(this, "错误", "删除失败");
            }
        }
    }
};

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

本节小结

QFileSystemModel 专门用于文件系统访问

自动更新 文件系统变化时自动刷新

过滤器 支持多种过滤方式

懒加载 按需加载目录内容,性能优秀

适用场景 文件浏览器、文件选择器、资源管理器

  • 文件系统模型的使用
  • 过滤器设置
  • 文件信息获取
  • 实战:简易文件浏览器

5.4 QSqlTableModel和QSqlQueryModel

Qt 提供了专门用于数据库操作的模型类,可以直接将数据库表或查询结果显示在视图中。

5.4.1 数据库模型简介

QSqlTableModel - 用于单表操作:

  • 提供可编辑的数据库表视图
  • 支持插入、删除、更新操作
  • 自动处理SQL语句

QSqlQueryModel - 用于只读查询:

  • 执行任意SQL查询
  • 只读模式
  • 适合复杂查询和联表操作

QSqlRelationalTableModel - 关联表模型:

  • 继承自QSqlTableModel
  • 支持外键关联
  • 自动显示关联表的数据

5.4.2 与数据库的集成

数据库连接

cpp 复制代码
#include <QSqlDatabase>
#include <QSqlError>
#include <QDebug>

bool connectDatabase() {
    // 创建数据库连接
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName("mydatabase.db");
    
    if (!db.open()) {
        qDebug() << "Error: " << db.lastError().text();
        return false;
    }
    
    return true;
}

使用QSqlTableModel

cpp 复制代码
#include <QSqlTableModel>

// 创建模型
QSqlTableModel *model = new QSqlTableModel;
model->setTable("employees");  // 设置表名
model->select();  // 执行查询

// 设置表头
model->setHeaderData(0, Qt::Horizontal, "ID");
model->setHeaderData(1, Qt::Horizontal, "姓名");
model->setHeaderData(2, Qt::Horizontal, "部门");
model->setHeaderData(3, Qt::Horizontal, "薪资");

// 设置编辑策略
model->setEditStrategy(QSqlTableModel::OnManualSubmit);  // 手动提交
// model->setEditStrategy(QSqlTableModel::OnFieldChange);  // 字段改变时提交
// model->setEditStrategy(QSqlTableModel::OnRowChange);    // 行改变时提交

// 创建视图
QTableView *view = new QTableView;
view->setModel(model);

使用QSqlQueryModel

cpp 复制代码
#include <QSqlQueryModel>

// 创建模型
QSqlQueryModel *model = new QSqlQueryModel;

// 执行查询
model->setQuery("SELECT id, name, department, salary FROM employees WHERE salary > 5000");

// 设置表头
model->setHeaderData(0, Qt::Horizontal, "ID");
model->setHeaderData(1, Qt::Horizontal, "姓名");
model->setHeaderData(2, Qt::Horizontal, "部门");
model->setHeaderData(3, Qt::Horizontal, "薪资");

// 创建视图
QTableView *view = new QTableView;
view->setModel(model);

5.4.3 实战:数据库数据展示

实现一个完整的员工管理系统。

cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QTableView>
#include <QSqlDatabase>
#include <QSqlTableModel>
#include <QSqlQuery>
#include <QSqlError>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMessageBox>
#include <QInputDialog>
#include <QHeaderView>

class EmployeeManager : public QWidget {
    Q_OBJECT
    
private:
    QSqlTableModel *m_model;
    QTableView *m_view;
    
public:
    EmployeeManager(QWidget *parent = nullptr) : QWidget(parent) {
        if (!initDatabase()) {
            QMessageBox::critical(this, "错误", "无法初始化数据库");
            return;
        }
        
        setupUI();
        setWindowTitle("员工管理系统");
        resize(800, 600);
    }
    
private:
    bool initDatabase() {
        // 创建数据库连接
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
        db.setDatabaseName("employees.db");
        
        if (!db.open()) {
            qDebug() << "Error: " << db.lastError().text();
            return false;
        }
        
        // 创建表
        QSqlQuery query;
        QString createTable = 
            "CREATE TABLE IF NOT EXISTS employees ("
            "id INTEGER PRIMARY KEY AUTOINCREMENT, "
            "name TEXT NOT NULL, "
            "department TEXT, "
            "position TEXT, "
            "salary REAL, "
            "hire_date TEXT)";
        
        if (!query.exec(createTable)) {
            qDebug() << "Create table error: " << query.lastError().text();
            return false;
        }
        
        // 插入示例数据(如果表为空)
        query.exec("SELECT COUNT(*) FROM employees");
        if (query.next() && query.value(0).toInt() == 0) {
            insertSampleData();
        }
        
        return true;
    }
    
    void insertSampleData() {
        QSqlQuery query;
        query.prepare("INSERT INTO employees (name, department, position, salary, hire_date) "
                     "VALUES (?, ?, ?, ?, ?)");
        
        QList<QList<QVariant>> data = {
            {"张三", "技术部", "工程师", 8000, "2020-01-15"},
            {"李四", "销售部", "销售经理", 12000, "2019-06-10"},
            {"王五", "技术部", "高级工程师", 15000, "2018-03-20"},
            {"赵六", "人事部", "HR", 6000, "2021-09-01"},
            {"孙七", "财务部", "会计", 7000, "2020-05-15"}
        };
        
        for (const QList<QVariant> &row : data) {
            query.addBindValue(row[0]);
            query.addBindValue(row[1]);
            query.addBindValue(row[2]);
            query.addBindValue(row[3]);
            query.addBindValue(row[4]);
            query.exec();
        }
    }
    
    void setupUI() {
        // 创建模型
        m_model = new QSqlTableModel(this);
        m_model->setTable("employees");
        m_model->setEditStrategy(QSqlTableModel::OnManualSubmit);
        m_model->select();
        
        // 设置表头
        m_model->setHeaderData(0, Qt::Horizontal, "ID");
        m_model->setHeaderData(1, Qt::Horizontal, "姓名");
        m_model->setHeaderData(2, Qt::Horizontal, "部门");
        m_model->setHeaderData(3, Qt::Horizontal, "职位");
        m_model->setHeaderData(4, Qt::Horizontal, "薪资");
        m_model->setHeaderData(5, Qt::Horizontal, "入职日期");
        
        // 创建视图
        m_view = new QTableView;
        m_view->setModel(m_model);
        m_view->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_view->setSelectionMode(QAbstractItemView::SingleSelection);
        m_view->setAlternatingRowColors(true);
        m_view->setSortingEnabled(true);
        
        // 隐藏ID列
        m_view->setColumnHidden(0, true);
        
        // 设置列宽
        m_view->setColumnWidth(1, 120);
        m_view->setColumnWidth(2, 100);
        m_view->setColumnWidth(3, 120);
        m_view->setColumnWidth(4, 100);
        m_view->horizontalHeader()->setStretchLastSection(true);
        
        // 按钮
        QPushButton *addBtn = new QPushButton("添加");
        QPushButton *deleteBtn = new QPushButton("删除");
        QPushButton *saveBtn = new QPushButton("保存");
        QPushButton *revertBtn = new QPushButton("撤销");
        QPushButton *refreshBtn = new QPushButton("刷新");
        
        connect(addBtn, &QPushButton::clicked, this, &EmployeeManager::addEmployee);
        connect(deleteBtn, &QPushButton::clicked, this, &EmployeeManager::deleteEmployee);
        connect(saveBtn, &QPushButton::clicked, this, &EmployeeManager::saveChanges);
        connect(revertBtn, &QPushButton::clicked, this, &EmployeeManager::revertChanges);
        connect(refreshBtn, &QPushButton::clicked, [=]() {
            m_model->select();
        });
        
        // 布局
        QVBoxLayout *mainLayout = new QVBoxLayout;
        mainLayout->addWidget(m_view);
        
        QHBoxLayout *btnLayout = new QHBoxLayout;
        btnLayout->addWidget(addBtn);
        btnLayout->addWidget(deleteBtn);
        btnLayout->addStretch();
        btnLayout->addWidget(saveBtn);
        btnLayout->addWidget(revertBtn);
        btnLayout->addWidget(refreshBtn);
        
        mainLayout->addLayout(btnLayout);
        setLayout(mainLayout);
    }
    
private slots:
    void addEmployee() {
        int row = m_model->rowCount();
        m_model->insertRow(row);
        
        // 设置默认值
        m_model->setData(m_model->index(row, 1), "新员工");
        m_model->setData(m_model->index(row, 2), "未分配");
        m_model->setData(m_model->index(row, 3), "职位");
        m_model->setData(m_model->index(row, 4), 0);
        m_model->setData(m_model->index(row, 5), QDate::currentDate().toString("yyyy-MM-dd"));
        
        // 选中新行
        m_view->selectRow(row);
        m_view->scrollToBottom();
    }
    
    void deleteEmployee() {
        QModelIndex current = m_view->currentIndex();
        if (!current.isValid())
            return;
        
        QString name = m_model->data(m_model->index(current.row(), 1)).toString();
        
        QMessageBox::StandardButton reply = QMessageBox::question(
            this, "确认删除",
            QString("确定要删除员工 '%1' 吗?").arg(name),
            QMessageBox::Yes | QMessageBox::No);
        
        if (reply == QMessageBox::Yes) {
            m_model->removeRow(current.row());
        }
    }
    
    void saveChanges() {
        if (m_model->submitAll()) {
            QMessageBox::information(this, "成功", "数据已保存");
        } else {
            QMessageBox::warning(this, "错误", 
                QString("保存失败: %1").arg(m_model->lastError().text()));
        }
    }
    
    void revertChanges() {
        m_model->revertAll();
        QMessageBox::information(this, "撤销", "已撤销所有未保存的更改");
    }
};

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

本节小结

QSqlTableModel 提供可编辑的数据库表视图

QSqlQueryModel 适合只读的复杂查询

自动化 自动处理SQL语句和数据同步

编辑策略 灵活控制数据提交时机

适用场景 数据库管理系统、后台管理工具

  • 数据库模型简介
  • 与数据库的集成
  • 实战:数据库数据展示

第5章总结

🎉 第5章 便捷模型类 已全部完成!

本章涵盖了:

  • ✅ QStringListModel - 简单字符串列表(待办事项)
  • ✅ QStandardItemModel - 通用模型(数据表格、分类树)
  • ✅ QFileSystemModel - 文件系统(文件浏览器)
  • ✅ QSqlTableModel - 数据库表(员工管理系统)

核心知识点

  1. 不同模型类的适用场景
  2. 如何选择合适的便捷模型
  3. 模型的配置和优化技巧
  4. 实战项目的完整实现

接下来可以继续学习第6章"自定义Model实战"!


相关推荐
用户805533698032 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner2 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz7 天前
QML Hello World 入门示例
qt
xcyxiner10 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner11 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner11 天前
DicomViewer (添加模型类)3
qt
xcyxiner12 天前
DicomViewer (目录调整) 2
qt
xcyxiner12 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00614 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术14 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript