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 ¤t, 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 ¤t, 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 - 树形视图(展开/折叠、多列)
- ✅ 视图通用属性(选择模型、编辑、右键菜单)
通过本章学习,你已经掌握了:
- 三种主要视图类型的使用
- 如何配置视图的显示和交互行为
- 如何处理选择、编辑、排序等操作
- 如何实现复杂的实战应用(文件浏览器、数据表格、分类管理等)
接下来可以继续学习第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 ¤t, 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 ¤t, 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 - 数据库表(员工管理系统)
核心知识点:
- 不同模型类的适用场景
- 如何选择合适的便捷模型
- 模型的配置和优化技巧
- 实战项目的完整实现
接下来可以继续学习第6章"自定义Model实战"!