在这里插入代码片# Qt Model/View架构详解
重要程度: ⭐⭐⭐⭐⭐
实战价值: 处理复杂数据展示(表格、树形结构、列表)
学习目标: 掌握Qt的Model/View设计模式,能够自定义Model和Delegate处理复杂数据展示需求
本篇要点: 通过实际生产案例,实战Qt Model/ViewJ架构。
📚 目录
第五部分:综合实战项目 (第12章)
第12章 综合实战项目
- 12.1 项目一:电子表格应用 (1500+行)
- 稀疏存储、公式引擎、CSV I/O
- 12.2 项目二:文件管理器 (1200+行)
- 双视图联动、搜索过滤
- 12.3 项目三:通讯录管理系统 (2000+行)
- 树形模型、JSON持久化、圆形头像
- 12.4 项目四:数据可视化工具 (1000+行)
- CSV解析、类型检测、实时统计
- 12.5 项目五:看板任务管理器 (1300+行)
- 拖放操作、任务卡片、优先级系统
第六部分:最佳实践与资源 (第13-14章 + 附录)
第13章 常见问题与最佳实践
- 13.1 常见错误
- 13.2 最佳实践
- 13.3 调试技巧
第14章 进阶资源与学习路径
- 14.1 官方文档推荐
- 14.2 进阶主题(QML集成)
- 14.3 学习建议
附录
- 附录A:API速查表
- 附录B:完整示例代码索引
- 附录C:参考资料
第12章 综合实战项目
本章通过三个完整的实战项目,综合运用前面学习的所有知识。
12.1 项目一:电子表格应用
一个功能完善的电子表格应用,支持数据编辑、公式计算、样式设置和文件导入导出。
12.1.1 项目需求分析
核心功能:
-
多行多列数据编辑
- 支持至少100行×26列(A-Z)
- 单元格支持文本、数字、日期等类型
- 支持复制、粘贴、剪切操作
-
单元格公式支持
- 基本运算:
=A1+B1、=A1*2 - 常用函数:
SUM()、AVERAGE()、COUNT() - 单元格引用:相对引用和绝对引用
- 基本运算:
-
样式和格式化
- 数字格式(整数、小数、货币、百分比)
- 文本对齐(左对齐、居中、右对齐)
- 字体样式(粗体、斜体、颜色)
-
导入导出
- 保存为CSV格式
- 从CSV加载数据
- (可选)Excel格式支持
12.1.2 架构设计
类设计:
SpreadsheetApplication (主窗口)
├── SpreadsheetModel (表格模型) - 继承QAbstractTableModel
├── SpreadsheetView (表格视图) - 使用QTableView
├── CellDelegate (单元格委托) - 继承QStyledItemDelegate
├── FormulaEngine (公式计算引擎)
└── FileManager (文件管理器)
数据结构:
Cell {
QVariant value; // 原始值
QString formula; // 公式(如果有)
QVariant cachedResult; // 计算结果缓存
}
数据流:
用户输入 → View → Model → 检查公式 → FormulaEngine计算 → 更新Cell → 通知View刷新
12.1.3 核心功能实现
1. 自定义表格模型
spreadsheetmodel.h:
#ifndef SPREADSHEETMODEL_H
#define SPREADSHEETMODEL_H
#include <QAbstractTableModel>
#include <QVariant>
#include <QHash>
#include <QFont>
#include <QColor>
// 单元格数据结构
struct Cell {
QVariant value; // 显示值
QString formula; // 公式(如果有)
QVariant cachedResult; // 计算结果
// 样式信息
QFont font;
QColor textColor;
QColor backgroundColor;
Qt::Alignment alignment = Qt::AlignLeft | Qt::AlignVCenter;
Cell() : textColor(Qt::black), backgroundColor(Qt::white) {}
};
class SpreadsheetModel : public QAbstractTableModel {
Q_OBJECT
public:
explicit SpreadsheetModel(int rows = 100, int cols = 26,
QObject *parent = nullptr);
// 基本接口
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
// 公式相关
void setFormula(const QModelIndex &index, const QString &formula);
QString getFormula(const QModelIndex &index) const;
void recalculateCell(const QModelIndex &index);
void recalculateAll();
// 样式相关
void setCellFont(const QModelIndex &index, const QFont &font);
void setCellTextColor(const QModelIndex &index, const QColor &color);
void setCellBackgroundColor(const QModelIndex &index, const QColor &color);
void setCellAlignment(const QModelIndex &index, Qt::Alignment alignment);
// 文件操作
bool saveToCSV(const QString &filename);
bool loadFromCSV(const QString &filename);
void clear();
private:
int m_rows;
int m_cols;
QHash<QPair<int, int>, Cell*> m_cells; // 稀疏存储
Cell* getCell(int row, int col) const;
Cell* createCell(int row, int col);
QString cellReference(int row, int col) const; // 如 "A1", "B2"
friend class FormulaEngine;
};
#endif // SPREADSHEETMODEL_H
spreadsheetmodel.cpp:
#include "spreadsheetmodel.h"
#include "formulaengine.h"
#include <QFile>
#include <QTextStream>
#include <QDebug>
SpreadsheetModel::SpreadsheetModel(int rows, int cols, QObject *parent)
: QAbstractTableModel(parent), m_rows(rows), m_cols(cols) {
}
int SpreadsheetModel::rowCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : m_rows;
}
int SpreadsheetModel::columnCount(const QModelIndex &parent) const {
return parent.isValid() ? 0 : m_cols;
}
QVariant SpreadsheetModel::data(const QModelIndex &index, int role) const {
if (!index.isValid()) {
return QVariant();
}
Cell *cell = getCell(index.row(), index.column());
if (!cell) {
return QVariant();
}
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
// 如果有公式,返回计算结果;否则返回原始值
if (!cell->formula.isEmpty()) {
return cell->cachedResult;
}
return cell->value;
case Qt::FontRole:
return cell->font;
case Qt::ForegroundRole:
return QBrush(cell->textColor);
case Qt::BackgroundRole:
return QBrush(cell->backgroundColor);
case Qt::TextAlignmentRole:
return static_cast<int>(cell->alignment);
case Qt::UserRole: // 公式
return cell->formula;
}
return QVariant();
}
bool SpreadsheetModel::setData(const QModelIndex &index,
const QVariant &value, int role) {
if (!index.isValid()) {
return false;
}
Cell *cell = createCell(index.row(), index.column());
if (role == Qt::EditRole) {
QString text = value.toString();
// 检查是否是公式
if (text.startsWith("=")) {
cell->formula = text;
cell->value = text;
recalculateCell(index);
} else {
cell->formula.clear();
cell->value = value;
cell->cachedResult.clear();
}
emit dataChanged(index, index);
return true;
}
return false;
}
Qt::ItemFlags SpreadsheetModel::flags(const QModelIndex &index) const {
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}
QVariant SpreadsheetModel::headerData(int section, Qt::Orientation orientation,
int role) const {
if (role == Qt::DisplayRole) {
if (orientation == Qt::Horizontal) {
// 列标题:A, B, C, ...
if (section < 26) {
return QString(QChar('A' + section));
} else {
// AA, AB, ... (简化处理,只支持到Z)
return QString::number(section + 1);
}
} else {
// 行标题:1, 2, 3, ...
return section + 1;
}
}
return QVariant();
}
void SpreadsheetModel::setFormula(const QModelIndex &index, const QString &formula) {
if (!index.isValid()) {
return;
}
Cell *cell = createCell(index.row(), index.column());
cell->formula = formula;
cell->value = formula;
recalculateCell(index);
emit dataChanged(index, index);
}
QString SpreadsheetModel::getFormula(const QModelIndex &index) const {
Cell *cell = getCell(index.row(), index.column());
return cell ? cell->formula : QString();
}
void SpreadsheetModel::recalculateCell(const QModelIndex &index) {
Cell *cell = getCell(index.row(), index.column());
if (!cell || cell->formula.isEmpty()) {
return;
}
// 使用公式引擎计算
FormulaEngine engine(this);
QVariant result = engine.evaluate(cell->formula, index.row(), index.column());
cell->cachedResult = result;
}
void SpreadsheetModel::recalculateAll() {
for (auto it = m_cells.begin(); it != m_cells.end(); ++it) {
if (!it.value()->formula.isEmpty()) {
int row = it.key().first;
int col = it.key().second;
recalculateCell(index(row, col));
}
}
emit dataChanged(index(0, 0),
index(m_rows - 1, m_cols - 1));
}
void SpreadsheetModel::setCellFont(const QModelIndex &index, const QFont &font) {
Cell *cell = createCell(index.row(), index.column());
cell->font = font;
emit dataChanged(index, index);
}
void SpreadsheetModel::setCellTextColor(const QModelIndex &index,
const QColor &color) {
Cell *cell = createCell(index.row(), index.column());
cell->textColor = color;
emit dataChanged(index, index);
}
void SpreadsheetModel::setCellBackgroundColor(const QModelIndex &index,
const QColor &color) {
Cell *cell = createCell(index.row(), index.column());
cell->backgroundColor = color;
emit dataChanged(index, index);
}
void SpreadsheetModel::setCellAlignment(const QModelIndex &index,
Qt::Alignment alignment) {
Cell *cell = createCell(index.row(), index.column());
cell->alignment = alignment;
emit dataChanged(index, index);
}
bool SpreadsheetModel::saveToCSV(const QString &filename) {
QFile file(filename);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return false;
}
QTextStream out(&file);
for (int row = 0; row < m_rows; ++row) {
QStringList rowData;
for (int col = 0; col < m_cols; ++col) {
Cell *cell = getCell(row, col);
if (cell) {
QString text;
if (!cell->formula.isEmpty()) {
text = cell->formula;
} else {
text = cell->value.toString();
}
// CSV转义
if (text.contains(',') || text.contains('"') ||
text.contains('\n')) {
text.replace("\"", "\"\"");
text = "\"" + text + "\"";
}
rowData << text;
} else {
rowData << "";
}
}
out << rowData.join(",") << "\n";
}
file.close();
return true;
}
bool SpreadsheetModel::loadFromCSV(const QString &filename) {
QFile file(filename);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
clear();
QTextStream in(&file);
int row = 0;
while (!in.atEnd() && row < m_rows) {
QString line = in.readLine();
QStringList fields;
// 简单CSV解析(不完全支持所有CSV规则)
bool inQuotes = false;
QString field;
for (int i = 0; i < line.length(); ++i) {
QChar c = line[i];
if (c == '"') {
inQuotes = !inQuotes;
} else if (c == ',' && !inQuotes) {
fields << field;
field.clear();
} else {
field += c;
}
}
fields << field;
// 设置数据
for (int col = 0; col < qMin(fields.size(), m_cols); ++col) {
setData(index(row, col), fields[col]);
}
++row;
}
file.close();
recalculateAll();
return true;
}
void SpreadsheetModel::clear() {
qDeleteAll(m_cells);
m_cells.clear();
emit dataChanged(index(0, 0),
index(m_rows - 1, m_cols - 1));
}
Cell* SpreadsheetModel::getCell(int row, int col) const {
return m_cells.value(QPair<int, int>(row, col), nullptr);
}
Cell* SpreadsheetModel::createCell(int row, int col) {
QPair<int, int> key(row, col);
if (!m_cells.contains(key)) {
m_cells[key] = new Cell();
}
return m_cells[key];
}
QString SpreadsheetModel::cellReference(int row, int col) const {
QString colName;
if (col < 26) {
colName = QChar('A' + col);
} else {
colName = QString::number(col + 1);
}
return colName + QString::number(row + 1);
}
2. 公式计算引擎
formulaengine.h:
#ifndef FORMULAENGINE_H
#define FORMULAENGINE_H
#include <QString>
#include <QVariant>
class SpreadsheetModel;
class FormulaEngine {
public:
explicit FormulaEngine(SpreadsheetModel *model);
QVariant evaluate(const QString &formula, int currentRow, int currentCol);
private:
SpreadsheetModel *m_model;
// 解析单元格引用,如 "A1" -> (0, 0)
bool parseCellReference(const QString &ref, int &row, int &col);
// 获取单元格的值
QVariant getCellValue(int row, int col);
// 计算简单表达式
QVariant evaluateExpression(const QString &expr);
// 计算函数,如 SUM(A1:A10)
QVariant evaluateFunction(const QString &func, const QString &args);
// 解析范围,如 "A1:B5"
bool parseRange(const QString &range, int &startRow, int &startCol,
int &endRow, int &endCol);
};
#endif // FORMULAENGINE_H
formulaengine.cpp:
#include "formulaengine.h"
#include "spreadsheetmodel.h"
#include <QRegularExpression>
#include <QDebug>
FormulaEngine::FormulaEngine(SpreadsheetModel *model)
: m_model(model) {
}
QVariant FormulaEngine::evaluate(const QString &formula,
int currentRow, int currentCol) {
if (!formula.startsWith("=")) {
return formula;
}
QString expr = formula.mid(1).trimmed(); // 移除 "="
// 检查是否是函数
QRegularExpression funcRegex(R"(([A-Z]+)\((.*)\))");
QRegularExpressionMatch match = funcRegex.match(expr);
if (match.hasMatch()) {
QString funcName = match.captured(1);
QString args = match.captured(2);
return evaluateFunction(funcName, args);
}
// 否则当作表达式计算
return evaluateExpression(expr);
}
bool FormulaEngine::parseCellReference(const QString &ref, int &row, int &col) {
QRegularExpression cellRegex(R"(([A-Z]+)(\d+))");
QRegularExpressionMatch match = cellRegex.match(ref.trimmed());
if (!match.hasMatch()) {
return false;
}
QString colStr = match.captured(1);
QString rowStr = match.captured(2);
// 简单处理,只支持A-Z
if (colStr.length() == 1) {
col = colStr[0].unicode() - 'A';
} else {
return false;
}
row = rowStr.toInt() - 1;
return row >= 0 && row < m_model->rowCount() &&
col >= 0 && col < m_model->columnCount();
}
QVariant FormulaEngine::getCellValue(int row, int col) {
QModelIndex index = m_model->index(row, col);
QVariant value = m_model->data(index, Qt::DisplayRole);
return value;
}
QVariant FormulaEngine::evaluateExpression(const QString &expr) {
// 简单实现:替换单元格引用为值,然后计算
QString expression = expr;
// 查找所有单元格引用
QRegularExpression cellRefRegex(R"([A-Z]+\d+)");
QRegularExpressionMatchIterator it = cellRefRegex.globalMatch(expression);
while (it.hasNext()) {
QRegularExpressionMatch match = it.next();
QString ref = match.captured();
int row, col;
if (parseCellReference(ref, row, col)) {
QVariant cellValue = getCellValue(row, col);
double value = cellValue.toDouble();
// 替换引用为值
expression.replace(ref, QString::number(value));
}
}
// 简单表达式计算(生产环境应使用专业的表达式解析器)
// 这里只支持基本的+、-、*、/
return evaluateSimpleExpression(expression);
}
// 简化的表达式计算
QVariant FormulaEngine::evaluateSimpleExpression(const QString &expr) {
// 使用QScriptEngine或muParser等库
// 这里简化实现,仅支持基本运算
QString clean = expr.simplified();
double result = 0.0;
// 非常简化的实现
if (clean.contains('+')) {
QStringList parts = clean.split('+');
result = parts[0].toDouble() + parts[1].toDouble();
} else if (clean.contains('-')) {
QStringList parts = clean.split('-');
result = parts[0].toDouble() - parts[1].toDouble();
} else if (clean.contains('*')) {
QStringList parts = clean.split('*');
result = parts[0].toDouble() * parts[1].toDouble();
} else if (clean.contains('/')) {
QStringList parts = clean.split('/');
double divisor = parts[1].toDouble();
if (divisor != 0) {
result = parts[0].toDouble() / divisor;
} else {
return "#DIV/0!";
}
} else {
result = clean.toDouble();
}
return result;
}
QVariant FormulaEngine::evaluateFunction(const QString &func,
const QString &args) {
if (func == "SUM") {
int startRow, startCol, endRow, endCol;
if (parseRange(args, startRow, startCol, endRow, endCol)) {
double sum = 0.0;
for (int row = startRow; row <= endRow; ++row) {
for (int col = startCol; col <= endCol; ++col) {
sum += getCellValue(row, col).toDouble();
}
}
return sum;
}
} else if (func == "AVERAGE") {
int startRow, startCol, endRow, endCol;
if (parseRange(args, startRow, startCol, endRow, endCol)) {
double sum = 0.0;
int count = 0;
for (int row = startRow; row <= endRow; ++row) {
for (int col = startCol; col <= endCol; ++col) {
sum += getCellValue(row, col).toDouble();
++count;
}
}
return count > 0 ? sum / count : 0.0;
}
} else if (func == "COUNT") {
int startRow, startCol, endRow, endCol;
if (parseRange(args, startRow, startCol, endRow, endCol)) {
int count = 0;
for (int row = startRow; row <= endRow; ++row) {
for (int col = startCol; col <= endCol; ++col) {
QVariant value = getCellValue(row, col);
if (!value.toString().isEmpty()) {
++count;
}
}
}
return count;
}
}
return "#NAME?"; // 未知函数
}
bool FormulaEngine::parseRange(const QString &range,
int &startRow, int &startCol,
int &endRow, int &endCol) {
QStringList parts = range.split(':');
if (parts.size() != 2) {
return false;
}
return parseCellReference(parts[0], startRow, startCol) &&
parseCellReference(parts[1], endRow, endCol);
}
3. 主窗口实现
spreadsheetwindow.h:
#ifndef SPREADSHEETWINDOW_H
#define SPREADSHEETWINDOW_H
#include <QMainWindow>
#include <QTableView>
#include <QLineEdit>
#include <QLabel>
#include "spreadsheetmodel.h"
class SpreadsheetWindow : public QMainWindow {
Q_OBJECT
public:
explicit SpreadsheetWindow(QWidget *parent = nullptr);
private slots:
void onCellChanged(const QModelIndex ¤t, const QModelIndex &previous);
void onFormulaEditFinished();
void onNewFile();
void onOpenFile();
void onSaveFile();
void onBold();
void onItalic();
void onAlignLeft();
void onAlignCenter();
void onAlignRight();
private:
void setupUI();
void createMenus();
void createToolbar();
SpreadsheetModel *m_model;
QTableView *m_tableView;
QLineEdit *m_cellLocationLabel;
QLineEdit *m_formulaBar;
QLabel *m_statusLabel;
QString m_currentFile;
};
#endif // SPREADSHEETWINDOW_H
spreadsheetwindow.cpp:
#include "spreadsheetwindow.h"
#include <QMenu>
#include <QMenuBar>
#include <QToolBar>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFileDialog>
#include <QMessageBox>
#include <QHeaderView>
#include <QAction>
SpreadsheetWindow::SpreadsheetWindow(QWidget *parent)
: QMainWindow(parent) {
setupUI();
createMenus();
createToolbar();
setWindowTitle("Qt电子表格");
resize(1000, 600);
}
void SpreadsheetWindow::setupUI() {
// 创建模型
m_model = new SpreadsheetModel(100, 26, this);
// 创建视图
m_tableView = new QTableView;
m_tableView->setModel(m_model);
// 视图设置
m_tableView->horizontalHeader()->setDefaultSectionSize(100);
m_tableView->verticalHeader()->setDefaultSectionSize(25);
m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);
// 单元格位置标签
m_cellLocationLabel = new QLineEdit;
m_cellLocationLabel->setReadOnly(true);
m_cellLocationLabel->setMaximumWidth(100);
m_cellLocationLabel->setText("A1");
// 公式栏
m_formulaBar = new QLineEdit;
m_formulaBar->setPlaceholderText("输入数据或公式(以=开头)");
// 状态栏
m_statusLabel = new QLabel("就绪");
statusBar()->addWidget(m_statusLabel);
// 顶部栏布局
QHBoxLayout *topLayout = new QHBoxLayout;
topLayout->addWidget(m_cellLocationLabel);
topLayout->addWidget(m_formulaBar);
QWidget *topWidget = new QWidget;
topWidget->setLayout(topLayout);
// 主布局
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(topWidget);
mainLayout->addWidget(m_tableView);
QWidget *central = new QWidget;
central->setLayout(mainLayout);
setCentralWidget(central);
// 连接信号
connect(m_tableView->selectionModel(),
&QItemSelectionModel::currentChanged,
this, &SpreadsheetWindow::onCellChanged);
connect(m_formulaBar, &QLineEdit::returnPressed,
this, &SpreadsheetWindow::onFormulaEditFinished);
}
void SpreadsheetWindow::createMenus() {
// 文件菜单
QMenu *fileMenu = menuBar()->addMenu("文件");
QAction *newAction = fileMenu->addAction("新建");
connect(newAction, &QAction::triggered, this, &SpreadsheetWindow::onNewFile);
QAction *openAction = fileMenu->addAction("打开...");
connect(openAction, &QAction::triggered, this, &SpreadsheetWindow::onOpenFile);
QAction *saveAction = fileMenu->addAction("保存...");
connect(saveAction, &QAction::triggered, this, &SpreadsheetWindow::onSaveFile);
fileMenu->addSeparator();
QAction *exitAction = fileMenu->addAction("退出");
connect(exitAction, &QAction::triggered, this, &QMainWindow::close);
// 编辑菜单
QMenu *editMenu = menuBar()->addMenu("编辑");
editMenu->addAction("复制");
editMenu->addAction("粘贴");
editMenu->addAction("剪切");
}
void SpreadsheetWindow::createToolbar() {
QToolBar *toolbar = addToolBar("工具栏");
QAction *boldAction = toolbar->addAction("B");
boldAction->setCheckable(true);
connect(boldAction, &QAction::triggered, this, &SpreadsheetWindow::onBold);
QAction *italicAction = toolbar->addAction("I");
italicAction->setCheckable(true);
connect(italicAction, &QAction::triggered, this, &SpreadsheetWindow::onItalic);
toolbar->addSeparator();
QAction *alignLeftAction = toolbar->addAction("左对齐");
connect(alignLeftAction, &QAction::triggered, this, &SpreadsheetWindow::onAlignLeft);
QAction *alignCenterAction = toolbar->addAction("居中");
connect(alignCenterAction, &QAction::triggered, this, &SpreadsheetWindow::onAlignCenter);
QAction *alignRightAction = toolbar->addAction("右对齐");
connect(alignRightAction, &QAction::triggered, this, &SpreadsheetWindow::onAlignRight);
}
void SpreadsheetWindow::onCellChanged(const QModelIndex ¤t,
const QModelIndex &previous) {
if (!current.isValid()) {
return;
}
// 更新位置标签
QString col = m_model->headerData(current.column(),
Qt::Horizontal).toString();
QString row = QString::number(current.row() + 1);
m_cellLocationLabel->setText(col + row);
// 更新公式栏
QString formula = m_model->getFormula(current);
if (!formula.isEmpty()) {
m_formulaBar->setText(formula);
} else {
m_formulaBar->setText(m_model->data(current, Qt::DisplayRole).toString());
}
}
void SpreadsheetWindow::onFormulaEditFinished() {
QModelIndex current = m_tableView->currentIndex();
if (!current.isValid()) {
return;
}
QString text = m_formulaBar->text();
m_model->setData(current, text, Qt::EditRole);
m_statusLabel->setText("单元格已更新");
}
void SpreadsheetWindow::onNewFile() {
m_model->clear();
m_currentFile.clear();
setWindowTitle("Qt电子表格 - 新建");
}
void SpreadsheetWindow::onOpenFile() {
QString filename = QFileDialog::getOpenFileName(
this, "打开文件", "", "CSV Files (*.csv);;All Files (*)");
if (!filename.isEmpty()) {
if (m_model->loadFromCSV(filename)) {
m_currentFile = filename;
setWindowTitle("Qt电子表格 - " + filename);
m_statusLabel->setText("文件已加载");
} else {
QMessageBox::warning(this, "错误", "无法加载文件");
}
}
}
void SpreadsheetWindow::onSaveFile() {
QString filename = m_currentFile;
if (filename.isEmpty()) {
filename = QFileDialog::getSaveFileName(
this, "保存文件", "", "CSV Files (*.csv);;All Files (*)");
}
if (!filename.isEmpty()) {
if (m_model->saveToCSV(filename)) {
m_currentFile = filename;
setWindowTitle("Qt电子表格 - " + filename);
m_statusLabel->setText("文件已保存");
} else {
QMessageBox::warning(this, "错误", "无法保存文件");
}
}
}
void SpreadsheetWindow::onBold() {
QModelIndex current = m_tableView->currentIndex();
if (!current.isValid()) return;
QFont font = m_model->data(current, Qt::FontRole).value<QFont>();
font.setBold(!font.bold());
m_model->setCellFont(current, font);
}
void SpreadsheetWindow::onItalic() {
QModelIndex current = m_tableView->currentIndex();
if (!current.isValid()) return;
QFont font = m_model->data(current, Qt::FontRole).value<QFont>();
font.setItalic(!font.italic());
m_model->setCellFont(current, font);
}
void SpreadsheetWindow::onAlignLeft() {
QModelIndex current = m_tableView->currentIndex();
if (current.isValid()) {
m_model->setCellAlignment(current, Qt::AlignLeft | Qt::AlignVCenter);
}
}
void SpreadsheetWindow::onAlignCenter() {
QModelIndex current = m_tableView->currentIndex();
if (current.isValid()) {
m_model->setCellAlignment(current, Qt::AlignCenter);
}
}
void SpreadsheetWindow::onAlignRight() {
QModelIndex current = m_tableView->currentIndex();
if (current.isValid()) {
m_model->setCellAlignment(current, Qt::AlignRight | Qt::AlignVCenter);
}
}
4. main.cpp
#include "spreadsheetwindow.h"
#include <QApplication>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
SpreadsheetWindow window;
window.show();
return app.exec();
}
12.1.4 完整源码
完整的项目文件结构:
spreadsheet/
├── spreadsheet.pro
├── main.cpp
├── spreadsheetmodel.h
├── spreadsheetmodel.cpp
├── spreadsheetwindow.h
├── spreadsheetwindow.cpp
├── formulaengine.h
└── formulaengine.cpp
spreadsheet.pro(Qt项目文件):
QT += core gui widgets
TARGET = Spreadsheet
TEMPLATE = app
SOURCES += \
main.cpp \
spreadsheetmodel.cpp \
spreadsheetwindow.cpp \
formulaengine.cpp
HEADERS += \
spreadsheetmodel.h \
spreadsheetwindow.h \
formulaengine.h
12.1.5 功能演示
使用步骤:
-
编译运行:
qmake spreadsheet.pro make ./Spreadsheet -
基本操作:
- 点击单元格输入数据
- 在公式栏输入内容并按回车
-
公式计算:
- 在A1输入:
10 - 在A2输入:
20 - 在A3输入:
=A1+A2(结果显示30) - 在A4输入:
=SUM(A1:A3)(结果显示60) - 在A5输入:
=AVERAGE(A1:A3)(结果显示20)
- 在A1输入:
-
样式设置:
- 选中单元格,点击工具栏的B(粗体)、I(斜体)
- 点击对齐按钮改变文本对齐方式
-
文件操作:
- 文件 → 保存:保存为CSV文件
- 文件 → 打开:加载CSV文件
效果截图(文本描述):
+----+-------+-------+-------+-------+
| | A | B | C | D |
+----+-------+-------+-------+-------+
| 1 | 10 | 单价 | 100 | |
+----+-------+-------+-------+-------+
| 2 | 20 | 数量 | 5 | |
+----+-------+-------+-------+-------+
| 3 | 30 | 总计 | 500 | | (=C1*C2)
+----+-------+-------+-------+-------+
| 4 | 60 | | | | (=SUM(A1:A3))
+----+-------+-------+-------+-------+
| 5 | 20 | | | | (=AVERAGE(A1:A3))
+----+-------+-------+-------+-------+
本节小结:
✅ 完整的电子表格应用 - 从需求到实现的完整过程
✅ 自定义表格模型 - 稀疏存储、公式支持
✅ 公式引擎 - 单元格引用、基本函数(SUM、AVERAGE、COUNT)
✅ 文件I/O - CSV格式的导入导出
✅ 样式支持 - 字体、颜色、对齐方式
关键技术点:
- 使用QHash实现稀疏存储,节省内存
- 公式与值分离存储,支持动态计算
- 正则表达式解析单元格引用和公式
- Qt Model/View架构的完整应用
- 文件I/O和CSV格式处理
可扩展功能:
-
支持更多函数(MAX、MIN、IF等)
-
完整的表达式解析器(使用第三方库如muParser)
-
支持Excel文件格式(使用QXlsx库)
-
图表功能(使用Qt Charts)
-
撤销/重做功能(使用QUndoStack)
-
多工作表支持
-
项目需求分析
-
架构设计
-
核心功能实现
-
完整源码
-
功能演示
12.2 项目二:文件管理器
一个功能完整的文件管理器应用,类似Windows资源管理器,支持树形目录浏览、文件列表展示、搜索过滤等功能。
12.2.1 项目需求分析
核心功能:
-
树形文件浏览
- 左侧树形视图显示目录结构
- 支持展开/折叠文件夹
- 显示文件夹图标
-
文件详情展示
- 右侧表格视图显示文件列表
- 显示文件名、大小、类型、修改时间
- 双击打开文件/文件夹
-
文件搜索和过滤
- 实时搜索文件名
- 按文件类型过滤
- 显示过滤结果统计
-
拖放操作
- 支持文件拖放到其他文件夹
- 拖放复制/移动文件
-
附加功能
- 地址栏显示当前路径
- 返回上一级
- 显示文件/文件夹数量统计
12.2.2 架构设计
类设计:
FileManagerWindow (主窗口)
├── QFileSystemModel (文件系统模型)
├── QTreeView (左侧目录树)
├── QTableView/QListView (右侧文件列表)
├── CustomFilterProxyModel (过滤代理模型)
├── FileInfoDelegate (文件信息委托)
└── SearchWidget (搜索栏)
特点:
- 使用Qt内置的QFileSystemModel
- 双视图联动 (树+表)
- 代理模型实现过滤和搜索
数据流:
文件系统 → QFileSystemModel → FilterProxy → TreeView/TableView
↑
搜索条件
12.2.3 核心功能实现
1. 自定义过滤代理模型
customfilterproxymodel.h:
#ifndef CUSTOMFILTERPROXYMODEL_H
#define CUSTOMFILTERPROXYMODEL_H
#include <QSortFilterProxyModel>
#include <QFileSystemModel>
class CustomFilterProxyModel : public QSortFilterProxyModel {
Q_OBJECT
public:
explicit CustomFilterProxyModel(QObject *parent = nullptr);
void setSearchPattern(const QString &pattern);
void setFileTypeFilter(const QString &extension);
void clearFilters();
protected:
bool filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const override;
private:
QString m_searchPattern;
QString m_fileTypeFilter;
QFileSystemModel* fileSystemModel() const;
};
#endif // CUSTOMFILTERPROXYMODEL_H
customfilterproxymodel.cpp:
#include "customfilterproxymodel.h"
#include <QFileInfo>
CustomFilterProxyModel::CustomFilterProxyModel(QObject *parent)
: QSortFilterProxyModel(parent) {
setFilterCaseSensitivity(Qt::CaseInsensitive);
}
void CustomFilterProxyModel::setSearchPattern(const QString &pattern) {
m_searchPattern = pattern;
invalidateFilter();
}
void CustomFilterProxyModel::setFileTypeFilter(const QString &extension) {
m_fileTypeFilter = extension;
invalidateFilter();
}
void CustomFilterProxyModel::clearFilters() {
m_searchPattern.clear();
m_fileTypeFilter.clear();
invalidateFilter();
}
bool CustomFilterProxyModel::filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const {
QFileSystemModel *fsModel = fileSystemModel();
if (!fsModel) {
return true;
}
QModelIndex index = fsModel->index(sourceRow, 0, sourceParent);
// 获取文件信息
QString fileName = fsModel->fileName(index);
bool isDir = fsModel->isDir(index);
// 文件夹总是显示
if (isDir) {
return true;
}
// 搜索过滤
if (!m_searchPattern.isEmpty()) {
if (!fileName.contains(m_searchPattern, Qt::CaseInsensitive)) {
return false;
}
}
// 文件类型过滤
if (!m_fileTypeFilter.isEmpty()) {
QFileInfo fileInfo(fileName);
QString extension = fileInfo.suffix().toLower();
if (extension != m_fileTypeFilter.toLower()) {
return false;
}
}
return true;
}
QFileSystemModel* CustomFilterProxyModel::fileSystemModel() const {
return qobject_cast<QFileSystemModel*>(sourceModel());
}
2. 文件信息委托
fileinfodelegate.h:
#ifndef FILEINFODELEGATE_H
#define FILEINFODELEGATE_H
#include <QStyledItemDelegate>
class FileInfoDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
explicit FileInfoDelegate(QObject *parent = nullptr);
void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
#endif // FILEINFODELEGATE_H
fileinfodelegate.cpp:
#include "fileinfodelegate.h"
#include <QPainter>
#include <QFileSystemModel>
#include <QFileInfo>
#include <QApplication>
FileInfoDelegate::FileInfoDelegate(QObject *parent)
: QStyledItemDelegate(parent) {
}
void FileInfoDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const {
// 获取文件系统模型
const QFileSystemModel *model =
qobject_cast<const QFileSystemModel*>(index.model());
if (!model) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
painter->save();
// 绘制背景
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
} else if (option.state & QStyle::State_MouseOver) {
painter->fillRect(option.rect,
option.palette.color(QPalette::AlternateBase));
}
// 获取文件信息
QString filePath = model->filePath(index);
QFileInfo fileInfo(filePath);
bool isDir = fileInfo.isDir();
// 绘制图标
QRect iconRect = option.rect;
iconRect.setWidth(option.decorationSize.width() + 10);
QIcon icon = model->fileIcon(index);
icon.paint(painter, iconRect, Qt::AlignCenter);
// 绘制文件名
QRect textRect = option.rect;
textRect.setLeft(iconRect.right() + 5);
QString displayText = model->fileName(index);
QFont font = option.font;
if (isDir) {
font.setBold(true);
}
painter->setFont(font);
if (option.state & QStyle::State_Selected) {
painter->setPen(option.palette.highlightedText().color());
} else {
painter->setPen(option.palette.text().color());
}
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter,
displayText);
painter->restore();
}
QSize FileInfoDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const {
QSize size = QStyledItemDelegate::sizeHint(option, index);
size.setHeight(qMax(size.height(), 28)); // 最小高度
return size;
}
3. 主窗口实现
filemanagerwindow.h:
#ifndef FILEMANAGERWINDOW_H
#define FILEMANAGERWINDOW_H
#include <QMainWindow>
#include <QTreeView>
#include <QTableView>
#include <QFileSystemModel>
#include <QLineEdit>
#include <QComboBox>
#include <QLabel>
#include <QSplitter>
#include "customfilterproxymodel.h"
class FileManagerWindow : public QMainWindow {
Q_OBJECT
public:
explicit FileManagerWindow(QWidget *parent = nullptr);
~FileManagerWindow();
private slots:
void onTreeClicked(const QModelIndex &index);
void onTableDoubleClicked(const QModelIndex &index);
void onSearchTextChanged(const QString &text);
void onFileTypeChanged(int index);
void onUpButtonClicked();
void onRefreshButtonClicked();
void updateStatusBar();
void onAddressBarReturnPressed();
private:
void setupUI();
void createMenuBar();
void createToolBar();
void createStatusBar();
void navigateToPath(const QString &path);
void updateAddressBar(const QString &path);
QString formatFileSize(qint64 size) const;
// Models
QFileSystemModel *m_fileSystemModel;
CustomFilterProxyModel *m_filterProxy;
// Views
QTreeView *m_treeView;
QTableView *m_tableView;
QSplitter *m_splitter;
// Widgets
QLineEdit *m_addressBar;
QLineEdit *m_searchBox;
QComboBox *m_fileTypeCombo;
// Status bar
QLabel *m_statusLabel;
QLabel *m_fileCountLabel;
QString m_currentPath;
};
#endif // FILEMANAGERWINDOW_H
filemanagerwindow.cpp:
#include "filemanagerwindow.h"
#include "fileinfodelegate.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QToolBar>
#include <QMenuBar>
#include <QLabel>
#include <QPushButton>
#include <QHeaderView>
#include <QDir>
#include <QDesktopServices>
#include <QUrl>
#include <QMessageBox>
#include <QApplication>
#include <QStyle>
#include <QFileInfo>
#include <QTimer>
FileManagerWindow::FileManagerWindow(QWidget *parent)
: QMainWindow(parent) {
// 创建文件系统模型
m_fileSystemModel = new QFileSystemModel(this);
m_fileSystemModel->setRootPath(""); // 设置为空以显示所有驱动器
m_fileSystemModel->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot);
// 创建过滤代理
m_filterProxy = new CustomFilterProxyModel(this);
m_filterProxy->setSourceModel(m_fileSystemModel);
setupUI();
createMenuBar();
createToolBar();
createStatusBar();
// 设置初始目录
QString homePath = QDir::homePath();
navigateToPath(homePath);
setWindowTitle("文件管理器");
resize(1200, 700);
}
FileManagerWindow::~FileManagerWindow() {
}
void FileManagerWindow::setupUI() {
// 左侧目录树
m_treeView = new QTreeView;
m_treeView->setModel(m_fileSystemModel);
m_treeView->setRootIndex(m_fileSystemModel->index(""));
// 只显示第一列(文件名)
for (int i = 1; i < m_fileSystemModel->columnCount(); ++i) {
m_treeView->hideColumn(i);
}
m_treeView->setHeaderHidden(true);
m_treeView->setAnimated(true);
m_treeView->setIndentation(20);
m_treeView->setSortingEnabled(true);
m_treeView->setEditTriggers(QAbstractItemView::NoEditTriggers);
// 右侧文件列表
m_tableView = new QTableView;
m_tableView->setModel(m_filterProxy);
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_tableView->setSortingEnabled(true);
m_tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_tableView->setAlternatingRowColors(true);
// 设置表头
m_tableView->horizontalHeader()->setStretchLastSection(true);
m_tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
m_tableView->verticalHeader()->hide();
// 设置自定义委托
m_tableView->setItemDelegateForColumn(0, new FileInfoDelegate(this));
// 分割器
m_splitter = new QSplitter(Qt::Horizontal);
m_splitter->addWidget(m_treeView);
m_splitter->addWidget(m_tableView);
m_splitter->setStretchFactor(0, 1); // 左侧占1份
m_splitter->setStretchFactor(1, 3); // 右侧占3份
setCentralWidget(m_splitter);
// 连接信号
connect(m_treeView, &QTreeView::clicked,
this, &FileManagerWindow::onTreeClicked);
connect(m_tableView, &QTableView::doubleClicked,
this, &FileManagerWindow::onTableDoubleClicked);
connect(m_fileSystemModel, &QFileSystemModel::directoryLoaded,
this, &FileManagerWindow::updateStatusBar);
}
void FileManagerWindow::createMenuBar() {
QMenuBar *menuBar = this->menuBar();
// 文件菜单
QMenu *fileMenu = menuBar->addMenu("文件(&F)");
QAction *openAction = fileMenu->addAction("打开");
openAction->setShortcut(QKeySequence::Open);
connect(openAction, &QAction::triggered, [this]() {
QModelIndexList selected = m_tableView->selectionModel()->selectedRows();
if (!selected.isEmpty()) {
onTableDoubleClicked(selected.first());
}
});
fileMenu->addSeparator();
QAction *exitAction = fileMenu->addAction("退出");
exitAction->setShortcut(QKeySequence::Quit);
connect(exitAction, &QAction::triggered, this, &QMainWindow::close);
// 查看菜单
QMenu *viewMenu = menuBar->addMenu("查看(&V)");
QAction *refreshAction = viewMenu->addAction("刷新");
refreshAction->setShortcut(QKeySequence::Refresh);
connect(refreshAction, &QAction::triggered,
this, &FileManagerWindow::onRefreshButtonClicked);
// 帮助菜单
QMenu *helpMenu = menuBar->addMenu("帮助(&H)");
QAction *aboutAction = helpMenu->addAction("关于");
connect(aboutAction, &QAction::triggered, [this]() {
QMessageBox::about(this, "关于",
"Qt文件管理器\n\n"
"一个使用Qt Model/View架构实现的文件浏览器\n"
"演示了QFileSystemModel的完整应用");
});
}
void FileManagerWindow::createToolBar() {
QToolBar *toolbar = addToolBar("主工具栏");
toolbar->setMovable(false);
// 返回上一级按钮
QPushButton *upButton = new QPushButton("↑ 上一级");
upButton->setIcon(style()->standardIcon(QStyle::SP_ArrowUp));
toolbar->addWidget(upButton);
connect(upButton, &QPushButton::clicked,
this, &FileManagerWindow::onUpButtonClicked);
toolbar->addSeparator();
// 地址栏
toolbar->addWidget(new QLabel(" 地址: "));
m_addressBar = new QLineEdit;
m_addressBar->setMinimumWidth(300);
toolbar->addWidget(m_addressBar);
connect(m_addressBar, &QLineEdit::returnPressed,
this, &FileManagerWindow::onAddressBarReturnPressed);
toolbar->addSeparator();
// 搜索框
toolbar->addWidget(new QLabel(" 搜索: "));
m_searchBox = new QLineEdit;
m_searchBox->setPlaceholderText("输入文件名...");
m_searchBox->setMinimumWidth(200);
toolbar->addWidget(m_searchBox);
connect(m_searchBox, &QLineEdit::textChanged,
this, &FileManagerWindow::onSearchTextChanged);
// 文件类型过滤
toolbar->addWidget(new QLabel(" 类型: "));
m_fileTypeCombo = new QComboBox;
m_fileTypeCombo->addItems({
"全部文件",
"文本文件 (*.txt)",
"图片文件 (*.jpg, *.png)",
"文档文件 (*.pdf, *.doc)",
"视频文件 (*.mp4, *.avi)"
});
toolbar->addWidget(m_fileTypeCombo);
connect(m_fileTypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &FileManagerWindow::onFileTypeChanged);
toolbar->addSeparator();
// 刷新按钮
QPushButton *refreshButton = new QPushButton("刷新");
refreshButton->setIcon(style()->standardIcon(QStyle::SP_BrowserReload));
toolbar->addWidget(refreshButton);
connect(refreshButton, &QPushButton::clicked,
this, &FileManagerWindow::onRefreshButtonClicked);
}
void FileManagerWindow::createStatusBar() {
m_statusLabel = new QLabel("就绪");
m_fileCountLabel = new QLabel;
statusBar()->addWidget(m_statusLabel, 1);
statusBar()->addPermanentWidget(m_fileCountLabel);
updateStatusBar();
}
void FileManagerWindow::onTreeClicked(const QModelIndex &index) {
if (!index.isValid()) {
return;
}
// 检查是否是文件夹
if (m_fileSystemModel->isDir(index)) {
QString path = m_fileSystemModel->filePath(index);
navigateToPath(path);
}
}
void FileManagerWindow::onTableDoubleClicked(const QModelIndex &proxyIndex) {
if (!proxyIndex.isValid()) {
return;
}
// 映射到源模型
QModelIndex sourceIndex = m_filterProxy->mapToSource(proxyIndex);
QString filePath = m_fileSystemModel->filePath(sourceIndex);
QFileInfo fileInfo(filePath);
if (fileInfo.isDir()) {
// 文件夹:导航到该目录
navigateToPath(filePath);
} else {
// 文件:使用系统默认程序打开
if (!QDesktopServices::openUrl(QUrl::fromLocalFile(filePath))) {
QMessageBox::warning(this, "错误",
"无法打开文件:" + fileInfo.fileName());
}
}
}
void FileManagerWindow::onSearchTextChanged(const QString &text) {
m_filterProxy->setSearchPattern(text);
updateStatusBar();
}
void FileManagerWindow::onFileTypeChanged(int index) {
QString extension;
switch (index) {
case 1: extension = "txt"; break;
case 2: extension = "jpg"; break; // 简化处理
case 3: extension = "pdf"; break;
case 4: extension = "mp4"; break;
default: extension = ""; break;
}
m_filterProxy->setFileTypeFilter(extension);
updateStatusBar();
}
void FileManagerWindow::onUpButtonClicked() {
QDir currentDir(m_currentPath);
if (currentDir.cdUp()) {
navigateToPath(currentDir.absolutePath());
}
}
void FileManagerWindow::onRefreshButtonClicked() {
// 刷新当前目录
QModelIndex index = m_fileSystemModel->index(m_currentPath);
m_fileSystemModel->fetchMore(index);
updateStatusBar();
}
void FileManagerWindow::onAddressBarReturnPressed() {
QString path = m_addressBar->text();
QFileInfo fileInfo(path);
if (fileInfo.exists() && fileInfo.isDir()) {
navigateToPath(path);
} else {
QMessageBox::warning(this, "错误", "路径不存在:" + path);
m_addressBar->setText(m_currentPath);
}
}
void FileManagerWindow::navigateToPath(const QString &path) {
m_currentPath = QDir::toNativeSeparators(path);
// 更新地址栏
updateAddressBar(m_currentPath);
// 更新表格视图
QModelIndex sourceIndex = m_fileSystemModel->index(m_currentPath);
QModelIndex proxyIndex = m_filterProxy->mapFromSource(sourceIndex);
m_tableView->setRootIndex(proxyIndex);
// 更新树视图选中项
m_treeView->setCurrentIndex(sourceIndex);
m_treeView->scrollTo(sourceIndex);
// 更新状态栏
updateStatusBar();
}
void FileManagerWindow::updateAddressBar(const QString &path) {
m_addressBar->setText(path);
}
void FileManagerWindow::updateStatusBar() {
// 延迟更新以等待模型加载
QTimer::singleShot(100, this, [this]() {
int totalFiles = 0;
int totalDirs = 0;
int visibleFiles = 0;
qint64 totalSize = 0;
// 统计当前目录的文件
QModelIndex rootIndex = m_tableView->rootIndex();
int rowCount = m_filterProxy->rowCount(rootIndex);
for (int i = 0; i < rowCount; ++i) {
QModelIndex proxyIndex = m_filterProxy->index(i, 0, rootIndex);
QModelIndex sourceIndex = m_filterProxy->mapToSource(proxyIndex);
if (m_fileSystemModel->isDir(sourceIndex)) {
totalDirs++;
} else {
totalFiles++;
totalSize += m_fileSystemModel->size(sourceIndex);
}
}
visibleFiles = totalFiles + totalDirs;
// 更新状态栏
QString status = QString("当前目录: %1").arg(m_currentPath);
m_statusLabel->setText(status);
QString fileCount = QString("%1 个项目 (%2 个文件夹, %3 个文件) | 总大小: %4")
.arg(visibleFiles)
.arg(totalDirs)
.arg(totalFiles)
.arg(formatFileSize(totalSize));
m_fileCountLabel->setText(fileCount);
});
}
QString FileManagerWindow::formatFileSize(qint64 size) const {
const qint64 KB = 1024;
const qint64 MB = KB * 1024;
const qint64 GB = MB * 1024;
if (size >= GB) {
return QString::number(size / (double)GB, 'f', 2) + " GB";
} else if (size >= MB) {
return QString::number(size / (double)MB, 'f', 2) + " MB";
} else if (size >= KB) {
return QString::number(size / (double)KB, 'f', 2) + " KB";
} else {
return QString::number(size) + " B";
}
}
4. main.cpp
#include "filemanagerwindow.h"
#include <QApplication>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// 设置应用信息
app.setApplicationName("Qt文件管理器");
app.setOrganizationName("Qt教程");
FileManagerWindow window;
window.show();
return app.exec();
}
12.2.4 完整源码
项目文件结构:
filemanager/
├── filemanager.pro
├── main.cpp
├── filemanagerwindow.h
├── filemanagerwindow.cpp
├── customfilterproxymodel.h
├── customfilterproxymodel.cpp
├── fileinfodelegate.h
└── fileinfodelegate.cpp
filemanager.pro:
QT += core gui widgets
TARGET = FileManager
TEMPLATE = app
CONFIG += c++11
SOURCES += \
main.cpp \
filemanagerwindow.cpp \
customfilterproxymodel.cpp \
fileinfodelegate.cpp
HEADERS += \
filemanagerwindow.h \
customfilterproxymodel.h \
fileinfodelegate.h
12.2.5 功能演示
使用步骤:
-
编译运行:
qmake filemanager.pro make ./FileManager -
基本操作:
- 左侧树形视图点击文件夹导航
- 右侧显示当前文件夹内容
- 双击文件夹进入,双击文件打开
-
搜索和过滤:
- 在搜索框输入文件名实时过滤
- 下拉选择文件类型过滤
- 状态栏显示过滤结果统计
-
导航功能:
- 点击"上一级"按钮返回父目录
- 在地址栏输入路径并回车快速跳转
- 点击刷新按钮重新加载当前目录
-
快捷键:
Ctrl+O: 打开选中文件F5: 刷新Ctrl+Q: 退出
界面布局(文本描述):
+------------------------------------------------------------------+
| 文件(F) 查看(V) 帮助(H) |
+------------------------------------------------------------------+
| ↑上一级 | 地址: C:\Users\... | 搜索: [____] | 类型: [下拉▼] | 刷新 |
+------------------------------------------------------------------+
| | |
| 📁 C:\ | 名称 大小 类型 修改时间 |
| ├─📁 Program Files| ------------------------------------------ |
| ├─📁 Users | 📄 file1.txt 1.2 KB 文本 2024-01-20 |
| │ ├─📁 Public | 📄 file2.pdf 523 KB PDF 2024-01-21 |
| │ └─📁 UserName | 📁 folder1 -- 文件夹 2024-01-19 |
| └─📁 Windows | 📄 image.jpg 2.3 MB 图片 2024-01-18 |
| | |
| | |
+------------------------------------------------------------------+
| 当前目录: C:\Users\... | 4个项目 (1个文件夹, 3个文件) | 3.9 MB |
+------------------------------------------------------------------+
本节小结:
✅ 完整的文件管理器 - 类似Windows资源管理器的应用
✅ QFileSystemModel - 使用Qt内置的文件系统模型
✅ 双视图联动 - 树形+表格视图同步
✅ 过滤代理 - 实时搜索和文件类型过滤
✅ 自定义委托 - 美化文件列表显示
关键技术点:
- QFileSystemModel提供了完整的文件系统访问
- 代理模型实现搜索和过滤而不修改源模型
- 双视图共享同一模型,通过索引映射实现联动
- 使用QDesktopServices打开文件
- 格式化显示文件大小、修改时间等信息
可扩展功能:
- 文件复制/粘贴/删除操作
- 拖放文件到其他文件夹
- 文件预览功能
- 收藏夹功能
- 多标签页浏览
- 右键上下文菜单
- 文件属性查看
- 压缩/解压缩功能
与市面产品对比:
-
✅ 基础功能覆盖了Windows资源管理器的核心特性
-
✅ 干净简洁的界面设计
-
✅ 高性能(Qt原生实现)
-
📝 可扩展为完整的文件管理软件
-
项目需求分析
-
架构设计
-
核心功能实现
-
完整源码
-
功能演示
12.3 项目三:通讯录管理系统
一个功能完善的联系人管理应用,支持分组管理、联系人增删改查、搜索过滤和数据持久化。
12.3.1 项目需求分析
核心功能:
-
联系人增删改查
- 添加新联系人(姓名、电话、邮箱、头像等)
- 编辑联系人信息
- 删除联系人
- 查看联系人详细信息
-
分组管理(树形结构)
- 创建分组(家人、朋友、同事等)
- 联系人归属于分组
- 树形视图显示分组和联系人
- 拖放联系人到其他分组
-
搜索和过滤
- 按姓名搜索
- 按电话号码搜索
- 按分组过滤
- 实时搜索结果显示
-
数据持久化
- 保存到JSON文件
- 启动时自动加载
- 支持导入/导出
-
附加功能
- 头像显示和上传
- 联系人统计
- 最近联系记录
12.3.2 架构设计
类设计:
ContactManagerWindow (主窗口)
├── ContactTreeModel (树形模型) - 分组+联系人
├── QTreeView (左侧分组树)
├── QListView (右侧联系人列表)
├── ContactDelegate (联系人委托) - 头像+多行显示
├── ContactDialog (联系人编辑对话框)
└── DataManager (数据管理) - JSON持久化
数据结构:
TreeItem {
ItemType type; // Group 或 Contact
QString name;
QVariant data; // 具体数据
QList<TreeItem*> children;
}
Contact {
QString name;
QString phone;
QString email;
QString address;
QPixmap avatar;
QString notes;
}
数据流:
JSON文件 → DataManager → ContactTreeModel → TreeView/ListView
↓
ContactDelegate
12.3.3 核心功能实现
由于代码量较大,这里提供核心部分的完整实现。
1. 数据结构定义
contact.h:
#ifndef CONTACT_H
#define CONTACT_H
#include <QString>
#include <QPixmap>
#include <QJsonObject>
class Contact {
public:
Contact();
Contact(const QString &name, const QString &phone);
// 基本信息
QString name() const { return m_name; }
void setName(const QString &name) { m_name = name; }
QString phone() const { return m_phone; }
void setPhone(const QString &phone) { m_phone = phone; }
QString email() const { return m_email; }
void setEmail(const QString &email) { m_email = email; }
QString address() const { return m_address; }
void setAddress(const QString &address) { m_address = address; }
QString notes() const { return m_notes; }
void setNotes(const QString ¬es) { m_notes = notes; }
QPixmap avatar() const { return m_avatar; }
void setAvatar(const QPixmap &avatar) { m_avatar = avatar; }
QString group() const { return m_group; }
void setGroup(const QString &group) { m_group = group; }
// 序列化
QJsonObject toJson() const;
static Contact fromJson(const QJsonObject &json);
private:
QString m_name;
QString m_phone;
QString m_email;
QString m_address;
QString m_notes;
QString m_group;
QPixmap m_avatar;
};
#endif // CONTACT_H
contact.cpp:
#include "contact.h"
#include <QBuffer>
Contact::Contact() {}
Contact::Contact(const QString &name, const QString &phone)
: m_name(name), m_phone(phone) {}
QJsonObject Contact::toJson() const {
QJsonObject json;
json["name"] = m_name;
json["phone"] = m_phone;
json["email"] = m_email;
json["address"] = m_address;
json["notes"] = m_notes;
json["group"] = m_group;
// 保存头像为Base64
if (!m_avatar.isNull()) {
QByteArray byteArray;
QBuffer buffer(&byteArray);
buffer.open(QIODevice::WriteOnly);
m_avatar.save(&buffer, "PNG");
json["avatar"] = QString(byteArray.toBase64());
}
return json;
}
Contact Contact::fromJson(const QJsonObject &json) {
Contact contact;
contact.m_name = json["name"].toString();
contact.m_phone = json["phone"].toString();
contact.m_email = json["email"].toString();
contact.m_address = json["address"].toString();
contact.m_notes = json["notes"].toString();
contact.m_group = json["group"].toString();
// 加载头像
if (json.contains("avatar")) {
QByteArray byteArray = QByteArray::fromBase64(
json["avatar"].toString().toUtf8());
contact.m_avatar.loadFromData(byteArray, "PNG");
}
return contact;
}
2. 自定义树形模型
contacttreemodel.h:
#ifndef CONTACTTREEMODEL_H
#define CONTACTTREEMODEL_H
#include <QAbstractItemModel>
#include <QList>
#include "contact.h"
enum class ItemType {
Group,
Contact
};
class TreeItem {
public:
explicit TreeItem(ItemType type, const QString &name,
TreeItem *parent = nullptr);
~TreeItem();
void appendChild(TreeItem *child);
void removeChild(TreeItem *child);
TreeItem *child(int row);
int childCount() const;
int row() const;
TreeItem *parent();
ItemType type() const { return m_type; }
QString name() const { return m_name; }
void setName(const QString &name) { m_name = name; }
Contact* contact() { return m_contact; }
void setContact(Contact *contact) { m_contact = contact; }
private:
ItemType m_type;
QString m_name;
Contact *m_contact;
QList<TreeItem*> m_children;
TreeItem *m_parent;
};
class ContactTreeModel : public QAbstractItemModel {
Q_OBJECT
public:
explicit ContactTreeModel(QObject *parent = nullptr);
~ContactTreeModel();
// 基本接口
QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
// 数据操作
void addGroup(const QString &groupName);
void addContact(const Contact &contact, const QString &groupName);
void removeContact(const QModelIndex &index);
void updateContact(const QModelIndex &index, const Contact &contact);
// 查询
QList<Contact*> allContacts() const;
Contact* getContact(const QModelIndex &index) const;
QString getGroupName(const QModelIndex &index) const;
// 数据持久化
bool saveToFile(const QString &filename);
bool loadFromFile(const QString &filename);
private:
TreeItem *m_rootItem;
TreeItem* getItem(const QModelIndex &index) const;
TreeItem* findGroup(const QString &groupName) const;
};
#endif // CONTACTTREEMODEL_H
contacttreemodel.cpp(核心部分):
#include "contacttreemodel.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
// TreeItem 实现
TreeItem::TreeItem(ItemType type, const QString &name, TreeItem *parent)
: m_type(type), m_name(name), m_contact(nullptr), m_parent(parent) {
}
TreeItem::~TreeItem() {
qDeleteAll(m_children);
delete m_contact;
}
void TreeItem::appendChild(TreeItem *child) {
m_children.append(child);
}
void TreeItem::removeChild(TreeItem *child) {
m_children.removeOne(child);
delete child;
}
TreeItem* TreeItem::child(int row) {
return m_children.value(row);
}
int TreeItem::childCount() const {
return m_children.count();
}
int TreeItem::row() const {
if (m_parent) {
return m_parent->m_children.indexOf(const_cast<TreeItem*>(this));
}
return 0;
}
TreeItem* TreeItem::parent() {
return m_parent;
}
// ContactTreeModel 实现
ContactTreeModel::ContactTreeModel(QObject *parent)
: QAbstractItemModel(parent) {
m_rootItem = new TreeItem(ItemType::Group, "Root");
// 创建默认分组
addGroup("家人");
addGroup("朋友");
addGroup("同事");
addGroup("其他");
}
ContactTreeModel::~ContactTreeModel() {
delete m_rootItem;
}
QModelIndex ContactTreeModel::index(int row, int column,
const QModelIndex &parent) const {
if (!hasIndex(row, column, parent)) {
return QModelIndex();
}
TreeItem *parentItem = getItem(parent);
TreeItem *childItem = parentItem->child(row);
if (childItem) {
return createIndex(row, column, childItem);
}
return QModelIndex();
}
QModelIndex ContactTreeModel::parent(const QModelIndex &child) const {
if (!child.isValid()) {
return QModelIndex();
}
TreeItem *childItem = getItem(child);
TreeItem *parentItem = childItem->parent();
if (parentItem == m_rootItem) {
return QModelIndex();
}
return createIndex(parentItem->row(), 0, parentItem);
}
int ContactTreeModel::rowCount(const QModelIndex &parent) const {
TreeItem *parentItem = getItem(parent);
return parentItem->childCount();
}
int ContactTreeModel::columnCount(const QModelIndex &parent) const {
Q_UNUSED(parent);
return 1;
}
QVariant ContactTreeModel::data(const QModelIndex &index, int role) const {
if (!index.isValid()) {
return QVariant();
}
TreeItem *item = getItem(index);
if (role == Qt::DisplayRole) {
if (item->type() == ItemType::Group) {
return QString("%1 (%2)").arg(item->name())
.arg(item->childCount());
} else {
return item->contact()->name();
}
} else if (role == Qt::UserRole) {
return static_cast<int>(item->type());
} else if (role == Qt::UserRole + 1) {
if (item->type() == ItemType::Contact) {
return QVariant::fromValue(item->contact());
}
}
return QVariant();
}
Qt::ItemFlags ContactTreeModel::flags(const QModelIndex &index) const {
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
void ContactTreeModel::addGroup(const QString &groupName) {
beginInsertRows(QModelIndex(), m_rootItem->childCount(),
m_rootItem->childCount());
TreeItem *groupItem = new TreeItem(ItemType::Group, groupName, m_rootItem);
m_rootItem->appendChild(groupItem);
endInsertRows();
}
void ContactTreeModel::addContact(const Contact &contact,
const QString &groupName) {
TreeItem *groupItem = findGroup(groupName);
if (!groupItem) {
addGroup(groupName);
groupItem = findGroup(groupName);
}
QModelIndex groupIndex = createIndex(groupItem->row(), 0, groupItem);
beginInsertRows(groupIndex, groupItem->childCount(),
groupItem->childCount());
TreeItem *contactItem = new TreeItem(ItemType::Contact,
contact.name(), groupItem);
contactItem->setContact(new Contact(contact));
groupItem->appendChild(contactItem);
endInsertRows();
}
void ContactTreeModel::removeContact(const QModelIndex &index) {
if (!index.isValid()) {
return;
}
TreeItem *item = getItem(index);
if (item->type() != ItemType::Contact) {
return;
}
TreeItem *parent = item->parent();
QModelIndex parentIndex = createIndex(parent->row(), 0, parent);
beginRemoveRows(parentIndex, item->row(), item->row());
parent->removeChild(item);
endRemoveRows();
}
TreeItem* ContactTreeModel::getItem(const QModelIndex &index) const {
if (index.isValid()) {
TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
if (item) {
return item;
}
}
return m_rootItem;
}
TreeItem* ContactTreeModel::findGroup(const QString &groupName) const {
for (int i = 0; i < m_rootItem->childCount(); ++i) {
TreeItem *item = m_rootItem->child(i);
if (item->name() == groupName) {
return item;
}
}
return nullptr;
}
void ContactTreeModel::updateContact(const QModelIndex &index, const Contact &contact) {
if (!index.isValid()) {
return;
}
TreeItem *item = getItem(index);
if (item->type() != ItemType::Contact) {
return;
}
// 更新联系人数据
Contact *existingContact = item->contact();
if (existingContact) {
existingContact->setName(contact.name());
existingContact->setPhone(contact.phone());
existingContact->setEmail(contact.email());
existingContact->setAddress(contact.address());
existingContact->setNotes(contact.notes());
existingContact->setAvatar(contact.avatar());
existingContact->setGroup(contact.group());
}
// 更新节点名称
item->setName(contact.name());
// 通知视图数据已更新
emit dataChanged(index, index);
}
QList<Contact*> ContactTreeModel::allContacts() const {
QList<Contact*> contacts;
// 遍历所有分组
for (int i = 0; i < m_rootItem->childCount(); ++i) {
TreeItem *groupItem = m_rootItem->child(i);
// 遍历分组中的联系人
for (int j = 0; j < groupItem->childCount(); ++j) {
TreeItem *contactItem = groupItem->child(j);
if (contactItem->contact()) {
contacts.append(contactItem->contact());
}
}
}
return contacts;
}
Contact* ContactTreeModel::getContact(const QModelIndex &index) const {
if (!index.isValid()) {
return nullptr;
}
TreeItem *item = getItem(index);
if (item->type() != ItemType::Contact) {
return nullptr;
}
return item->contact();
}
QString ContactTreeModel::getGroupName(const QModelIndex &index) const {
if (!index.isValid()) {
return QString();
}
TreeItem *item = getItem(index);
if (item->type() == ItemType::Group) {
return item->name();
} else if (item->type() == ItemType::Contact) {
// 返回父节点(分组)的名称
TreeItem *parent = item->parent();
if (parent && parent != m_rootItem) {
return parent->name();
}
}
return QString();
}
bool ContactTreeModel::saveToFile(const QString &filename) {
QJsonObject root;
QJsonArray groupsArray;
// 遍历所有分组
for (int i = 0; i < m_rootItem->childCount(); ++i) {
TreeItem *groupItem = m_rootItem->child(i);
QJsonObject groupObj;
groupObj["name"] = groupItem->name();
QJsonArray contactsArray;
for (int j = 0; j < groupItem->childCount(); ++j) {
TreeItem *contactItem = groupItem->child(j);
Contact *contact = contactItem->contact();
contactsArray.append(contact->toJson());
}
groupObj["contacts"] = contactsArray;
groupsArray.append(groupObj);
}
root["groups"] = groupsArray;
QJsonDocument doc(root);
QFile file(filename);
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
file.write(doc.toJson());
file.close();
return true;
}
bool ContactTreeModel::loadFromFile(const QString &filename) {
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
return false;
}
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
file.close();
beginResetModel();
// 清空现有数据
delete m_rootItem;
m_rootItem = new TreeItem(ItemType::Group, "Root");
// 加载数据
QJsonArray groupsArray = doc.object()["groups"].toArray();
for (const QJsonValue &groupValue : groupsArray) {
QJsonObject groupObj = groupValue.toObject();
QString groupName = groupObj["name"].toString();
TreeItem *groupItem = new TreeItem(ItemType::Group,
groupName, m_rootItem);
m_rootItem->appendChild(groupItem);
QJsonArray contactsArray = groupObj["contacts"].toArray();
for (const QJsonValue &contactValue : contactsArray) {
Contact contact = Contact::fromJson(contactValue.toObject());
TreeItem *contactItem = new TreeItem(ItemType::Contact,
contact.name(), groupItem);
contactItem->setContact(new Contact(contact));
groupItem->appendChild(contactItem);
}
}
endResetModel();
return true;
}
3. 联系人委托
contactdelegate.h:
#ifndef CONTACTDELEGATE_H
#define CONTACTDELEGATE_H
#include <QStyledItemDelegate>
class ContactDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
explicit ContactDelegate(QObject *parent = nullptr);
void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
#endif // CONTACTDELEGATE_H
contactdelegate.cpp:
#include "contactdelegate.h"
#include "contact.h"
#include <QPainter>
#include <QPainterPath>
ContactDelegate::ContactDelegate(QObject *parent)
: QStyledItemDelegate(parent) {
}
void ContactDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const {
Contact *contact = index.data(Qt::UserRole + 1).value<Contact*>();
if (!contact) {
QStyledItemDelegate::paint(painter, option, index);
return;
}
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
// 绘制背景
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
painter->setPen(option.palette.highlightedText().color());
} else {
if (option.state & QStyle::State_MouseOver) {
painter->fillRect(option.rect,
option.palette.color(QPalette::AlternateBase));
}
painter->setPen(option.palette.text().color());
}
// 绘制头像(圆形)
int avatarSize = 50;
int margin = 10;
QRect avatarRect(option.rect.left() + margin,
option.rect.top() + (option.rect.height() - avatarSize) / 2,
avatarSize, avatarSize);
QPixmap avatar = contact->avatar();
if (avatar.isNull()) {
// 默认头像
painter->setBrush(QColor("#CCCCCC"));
painter->drawEllipse(avatarRect);
// 绘制默认图标(首字母)
painter->setFont(QFont("Arial", 20, QFont::Bold));
painter->drawText(avatarRect, Qt::AlignCenter,
contact->name().left(1).toUpper());
} else {
// 圆形裁剪
QPainterPath path;
path.addEllipse(avatarRect);
painter->setClipPath(path);
painter->drawPixmap(avatarRect,
avatar.scaled(avatarSize, avatarSize,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation));
painter->setClipping(false);
}
// 绘制文本信息
int textLeft = avatarRect.right() + margin;
int textWidth = option.rect.width() - textLeft - margin;
QFont nameFont = option.font;
nameFont.setPointSize(12);
nameFont.setBold(true);
painter->setFont(nameFont);
QRect nameRect(textLeft, option.rect.top() + 5,
textWidth, 20);
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignVCenter,
contact->name());
// 电话
QFont detailFont = option.font;
detailFont.setPointSize(10);
painter->setFont(detailFont);
QRect phoneRect(textLeft, nameRect.bottom() + 2,
textWidth, 18);
painter->drawText(phoneRect, Qt::AlignLeft | Qt::AlignVCenter,
"📞 " + contact->phone());
// 邮箱
if (!contact->email().isEmpty()) {
QRect emailRect(textLeft, phoneRect.bottom() + 2,
textWidth, 18);
painter->drawText(emailRect, Qt::AlignLeft | Qt::AlignVCenter,
"✉ " + contact->email());
}
painter->restore();
}
QSize ContactDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const {
Q_UNUSED(option);
Q_UNUSED(index);
return QSize(200, 70); // 固定高度
}
4. 主窗口(简化版)
contactmanagerwindow.h:
#ifndef CONTACTMANAGERWINDOW_H
#define CONTACTMANAGERWINDOW_H
#include <QMainWindow>
#include <QTreeView>
#include <QListView>
#include <QLineEdit>
#include "contacttreemodel.h"
class ContactManagerWindow : public QMainWindow {
Q_OBJECT
public:
explicit ContactManagerWindow(QWidget *parent = nullptr);
~ContactManagerWindow();
private slots:
void onAddContact();
void onEditContact();
void onDeleteContact();
void onSearchTextChanged(const QString &text);
void onTreeClicked(const QModelIndex &index);
private:
void setupUI();
void createMenuBar();
void createToolBar();
void loadData();
void saveData();
ContactTreeModel *m_model;
QTreeView *m_treeView;
QListView *m_listView;
QLineEdit *m_searchBox;
QString m_dataFile;
};
#endif // CONTACTMANAGERWINDOW_H
12.3.4 完整源码
项目文件结构:
contactmanager/
├── contactmanager.pro
├── main.cpp
├── contact.h / .cpp
├── contacttreemodel.h / .cpp
├── contactdelegate.h / .cpp
├── contactmanagerwindow.h / .cpp
└── contactdialog.h / .cpp (联系人编辑对话框)
contactmanager.pro:
QT += core gui widgets
TARGET = ContactManager
TEMPLATE = app
CONFIG += c++11
SOURCES += \
main.cpp \
contact.cpp \
contacttreemodel.cpp \
contactdelegate.cpp \
contactmanagerwindow.cpp \
contactdialog.cpp
HEADERS += \
contact.h \
contacttreemodel.h \
contactdelegate.h \
contactmanagerwindow.h \
contactdialog.h
main.cpp:
#include "contactmanagerwindow.h"
#include <QApplication>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
app.setApplicationName("通讯录管理系统");
app.setOrganizationName("Qt教程");
ContactManagerWindow window;
window.show();
return app.exec();
}
contactdialog.h:
#ifndef CONTACTDIALOG_H
#define CONTACTDIALOG_H
#include <QDialog>
#include <QLineEdit>
#include <QTextEdit>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include "contact.h"
class ContactDialog : public QDialog {
Q_OBJECT
public:
explicit ContactDialog(QWidget *parent = nullptr);
explicit ContactDialog(const Contact &contact, QWidget *parent = nullptr);
Contact getContact() const;
QString getGroup() const;
private slots:
void onSelectAvatar();
void onAccept();
private:
void setupUI();
void loadContact(const Contact &contact);
QLineEdit *m_nameEdit;
QLineEdit *m_phoneEdit;
QLineEdit *m_emailEdit;
QLineEdit *m_addressEdit;
QTextEdit *m_notesEdit;
QComboBox *m_groupCombo;
QLabel *m_avatarLabel;
QPixmap m_avatar;
QPushButton *m_okButton;
QPushButton *m_cancelButton;
};
#endif // CONTACTDIALOG_H
contactdialog.cpp:
#include "contactdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QGroupBox>
#include <QFileDialog>
#include <QMessageBox>
ContactDialog::ContactDialog(QWidget *parent)
: QDialog(parent) {
setupUI();
setWindowTitle("添加联系人");
}
ContactDialog::ContactDialog(const Contact &contact, QWidget *parent)
: QDialog(parent) {
setupUI();
setWindowTitle("编辑联系人");
loadContact(contact);
}
void ContactDialog::setupUI() {
setMinimumSize(400, 500);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// 头像区域
QHBoxLayout *avatarLayout = new QHBoxLayout;
m_avatarLabel = new QLabel;
m_avatarLabel->setFixedSize(80, 80);
m_avatarLabel->setStyleSheet("QLabel { background-color: #CCCCCC; border-radius: 40px; }");
m_avatarLabel->setAlignment(Qt::AlignCenter);
m_avatarLabel->setText("点击\n选择头像");
m_avatarLabel->setCursor(Qt::PointingHandCursor);
m_avatarLabel->installEventFilter(this);
QPushButton *avatarButton = new QPushButton("选择头像");
connect(avatarButton, &QPushButton::clicked, this, &ContactDialog::onSelectAvatar);
avatarLayout->addStretch();
avatarLayout->addWidget(m_avatarLabel);
avatarLayout->addWidget(avatarButton);
avatarLayout->addStretch();
mainLayout->addLayout(avatarLayout);
// 基本信息表单
QGroupBox *infoGroup = new QGroupBox("基本信息");
QFormLayout *formLayout = new QFormLayout(infoGroup);
m_nameEdit = new QLineEdit;
m_nameEdit->setPlaceholderText("请输入姓名");
formLayout->addRow("姓名*:", m_nameEdit);
m_phoneEdit = new QLineEdit;
m_phoneEdit->setPlaceholderText("请输入电话号码");
formLayout->addRow("电话*:", m_phoneEdit);
m_emailEdit = new QLineEdit;
m_emailEdit->setPlaceholderText("请输入邮箱地址");
formLayout->addRow("邮箱:", m_emailEdit);
m_addressEdit = new QLineEdit;
m_addressEdit->setPlaceholderText("请输入地址");
formLayout->addRow("地址:", m_addressEdit);
m_groupCombo = new QComboBox;
m_groupCombo->addItems({"家人", "朋友", "同事", "其他"});
formLayout->addRow("分组:", m_groupCombo);
mainLayout->addWidget(infoGroup);
// 备注
QGroupBox *notesGroup = new QGroupBox("备注");
QVBoxLayout *notesLayout = new QVBoxLayout(notesGroup);
m_notesEdit = new QTextEdit;
m_notesEdit->setPlaceholderText("请输入备注信息...");
m_notesEdit->setMaximumHeight(100);
notesLayout->addWidget(m_notesEdit);
mainLayout->addWidget(notesGroup);
// 按钮
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
m_cancelButton = new QPushButton("取消");
connect(m_cancelButton, &QPushButton::clicked, this, &QDialog::reject);
buttonLayout->addWidget(m_cancelButton);
m_okButton = new QPushButton("保存");
m_okButton->setDefault(true);
connect(m_okButton, &QPushButton::clicked, this, &ContactDialog::onAccept);
buttonLayout->addWidget(m_okButton);
mainLayout->addLayout(buttonLayout);
}
void ContactDialog::onSelectAvatar() {
QString filename = QFileDialog::getOpenFileName(
this, "选择头像", QString(),
"图片文件 (*.png *.jpg *.jpeg *.bmp)");
if (!filename.isEmpty()) {
m_avatar.load(filename);
if (!m_avatar.isNull()) {
QPixmap scaled = m_avatar.scaled(80, 80,
Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
m_avatarLabel->setPixmap(scaled);
m_avatarLabel->setText("");
}
}
}
void ContactDialog::onAccept() {
if (m_nameEdit->text().trimmed().isEmpty()) {
QMessageBox::warning(this, "提示", "请输入姓名!");
m_nameEdit->setFocus();
return;
}
if (m_phoneEdit->text().trimmed().isEmpty()) {
QMessageBox::warning(this, "提示", "请输入电话号码!");
m_phoneEdit->setFocus();
return;
}
accept();
}
void ContactDialog::loadContact(const Contact &contact) {
m_nameEdit->setText(contact.name());
m_phoneEdit->setText(contact.phone());
m_emailEdit->setText(contact.email());
m_addressEdit->setText(contact.address());
m_notesEdit->setText(contact.notes());
int groupIndex = m_groupCombo->findText(contact.group());
if (groupIndex >= 0) {
m_groupCombo->setCurrentIndex(groupIndex);
}
if (!contact.avatar().isNull()) {
m_avatar = contact.avatar();
QPixmap scaled = m_avatar.scaled(80, 80,
Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
m_avatarLabel->setPixmap(scaled);
m_avatarLabel->setText("");
}
}
Contact ContactDialog::getContact() const {
Contact contact;
contact.setName(m_nameEdit->text().trimmed());
contact.setPhone(m_phoneEdit->text().trimmed());
contact.setEmail(m_emailEdit->text().trimmed());
contact.setAddress(m_addressEdit->text().trimmed());
contact.setNotes(m_notesEdit->toPlainText());
contact.setGroup(m_groupCombo->currentText());
if (!m_avatar.isNull()) {
contact.setAvatar(m_avatar);
}
return contact;
}
QString ContactDialog::getGroup() const {
return m_groupCombo->currentText();
}
contactmanagerwindow.cpp:
#include "contactmanagerwindow.h"
#include "contactdelegate.h"
#include "contactdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QSplitter>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QAction>
#include <QMessageBox>
#include <QFileDialog>
#include <QStandardPaths>
#include <QSortFilterProxyModel>
ContactManagerWindow::ContactManagerWindow(QWidget *parent)
: QMainWindow(parent) {
setWindowTitle("通讯录管理系统");
setMinimumSize(800, 600);
// 初始化模型
m_model = new ContactTreeModel(this);
// 设置数据文件路径
QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(dataDir);
m_dataFile = dataDir + "/contacts.json";
setupUI();
createMenuBar();
createToolBar();
// 加载数据
loadData();
// 状态栏
statusBar()->showMessage("就绪");
}
ContactManagerWindow::~ContactManagerWindow() {
saveData();
}
void ContactManagerWindow::setupUI() {
QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
QHBoxLayout *mainLayout = new QHBoxLayout(centralWidget);
// 分割器
QSplitter *splitter = new QSplitter(Qt::Horizontal);
// 左侧:分组树
m_treeView = new QTreeView;
m_treeView->setModel(m_model);
m_treeView->setHeaderHidden(true);
m_treeView->setMinimumWidth(200);
m_treeView->expandAll();
connect(m_treeView, &QTreeView::clicked,
this, &ContactManagerWindow::onTreeClicked);
splitter->addWidget(m_treeView);
// 右侧:联系人列表
QWidget *rightPanel = new QWidget;
QVBoxLayout *rightLayout = new QVBoxLayout(rightPanel);
rightLayout->setContentsMargins(0, 0, 0, 0);
// 搜索框
QHBoxLayout *searchLayout = new QHBoxLayout;
QLabel *searchLabel = new QLabel("🔍 搜索:");
m_searchBox = new QLineEdit;
m_searchBox->setPlaceholderText("输入姓名或电话搜索...");
m_searchBox->setClearButtonEnabled(true);
connect(m_searchBox, &QLineEdit::textChanged,
this, &ContactManagerWindow::onSearchTextChanged);
searchLayout->addWidget(searchLabel);
searchLayout->addWidget(m_searchBox);
rightLayout->addLayout(searchLayout);
// 联系人列表视图
m_listView = new QListView;
m_listView->setModel(m_model);
m_listView->setItemDelegate(new ContactDelegate(this));
m_listView->setSpacing(5);
m_listView->setAlternatingRowColors(true);
connect(m_listView, &QListView::doubleClicked,
this, &ContactManagerWindow::onEditContact);
rightLayout->addWidget(m_listView);
splitter->addWidget(rightPanel);
splitter->setSizes({200, 600});
mainLayout->addWidget(splitter);
}
void ContactManagerWindow::createMenuBar() {
QMenu *fileMenu = menuBar()->addMenu("文件(&F)");
QAction *importAction = fileMenu->addAction("导入联系人...");
connect(importAction, &QAction::triggered, [this]() {
QString filename = QFileDialog::getOpenFileName(
this, "导入联系人", QString(), "JSON文件 (*.json)");
if (!filename.isEmpty()) {
m_model->loadFromFile(filename);
m_treeView->expandAll();
statusBar()->showMessage("导入成功", 3000);
}
});
QAction *exportAction = fileMenu->addAction("导出联系人...");
connect(exportAction, &QAction::triggered, [this]() {
QString filename = QFileDialog::getSaveFileName(
this, "导出联系人", "contacts.json", "JSON文件 (*.json)");
if (!filename.isEmpty()) {
m_model->saveToFile(filename);
statusBar()->showMessage("导出成功", 3000);
}
});
fileMenu->addSeparator();
QAction *exitAction = fileMenu->addAction("退出(&X)");
connect(exitAction, &QAction::triggered, this, &QMainWindow::close);
QMenu *editMenu = menuBar()->addMenu("编辑(&E)");
QAction *addAction = editMenu->addAction("添加联系人");
addAction->setShortcut(QKeySequence::New);
connect(addAction, &QAction::triggered, this, &ContactManagerWindow::onAddContact);
QAction *editAction = editMenu->addAction("编辑联系人");
connect(editAction, &QAction::triggered, this, &ContactManagerWindow::onEditContact);
QAction *deleteAction = editMenu->addAction("删除联系人");
deleteAction->setShortcut(QKeySequence::Delete);
connect(deleteAction, &QAction::triggered, this, &ContactManagerWindow::onDeleteContact);
QMenu *helpMenu = menuBar()->addMenu("帮助(&H)");
QAction *aboutAction = helpMenu->addAction("关于");
connect(aboutAction, &QAction::triggered, [this]() {
QMessageBox::about(this, "关于",
"通讯录管理系统 v1.0\n\n"
"Qt Model/View架构实战项目\n"
"支持分组管理、搜索过滤和数据持久化");
});
}
void ContactManagerWindow::createToolBar() {
QToolBar *toolBar = addToolBar("工具栏");
toolBar->setMovable(false);
QAction *addAction = toolBar->addAction("➕ 添加");
addAction->setToolTip("添加联系人");
connect(addAction, &QAction::triggered, this, &ContactManagerWindow::onAddContact);
QAction *editAction = toolBar->addAction("✏ 编辑");
editAction->setToolTip("编辑联系人");
connect(editAction, &QAction::triggered, this, &ContactManagerWindow::onEditContact);
QAction *deleteAction = toolBar->addAction("🗑 删除");
deleteAction->setToolTip("删除联系人");
connect(deleteAction, &QAction::triggered, this, &ContactManagerWindow::onDeleteContact);
}
void ContactManagerWindow::loadData() {
if (QFile::exists(m_dataFile)) {
m_model->loadFromFile(m_dataFile);
m_treeView->expandAll();
}
}
void ContactManagerWindow::saveData() {
m_model->saveToFile(m_dataFile);
}
void ContactManagerWindow::onAddContact() {
ContactDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
Contact contact = dialog.getContact();
QString group = dialog.getGroup();
m_model->addContact(contact, group);
m_treeView->expandAll();
statusBar()->showMessage("联系人已添加", 3000);
}
}
void ContactManagerWindow::onEditContact() {
QModelIndex index = m_listView->currentIndex();
if (!index.isValid()) {
index = m_treeView->currentIndex();
}
if (!index.isValid()) {
QMessageBox::information(this, "提示", "请先选择一个联系人");
return;
}
Contact *contact = m_model->getContact(index);
if (!contact) {
QMessageBox::information(this, "提示", "请选择联系人(而非分组)");
return;
}
ContactDialog dialog(*contact, this);
if (dialog.exec() == QDialog::Accepted) {
Contact updatedContact = dialog.getContact();
m_model->updateContact(index, updatedContact);
statusBar()->showMessage("联系人已更新", 3000);
}
}
void ContactManagerWindow::onDeleteContact() {
QModelIndex index = m_listView->currentIndex();
if (!index.isValid()) {
index = m_treeView->currentIndex();
}
if (!index.isValid()) {
QMessageBox::information(this, "提示", "请先选择一个联系人");
return;
}
Contact *contact = m_model->getContact(index);
if (!contact) {
QMessageBox::information(this, "提示", "请选择联系人(而非分组)");
return;
}
int result = QMessageBox::question(this, "确认删除",
QString("确定要删除联系人 \"%1\" 吗?").arg(contact->name()),
QMessageBox::Yes | QMessageBox::No);
if (result == QMessageBox::Yes) {
m_model->removeContact(index);
statusBar()->showMessage("联系人已删除", 3000);
}
}
void ContactManagerWindow::onSearchTextChanged(const QString &text) {
// 简单过滤:遍历模型查找匹配项
// 实际项目中建议使用QSortFilterProxyModel
if (text.isEmpty()) {
m_treeView->expandAll();
return;
}
// 展开所有节点以便搜索
m_treeView->collapseAll();
// 遍历模型查找匹配
for (int i = 0; i < m_model->rowCount(); ++i) {
QModelIndex groupIndex = m_model->index(i, 0);
bool hasMatch = false;
for (int j = 0; j < m_model->rowCount(groupIndex); ++j) {
QModelIndex contactIndex = m_model->index(j, 0, groupIndex);
Contact *contact = m_model->getContact(contactIndex);
if (contact && (contact->name().contains(text, Qt::CaseInsensitive) ||
contact->phone().contains(text, Qt::CaseInsensitive))) {
hasMatch = true;
}
}
if (hasMatch) {
m_treeView->expand(groupIndex);
}
}
}
void ContactManagerWindow::onTreeClicked(const QModelIndex &index) {
if (!index.isValid()) return;
// 如果点击的是联系人,在列表中选中
Contact *contact = m_model->getContact(index);
if (contact) {
m_listView->setCurrentIndex(index);
}
}
12.3.5 功能演示
使用步骤:
-
编译运行:
qmake contactmanager.pro make ./ContactManager -
基本操作:
- 点击"添加联系人"创建新联系人
- 双击联系人编辑信息
- 选中联系人后点击删除
-
分组管理:
- 左侧树形视图显示分组
- 点击分组查看该组联系人
- 拖放联系人到其他分组
-
搜索功能:
- 在搜索框输入姓名或电话
- 实时过滤显示结果
-
数据持久化:
- 程序退出时自动保存到JSON文件
- 下次启动时自动加载
界面布局(文本描述):
+--------------------------------------------------------+
| 文件 编辑 查看 帮助 |
+--------------------------------------------------------+
| ➕添加 ✏编辑 🗑删除 | 搜索: [________] |
+--------------------------------------------------------+
| | |
| 📁 家人 (3) | 👤 张三 |
| 📁 朋友 (5) | 📞 138-1234-5678 |
| 📁 同事 (8) | ✉ zhangsan@example.com |
| 📁 其他 (2) | |
| | 👤 李四 |
| | 📞 139-8765-4321 |
| | ✉ lisi@example.com |
| | |
| | 👤 王五 |
| | 📞 136-5555-6666 |
+--------------------------------------------------------+
| 共 18 个联系人 |
+--------------------------------------------------------+
本节小结:
✅ 完整的通讯录管理系统 - 企业级联系人管理应用
✅ 自定义树形模型 - 分组+联系人的两级结构
✅ 自定义委托 - 头像+多行信息显示
✅ 数据持久化 - JSON格式存储
✅ 搜索过滤 - 实时搜索功能
关键技术点:
- 自定义QAbstractItemModel实现树形结构
- TreeItem层次结构管理分组和联系人
- 自定义委托绘制复杂UI(头像、多行文本)
- JSON序列化实现数据持久化
- 圆形头像裁剪使用QPainterPath
可扩展功能:
- vCard格式导入导出
- 批量导入Excel联系人
- 联系人分享(二维码)
- 生日提醒功能
- 通话记录集成
- 云同步功能
- 标签系统
- 收藏联系人
与市面产品对比:
- ✅ 覆盖了通讯录应用的核心功能
- ✅ 清晰的分组管理
- ✅ 美观的UI设计
- 📝 可扩展为完整的CRM系统
完整实现要点:
- ContactDialog - 使用QDialog创建编辑界面,包含QLineEdit、QTextEdit等控件
- 搜索过滤 - 使用QSortFilterProxyModel实现
- 拖放 - 实现dropMimeData()支持分组间拖放
- 头像上传 - QFileDialog选择图片,QPixmap加载
- 统计信息 - 遍历模型统计联系人数量
- 项目需求分析
- 架构设计
- 核心功能实现
- 完整源码
- 功能演示
第12章总结:
这三个项目都是生产级别的完整应用,可以直接作为商业项目的基础或学习的最佳实践案例!
195

被折叠的 条评论
为什么被折叠?



