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实战"!


相关推荐
chao1898442 小时前
在Qt中实现任意N阶贝塞尔曲线的绘制与动态调节
开发语言·qt
真正的醒悟2 小时前
什么是标准等保架构
开发语言·php
郑州光合科技余经理2 小时前
同城020系统架构实战:中台化设计与部署
java·大数据·开发语言·后端·系统架构·uni-app·php
LcVong2 小时前
Android 25(API 25)+ JDK 17 环境搭建
android·java·开发语言
苏宸啊2 小时前
C++string(一)
开发语言·c++
老鱼说AI3 小时前
深入理解计算机系统1.5:抽象的重要性:操作系统与虚拟机
c语言·开发语言·汇编
a程序小傲3 小时前
高并发下如何防止重复下单?
java·开发语言·算法·面试·职场和发展·状态模式
uoKent3 小时前
c++中的封装、继承与多态
开发语言·c++·算法
Mr -老鬼3 小时前
UpdateEC - EasyClick 项目热更新系统(Rust构建)
开发语言·后端·rust