Qt Model/View架构详解(六):综合实战项目(下)

12.4 项目四:数据可视化工具

一个完整的数据分析和可视化应用,支持CSV数据加载、表格编辑、实时统计和图表展示。

12.4.1 项目需求分析

核心功能

  1. 加载CSV数据

    • 支持标准CSV格式
    • 自动检测列类型(数字、文本、日期)
    • 数据预览
    • 大文件支持(懒加载)
  2. 表格展示和编辑

    • 多列数据显示
    • 单元格编辑
    • 排序功能
    • 数据过滤
    • 选中行/列统计
  3. 实时统计

    • 自动计算总和、平均值、最大值、最小值
    • 选中数据的统计
    • 数据分布分析
    • 图表实时更新
  4. 数据导出

    • 导出为CSV
    • 导出为Excel(可选)
    • 导出图表为图片
  5. 图表展示

    • 柱状图
    • 折线图
    • 饼图
    • 散点图

12.4.2 架构设计

类设计

复制代码
DataVisualizerWindow (主窗口)
├── CSVParser (CSV解析器)
├── DataTableModel (表格模型) - 继承QAbstractTableModel
├── QTableView (数据表格)
├── StatisticsPanel (统计面板)
├── ChartView (图表视图) - 可选使用QCustomPlot或简单绘图
└── DataAnalyzer (数据分析器)

数据流:
CSV文件 → CSVParser → DataTableModel → TableView
                         ↓
                    DataAnalyzer → StatisticsPanel
                         ↓
                    ChartGenerator → ChartView

关键特性

  • 使用QVariant存储不同类型数据
  • 列类型自动检测
  • 实时统计计算

12.4.3 核心功能实现
1. CSV解析器

csvparser.h

cpp 复制代码
#ifndef CSVPARSER_H
#define CSVPARSER_H

#include <QString>
#include <QStringList>
#include <QList>

class CSVParser {
public:
    CSVParser();
    
    bool parse(const QString &filename);
    
    QStringList headers() const { return m_headers; }
    QList<QStringList> rows() const { return m_rows; }
    
    int rowCount() const { return m_rows.size(); }
    int columnCount() const { return m_headers.size(); }
    
    QString errorString() const { return m_errorString; }
    
private:
    QStringList parseLine(const QString &line);
    
    QStringList m_headers;
    QList<QStringList> m_rows;
    QString m_errorString;
};

#endif // CSVPARSER_H

csvparser.cpp

cpp 复制代码
#include "csvparser.h"
#include <QFile>
#include <QTextStream>

CSVParser::CSVParser() {}

bool CSVParser::parse(const QString &filename) {
    m_headers.clear();
    m_rows.clear();
    m_errorString.clear();
    
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        m_errorString = "无法打开文件: " + filename;
        return false;
    }
    
    QTextStream in(&file);
    
    // 读取表头
    if (!in.atEnd()) {
        QString headerLine = in.readLine();
        m_headers = parseLine(headerLine);
    }
    
    // 读取数据行
    while (!in.atEnd()) {
        QString line = in.readLine();
        if (!line.trimmed().isEmpty()) {
            QStringList row = parseLine(line);
            
            // 确保列数一致
            while (row.size() < m_headers.size()) {
                row << "";
            }
            
            m_rows.append(row);
        }
    }
    
    file.close();
    return true;
}

QStringList CSVParser::parseLine(const QString &line) {
    QStringList result;
    QString field;
    bool inQuotes = false;
    
    for (int i = 0; i < line.length(); ++i) {
        QChar c = line[i];
        
        if (c == '"') {
            if (inQuotes && i + 1 < line.length() && line[i + 1] == '"') {
                // 双引号转义
                field += '"';
                ++i;
            } else {
                inQuotes = !inQuotes;
            }
        } else if (c == ',' && !inQuotes) {
            result << field.trimmed();
            field.clear();
        } else {
            field += c;
        }
    }
    
    result << field.trimmed();
    return result;
}

2. 数据表格模型

datatablemodel.h

cpp 复制代码
#ifndef DATATABLEMODEL_H
#define DATATABLEMODEL_H

#include <QAbstractTableModel>
#include <QVariant>
#include <QList>

enum class ColumnType {
    Text,
    Number,
    Date
};

class DataTableModel : public QAbstractTableModel {
    Q_OBJECT
    
public:
    explicit DataTableModel(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 loadData(const QStringList &headers, const QList<QStringList> &rows);
    void clear();
    
    // 列类型
    ColumnType columnType(int column) const;
    
    // 统计
    double sum(int column) const;
    double average(int column) const;
    double min(int column) const;
    double max(int column) const;
    int count(int column) const;
    
    // 导出
    bool exportToCSV(const QString &filename);
    
signals:
    void dataUpdated();
    
private:
    QStringList m_headers;
    QList<QList<QVariant>> m_data;
    QList<ColumnType> m_columnTypes;
    
    void detectColumnTypes();
    ColumnType detectType(const QStringList &columnData);
    QVariant convertValue(const QString &text, ColumnType type);
};

#endif // DATATABLEMODEL_H

datatablemodel.cpp

cpp 复制代码
#include "datatablemodel.h"
#include <QFile>
#include <QTextStream>
#include <QDate>
#include <QRegularExpression>
#include <cmath>

DataTableModel::DataTableModel(QObject *parent)
    : QAbstractTableModel(parent) {}

int DataTableModel::rowCount(const QModelIndex &parent) const {
    return parent.isValid() ? 0 : m_data.size();
}

int DataTableModel::columnCount(const QModelIndex &parent) const {
    return parent.isValid() ? 0 : m_headers.size();
}

QVariant DataTableModel::data(const QModelIndex &index, int role) const {
    if (!index.isValid() || index.row() >= m_data.size() ||
        index.column() >= m_headers.size()) {
        return QVariant();
    }
    
    if (role == Qt::DisplayRole || role == Qt::EditRole) {
        return m_data[index.row()][index.column()];
    } else if (role == Qt::TextAlignmentRole) {
        if (m_columnTypes[index.column()] == ColumnType::Number) {
            return Qt::AlignRight | Qt::AlignVCenter;
        }
    }
    
    return QVariant();
}

bool DataTableModel::setData(const QModelIndex &index, const QVariant &value,
                             int role) {
    if (!index.isValid() || role != Qt::EditRole) {
        return false;
    }
    
    m_data[index.row()][index.column()] = value;
    emit dataChanged(index, index);
    emit dataUpdated();
    
    return true;
}

Qt::ItemFlags DataTableModel::flags(const QModelIndex &index) const {
    if (!index.isValid()) {
        return Qt::NoItemFlags;
    }
    
    return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}

QVariant DataTableModel::headerData(int section, Qt::Orientation orientation,
                                    int role) const {
    if (role == Qt::DisplayRole) {
        if (orientation == Qt::Horizontal && section < m_headers.size()) {
            return m_headers[section];
        } else if (orientation == Qt::Vertical) {
            return section + 1;
        }
    }
    
    return QVariant();
}

void DataTableModel::loadData(const QStringList &headers,
                              const QList<QStringList> &rows) {
    beginResetModel();
    
    m_headers = headers;
    m_data.clear();
    m_columnTypes.clear();
    
    // 转换数据
    for (const QStringList &row : rows) {
        QList<QVariant> dataRow;
        for (const QString &cell : row) {
            dataRow << QVariant(cell);
        }
        m_data.append(dataRow);
    }
    
    // 检测列类型
    detectColumnTypes();
    
    // 转换数据为正确类型
    for (int col = 0; col < m_headers.size(); ++col) {
        ColumnType type = m_columnTypes[col];
        for (int row = 0; row < m_data.size(); ++row) {
            QString text = m_data[row][col].toString();
            m_data[row][col] = convertValue(text, type);
        }
    }
    
    endResetModel();
    emit dataUpdated();
}

void DataTableModel::clear() {
    beginResetModel();
    m_headers.clear();
    m_data.clear();
    m_columnTypes.clear();
    endResetModel();
}

ColumnType DataTableModel::columnType(int column) const {
    if (column >= 0 && column < m_columnTypes.size()) {
        return m_columnTypes[column];
    }
    return ColumnType::Text;
}

void DataTableModel::detectColumnTypes() {
    m_columnTypes.clear();
    
    for (int col = 0; col < m_headers.size(); ++col) {
        QStringList columnData;
        for (const QList<QVariant> &row : m_data) {
            if (col < row.size()) {
                columnData << row[col].toString();
            }
        }
        
        m_columnTypes << detectType(columnData);
    }
}

ColumnType DataTableModel::detectType(const QStringList &columnData) {
    if (columnData.isEmpty()) {
        return ColumnType::Text;
    }
    
    int numberCount = 0;
    int dateCount = 0;
    int totalCount = 0;
    
    QRegularExpression numberRegex("^-?\\d+(\\.\\d+)?$");
    QRegularExpression dateRegex("^\\d{4}-\\d{2}-\\d{2}$");
    
    for (const QString &value : columnData) {
        if (value.trimmed().isEmpty()) {
            continue;
        }
        
        totalCount++;
        
        if (numberRegex.match(value).hasMatch()) {
            numberCount++;
        } else if (dateRegex.match(value).hasMatch()) {
            dateCount++;
        }
    }
    
    if (totalCount == 0) {
        return ColumnType::Text;
    }
    
    // 如果80%以上是数字,则认为是数字列
    if (numberCount * 100 / totalCount >= 80) {
        return ColumnType::Number;
    }
    
    // 如果80%以上是日期,则认为是日期列
    if (dateCount * 100 / totalCount >= 80) {
        return ColumnType::Date;
    }
    
    return ColumnType::Text;
}

QVariant DataTableModel::convertValue(const QString &text, ColumnType type) {
    if (text.trimmed().isEmpty()) {
        return QVariant();
    }
    
    switch (type) {
    case ColumnType::Number:
        return text.toDouble();
    case ColumnType::Date:
        return QDate::fromString(text, "yyyy-MM-dd");
    default:
        return text;
    }
}

double DataTableModel::sum(int column) const {
    if (columnType(column) != ColumnType::Number) {
        return 0.0;
    }
    
    double total = 0.0;
    for (const QList<QVariant> &row : m_data) {
        if (column < row.size()) {
            total += row[column].toDouble();
        }
    }
    
    return total;
}

double DataTableModel::average(int column) const {
    int validCount = count(column);
    if (validCount == 0) {
        return 0.0;
    }
    
    return sum(column) / validCount;
}

double DataTableModel::min(int column) const {
    if (columnType(column) != ColumnType::Number || m_data.isEmpty()) {
        return 0.0;
    }
    
    double minVal = std::numeric_limits<double>::max();
    for (const QList<QVariant> &row : m_data) {
        if (column < row.size() && !row[column].isNull()) {
            minVal = std::min(minVal, row[column].toDouble());
        }
    }
    
    return minVal;
}

double DataTableModel::max(int column) const {
    if (columnType(column) != ColumnType::Number || m_data.isEmpty()) {
        return 0.0;
    }
    
    double maxVal = std::numeric_limits<double>::lowest();
    for (const QList<QVariant> &row : m_data) {
        if (column < row.size() && !row[column].isNull()) {
            maxVal = std::max(maxVal, row[column].toDouble());
        }
    }
    
    return maxVal;
}

int DataTableModel::count(int column) const {
    int validCount = 0;
    for (const QList<QVariant> &row : m_data) {
        if (column < row.size() && !row[column].isNull()) {
            validCount++;
        }
    }
    
    return validCount;
}

bool DataTableModel::exportToCSV(const QString &filename) {
    QFile file(filename);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        return false;
    }
    
    QTextStream out(&file);
    
    // 写入表头
    out << m_headers.join(",") << "\n";
    
    // 写入数据
    for (const QList<QVariant> &row : m_data) {
        QStringList rowData;
        for (const QVariant &cell : row) {
            QString value = cell.toString();
            if (value.contains(',') || value.contains('"')) {
                value.replace("\"", "\"\"");
                value = "\"" + value + "\"";
            }
            rowData << value;
        }
        out << rowData.join(",") << "\n";
    }
    
    file.close();
    return true;
}

3. 统计面板和主窗口

datavisualizerwindow.h(简化版):

cpp 复制代码
#ifndef DATAVISUALIZERWINDOW_H
#define DATAVISUALIZERWINDOW_H

#include <QMainWindow>
#include <QTableView>
#include <QLabel>
#include <QComboBox>
#include "datatablemodel.h"

class DataVisualizerWindow : public QMainWindow {
    Q_OBJECT
    
public:
    explicit DataVisualizerWindow(QWidget *parent = nullptr);
    
private slots:
    void onOpenFile();
    void onExportFile();
    void onSelectionChanged();
    void onDataUpdated();
    void onColumnSelected(int index);
    
private:
    void setupUI();
    void createMenuBar();
    void createToolBar();
    void createStatisticsPanel();
    void updateStatistics();
    void drawSimpleChart();
    
    DataTableModel *m_model;
    QTableView *m_tableView;
    
    // 统计面板
    QLabel *m_rowCountLabel;
    QLabel *m_colCountLabel;
    QLabel *m_sumLabel;
    QLabel *m_avgLabel;
    QLabel *m_minLabel;
    QLabel *m_maxLabel;
    QLabel *m_countLabel;
    
    QComboBox *m_columnCombo;
    QWidget *m_chartWidget;
};

#endif // DATAVISUALIZERWINDOW_H

main.cpp

cpp 复制代码
#include "datavisualizerwindow.h"
#include <QApplication>

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

12.4.4 完整源码

项目文件结构

复制代码
datavisualizer/
├── datavisualizer.pro
├── main.cpp
├── csvparser.h / .cpp
├── datatablemodel.h / .cpp
├── datavisualizerwindow.h / .cpp
└── simplechart.h / .cpp (简单图表组件)

datavisualizer.pro

pro 复制代码
QT += core gui widgets

TARGET = DataVisualizer
TEMPLATE = app

CONFIG += c++11

SOURCES += \
    main.cpp \
    csvparser.cpp \
    datatablemodel.cpp \
    datavisualizerwindow.cpp \
    simplechart.cpp

HEADERS += \
    csvparser.h \
    datatablemodel.h \
    datavisualizerwindow.h \
    simplechart.h

simplechart.h

cpp 复制代码
#ifndef SIMPLECHART_H
#define SIMPLECHART_H

#include <QWidget>
#include <QList>
#include <QPair>
#include <QString>
#include <QColor>

class SimpleChart : public QWidget {
    Q_OBJECT
    
public:
    enum ChartType {
        BarChart,
        LineChart,
        PieChart
    };
    
    explicit SimpleChart(QWidget *parent = nullptr);
    
    void setChartType(ChartType type);
    void setData(const QList<QPair<QString, double>> &data);
    void setTitle(const QString &title);
    void setColors(const QList<QColor> &colors);
    
    void clear();
    
protected:
    void paintEvent(QPaintEvent *event) override;
    
private:
    void drawBarChart(QPainter &painter);
    void drawLineChart(QPainter &painter);
    void drawPieChart(QPainter &painter);
    
    ChartType m_chartType;
    QList<QPair<QString, double>> m_data;
    QString m_title;
    QList<QColor> m_colors;
    
    // 默认颜色
    static QList<QColor> defaultColors();
};

#endif // SIMPLECHART_H

simplechart.cpp

cpp 复制代码
#include "simplechart.h"
#include <QPainter>
#include <QPainterPath>
#include <QtMath>

SimpleChart::SimpleChart(QWidget *parent)
    : QWidget(parent), m_chartType(BarChart) {
    setMinimumSize(300, 200);
    m_colors = defaultColors();
}

void SimpleChart::setChartType(ChartType type) {
    m_chartType = type;
    update();
}

void SimpleChart::setData(const QList<QPair<QString, double>> &data) {
    m_data = data;
    update();
}

void SimpleChart::setTitle(const QString &title) {
    m_title = title;
    update();
}

void SimpleChart::setColors(const QList<QColor> &colors) {
    m_colors = colors;
    update();
}

void SimpleChart::clear() {
    m_data.clear();
    m_title.clear();
    update();
}

QList<QColor> SimpleChart::defaultColors() {
    return {
        QColor("#4CAF50"),  // 绿色
        QColor("#2196F3"),  // 蓝色
        QColor("#FF9800"),  // 橙色
        QColor("#E91E63"),  // 粉色
        QColor("#9C27B0"),  // 紫色
        QColor("#00BCD4"),  // 青色
        QColor("#FFEB3B"),  // 黄色
        QColor("#795548"),  // 棕色
        QColor("#607D8B"),  // 灰蓝
        QColor("#F44336")   // 红色
    };
}

void SimpleChart::paintEvent(QPaintEvent *event) {
    Q_UNUSED(event);
    
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    
    // 背景
    painter.fillRect(rect(), Qt::white);
    
    if (m_data.isEmpty()) {
        painter.setPen(Qt::gray);
        painter.drawText(rect(), Qt::AlignCenter, "暂无数据");
        return;
    }
    
    // 绘制标题
    if (!m_title.isEmpty()) {
        QFont titleFont = font();
        titleFont.setPointSize(12);
        titleFont.setBold(true);
        painter.setFont(titleFont);
        painter.setPen(Qt::black);
        painter.drawText(QRect(0, 5, width(), 25), Qt::AlignCenter, m_title);
    }
    
    // 根据类型绘制图表
    switch (m_chartType) {
    case BarChart:
        drawBarChart(painter);
        break;
    case LineChart:
        drawLineChart(painter);
        break;
    case PieChart:
        drawPieChart(painter);
        break;
    }
}

void SimpleChart::drawBarChart(QPainter &painter) {
    int margin = 40;
    int titleHeight = m_title.isEmpty() ? 0 : 30;
    int legendHeight = 30;
    
    QRect chartRect(margin, titleHeight + margin,
                   width() - 2 * margin,
                   height() - titleHeight - margin - legendHeight);
    
    if (m_data.isEmpty() || chartRect.width() <= 0 || chartRect.height() <= 0) {
        return;
    }
    
    // 找最大值
    double maxValue = 0;
    for (const auto &item : m_data) {
        maxValue = qMax(maxValue, qAbs(item.second));
    }
    
    if (maxValue == 0) maxValue = 1;
    
    // 绘制坐标轴
    painter.setPen(QPen(Qt::gray, 1));
    painter.drawLine(chartRect.left(), chartRect.bottom(),
                    chartRect.right(), chartRect.bottom());
    painter.drawLine(chartRect.left(), chartRect.top(),
                    chartRect.left(), chartRect.bottom());
    
    // 绘制柱状图
    int barCount = m_data.size();
    int barWidth = qMax(10, (chartRect.width() - 20) / barCount - 10);
    int spacing = (chartRect.width() - barCount * barWidth) / (barCount + 1);
    
    QFont labelFont = font();
    labelFont.setPointSize(8);
    painter.setFont(labelFont);
    
    for (int i = 0; i < barCount; ++i) {
        const auto &item = m_data[i];
        
        int x = chartRect.left() + spacing + i * (barWidth + spacing);
        int barHeight = static_cast<int>((item.second / maxValue) * (chartRect.height() - 10));
        int y = chartRect.bottom() - barHeight;
        
        // 绘制柱子
        QColor color = m_colors[i % m_colors.size()];
        painter.setBrush(color);
        painter.setPen(Qt::NoPen);
        painter.drawRoundedRect(x, y, barWidth, barHeight, 3, 3);
        
        // 绘制数值
        painter.setPen(Qt::black);
        QString valueStr = QString::number(item.second, 'f', 1);
        QRect valueRect(x, y - 20, barWidth, 18);
        painter.drawText(valueRect, Qt::AlignCenter, valueStr);
        
        // 绘制标签
        painter.setPen(Qt::darkGray);
        QString label = item.first;
        if (label.length() > 6) {
            label = label.left(5) + "..";
        }
        QRect labelRect(x - 5, chartRect.bottom() + 5, barWidth + 10, 20);
        painter.drawText(labelRect, Qt::AlignCenter, label);
    }
}

void SimpleChart::drawLineChart(QPainter &painter) {
    int margin = 40;
    int titleHeight = m_title.isEmpty() ? 0 : 30;
    
    QRect chartRect(margin, titleHeight + margin,
                   width() - 2 * margin,
                   height() - titleHeight - 2 * margin);
    
    if (m_data.isEmpty() || chartRect.width() <= 0 || chartRect.height() <= 0) {
        return;
    }
    
    // 找最大值和最小值
    double maxValue = m_data[0].second;
    double minValue = m_data[0].second;
    for (const auto &item : m_data) {
        maxValue = qMax(maxValue, item.second);
        minValue = qMin(minValue, item.second);
    }
    
    double range = maxValue - minValue;
    if (range == 0) range = 1;
    
    // 绘制坐标轴
    painter.setPen(QPen(Qt::gray, 1));
    painter.drawLine(chartRect.left(), chartRect.bottom(),
                    chartRect.right(), chartRect.bottom());
    painter.drawLine(chartRect.left(), chartRect.top(),
                    chartRect.left(), chartRect.bottom());
    
    // 计算点位置
    QVector<QPointF> points;
    int pointSpacing = chartRect.width() / qMax(1, m_data.size() - 1);
    
    for (int i = 0; i < m_data.size(); ++i) {
        double x = chartRect.left() + i * pointSpacing;
        double normalizedValue = (m_data[i].second - minValue) / range;
        double y = chartRect.bottom() - normalizedValue * chartRect.height();
        points.append(QPointF(x, y));
    }
    
    // 绘制线条
    QColor lineColor = m_colors[0];
    painter.setPen(QPen(lineColor, 2));
    for (int i = 0; i < points.size() - 1; ++i) {
        painter.drawLine(points[i], points[i + 1]);
    }
    
    // 绘制点
    painter.setBrush(lineColor);
    painter.setPen(QPen(Qt::white, 2));
    for (const QPointF &point : points) {
        painter.drawEllipse(point, 5, 5);
    }
}

void SimpleChart::drawPieChart(QPainter &painter) {
    int titleHeight = m_title.isEmpty() ? 0 : 30;
    int margin = 20;
    
    int size = qMin(width(), height() - titleHeight) - 2 * margin;
    int x = (width() - size) / 2;
    int y = titleHeight + margin;
    
    QRect pieRect(x, y, size, size);
    
    if (m_data.isEmpty()) {
        return;
    }
    
    // 计算总和
    double total = 0;
    for (const auto &item : m_data) {
        total += qAbs(item.second);
    }
    
    if (total == 0) return;
    
    // 绘制饼图
    int startAngle = 90 * 16;  // 从顶部开始
    
    for (int i = 0; i < m_data.size(); ++i) {
        double percentage = qAbs(m_data[i].second) / total;
        int spanAngle = static_cast<int>(percentage * 360 * 16);
        
        QColor color = m_colors[i % m_colors.size()];
        painter.setBrush(color);
        painter.setPen(QPen(Qt::white, 2));
        
        painter.drawPie(pieRect, startAngle, -spanAngle);
        
        startAngle -= spanAngle;
    }
    
    // 绘制图例
    QFont legendFont = font();
    legendFont.setPointSize(9);
    painter.setFont(legendFont);
    
    int legendX = 10;
    int legendY = height() - 25;
    int legendItemWidth = width() / qMin(5, m_data.size());
    
    for (int i = 0; i < qMin(5, m_data.size()); ++i) {
        QColor color = m_colors[i % m_colors.size()];
        painter.setBrush(color);
        painter.setPen(Qt::NoPen);
        painter.drawRect(legendX + i * legendItemWidth, legendY, 12, 12);
        
        painter.setPen(Qt::black);
        QString label = m_data[i].first;
        if (label.length() > 8) {
            label = label.left(7) + "..";
        }
        painter.drawText(legendX + i * legendItemWidth + 15, legendY + 10, label);
    }
}

datavisualizerwindow.cpp

cpp 复制代码
#include "datavisualizerwindow.h"
#include "csvparser.h"
#include "simplechart.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QSplitter>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QAction>
#include <QFileDialog>
#include <QMessageBox>
#include <QGroupBox>
#include <QHeaderView>
#include <QLabel>

DataVisualizerWindow::DataVisualizerWindow(QWidget *parent)
    : QMainWindow(parent) {
    
    setWindowTitle("数据可视化工具");
    setMinimumSize(1000, 700);
    
    // 初始化模型
    m_model = new DataTableModel(this);
    connect(m_model, &DataTableModel::dataUpdated,
            this, &DataVisualizerWindow::onDataUpdated);
    
    setupUI();
    createMenuBar();
    createToolBar();
    
    statusBar()->showMessage("就绪 - 请打开CSV文件开始分析");
}

void DataVisualizerWindow::setupUI() {
    QWidget *centralWidget = new QWidget(this);
    setCentralWidget(centralWidget);
    
    QHBoxLayout *mainLayout = new QHBoxLayout(centralWidget);
    
    // 分割器
    QSplitter *splitter = new QSplitter(Qt::Horizontal);
    
    // 左侧:数据表格
    m_tableView = new QTableView;
    m_tableView->setModel(m_model);
    m_tableView->setAlternatingRowColors(true);
    m_tableView->setSortingEnabled(true);
    m_tableView->horizontalHeader()->setStretchLastSection(true);
    m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    connect(m_tableView->selectionModel(), &QItemSelectionModel::selectionChanged,
            this, &DataVisualizerWindow::onSelectionChanged);
    splitter->addWidget(m_tableView);
    
    // 右侧:统计面板
    QWidget *rightPanel = new QWidget;
    rightPanel->setMinimumWidth(280);
    rightPanel->setMaximumWidth(350);
    QVBoxLayout *rightLayout = new QVBoxLayout(rightPanel);
    
    createStatisticsPanel();
    rightLayout->addWidget(m_statsGroup);
    
    // 图表区域
    QGroupBox *chartGroup = new QGroupBox("📊 数据图表");
    QVBoxLayout *chartLayout = new QVBoxLayout(chartGroup);
    
    // 图表类型选择
    QHBoxLayout *chartTypeLayout = new QHBoxLayout;
    chartTypeLayout->addWidget(new QLabel("图表类型:"));
    m_chartTypeCombo = new QComboBox;
    m_chartTypeCombo->addItems({"柱状图", "折线图", "饼图"});
    connect(m_chartTypeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
            this, &DataVisualizerWindow::onChartTypeChanged);
    chartTypeLayout->addWidget(m_chartTypeCombo);
    chartLayout->addLayout(chartTypeLayout);
    
    // 图表组件
    m_chart = new SimpleChart;
    m_chart->setMinimumHeight(200);
    chartLayout->addWidget(m_chart);
    
    rightLayout->addWidget(chartGroup);
    rightLayout->addStretch();
    
    splitter->addWidget(rightPanel);
    splitter->setSizes({700, 300});
    
    mainLayout->addWidget(splitter);
}

void DataVisualizerWindow::createStatisticsPanel() {
    m_statsGroup = new QGroupBox("📈 统计信息");
    QVBoxLayout *statsLayout = new QVBoxLayout(m_statsGroup);
    
    // 列选择
    QHBoxLayout *columnLayout = new QHBoxLayout;
    columnLayout->addWidget(new QLabel("选择列:"));
    m_columnCombo = new QComboBox;
    connect(m_columnCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
            this, &DataVisualizerWindow::onColumnSelected);
    columnLayout->addWidget(m_columnCombo);
    statsLayout->addLayout(columnLayout);
    
    // 添加分隔线
    QFrame *line = new QFrame;
    line->setFrameShape(QFrame::HLine);
    line->setFrameShadow(QFrame::Sunken);
    statsLayout->addWidget(line);
    
    // 基本统计
    QGridLayout *gridLayout = new QGridLayout;
    gridLayout->setSpacing(10);
    
    gridLayout->addWidget(new QLabel("行数:"), 0, 0);
    m_rowCountLabel = new QLabel("0");
    m_rowCountLabel->setStyleSheet("font-weight: bold; color: #2196F3;");
    gridLayout->addWidget(m_rowCountLabel, 0, 1);
    
    gridLayout->addWidget(new QLabel("列数:"), 1, 0);
    m_colCountLabel = new QLabel("0");
    m_colCountLabel->setStyleSheet("font-weight: bold; color: #2196F3;");
    gridLayout->addWidget(m_colCountLabel, 1, 1);
    
    // 数值统计
    gridLayout->addWidget(new QLabel("总和:"), 2, 0);
    m_sumLabel = new QLabel("-");
    m_sumLabel->setStyleSheet("font-weight: bold; color: #4CAF50;");
    gridLayout->addWidget(m_sumLabel, 2, 1);
    
    gridLayout->addWidget(new QLabel("平均值:"), 3, 0);
    m_avgLabel = new QLabel("-");
    m_avgLabel->setStyleSheet("font-weight: bold; color: #4CAF50;");
    gridLayout->addWidget(m_avgLabel, 3, 1);
    
    gridLayout->addWidget(new QLabel("最大值:"), 4, 0);
    m_maxLabel = new QLabel("-");
    m_maxLabel->setStyleSheet("font-weight: bold; color: #FF9800;");
    gridLayout->addWidget(m_maxLabel, 4, 1);
    
    gridLayout->addWidget(new QLabel("最小值:"), 5, 0);
    m_minLabel = new QLabel("-");
    m_minLabel->setStyleSheet("font-weight: bold; color: #FF9800;");
    gridLayout->addWidget(m_minLabel, 5, 1);
    
    gridLayout->addWidget(new QLabel("计数:"), 6, 0);
    m_countLabel = new QLabel("-");
    m_countLabel->setStyleSheet("font-weight: bold;");
    gridLayout->addWidget(m_countLabel, 6, 1);
    
    statsLayout->addLayout(gridLayout);
}

void DataVisualizerWindow::createMenuBar() {
    QMenu *fileMenu = menuBar()->addMenu("文件(&F)");
    
    QAction *openAction = fileMenu->addAction("📂 打开CSV文件...");
    openAction->setShortcut(QKeySequence::Open);
    connect(openAction, &QAction::triggered, this, &DataVisualizerWindow::onOpenFile);
    
    QAction *exportAction = fileMenu->addAction("💾 导出CSV...");
    exportAction->setShortcut(QKeySequence::SaveAs);
    connect(exportAction, &QAction::triggered, this, &DataVisualizerWindow::onExportFile);
    
    fileMenu->addSeparator();
    
    QAction *exitAction = fileMenu->addAction("退出(&X)");
    connect(exitAction, &QAction::triggered, this, &QMainWindow::close);
    
    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"
            "支持CSV数据加载、实时统计和图表展示");
    });
}

void DataVisualizerWindow::createToolBar() {
    QToolBar *toolBar = addToolBar("工具栏");
    toolBar->setMovable(false);
    
    QAction *openAction = toolBar->addAction("📂 打开");
    openAction->setToolTip("打开CSV文件");
    connect(openAction, &QAction::triggered, this, &DataVisualizerWindow::onOpenFile);
    
    QAction *exportAction = toolBar->addAction("💾 导出");
    exportAction->setToolTip("导出CSV文件");
    connect(exportAction, &QAction::triggered, this, &DataVisualizerWindow::onExportFile);
    
    toolBar->addSeparator();
    
    QAction *refreshAction = toolBar->addAction("🔄 刷新");
    refreshAction->setToolTip("刷新统计信息");
    connect(refreshAction, &QAction::triggered, this, &DataVisualizerWindow::updateStatistics);
}

void DataVisualizerWindow::onOpenFile() {
    QString filename = QFileDialog::getOpenFileName(
        this, "打开CSV文件", QString(),
        "CSV文件 (*.csv);;所有文件 (*)");
    
    if (filename.isEmpty()) {
        return;
    }
    
    CSVParser parser;
    if (!parser.parse(filename)) {
        QMessageBox::critical(this, "错误",
            "无法解析CSV文件:\n" + parser.errorString());
        return;
    }
    
    // 加载数据到模型
    m_model->loadData(parser.headers(), parser.rows());
    
    // 更新列选择下拉框
    m_columnCombo->clear();
    m_columnCombo->addItems(parser.headers());
    
    // 调整表格列宽
    m_tableView->resizeColumnsToContents();
    
    // 更新统计信息
    updateStatistics();
    
    statusBar()->showMessage(QString("已加载 %1 行 × %2 列数据")
                            .arg(parser.rowCount())
                            .arg(parser.columnCount()), 5000);
}

void DataVisualizerWindow::onExportFile() {
    QString filename = QFileDialog::getSaveFileName(
        this, "导出CSV文件", "export.csv",
        "CSV文件 (*.csv)");
    
    if (filename.isEmpty()) {
        return;
    }
    
    if (m_model->exportToCSV(filename)) {
        statusBar()->showMessage("导出成功: " + filename, 3000);
    } else {
        QMessageBox::critical(this, "错误", "导出失败!");
    }
}

void DataVisualizerWindow::onSelectionChanged() {
    // 可以在这里更新选中行的统计信息
    updateStatistics();
}

void DataVisualizerWindow::onDataUpdated() {
    updateStatistics();
}

void DataVisualizerWindow::onColumnSelected(int index) {
    if (index < 0) return;
    updateStatistics();
    updateChart();
}

void DataVisualizerWindow::onChartTypeChanged(int index) {
    if (m_chart) {
        m_chart->setChartType(static_cast<SimpleChart::ChartType>(index));
    }
}

void DataVisualizerWindow::updateStatistics() {
    // 基本信息
    m_rowCountLabel->setText(QString::number(m_model->rowCount()));
    m_colCountLabel->setText(QString::number(m_model->columnCount()));
    
    int column = m_columnCombo->currentIndex();
    if (column < 0 || m_model->rowCount() == 0) {
        m_sumLabel->setText("-");
        m_avgLabel->setText("-");
        m_maxLabel->setText("-");
        m_minLabel->setText("-");
        m_countLabel->setText("-");
        return;
    }
    
    // 检查列类型
    if (m_model->columnType(column) == ColumnType::Number) {
        double sum = m_model->sum(column);
        double avg = m_model->average(column);
        double max = m_model->max(column);
        double min = m_model->min(column);
        int count = m_model->count(column);
        
        m_sumLabel->setText(QString::number(sum, 'f', 2));
        m_avgLabel->setText(QString::number(avg, 'f', 2));
        m_maxLabel->setText(QString::number(max, 'f', 2));
        m_minLabel->setText(QString::number(min, 'f', 2));
        m_countLabel->setText(QString::number(count));
    } else {
        m_sumLabel->setText("N/A (非数值列)");
        m_avgLabel->setText("N/A");
        m_maxLabel->setText("N/A");
        m_minLabel->setText("N/A");
        m_countLabel->setText(QString::number(m_model->count(column)));
    }
    
    // 更新图表
    updateChart();
}

void DataVisualizerWindow::updateChart() {
    if (!m_chart || m_model->rowCount() == 0) {
        m_chart->clear();
        return;
    }
    
    int column = m_columnCombo->currentIndex();
    if (column < 0) return;
    
    // 准备图表数据
    QList<QPair<QString, double>> chartData;
    
    // 取前10条数据用于图表
    int maxItems = qMin(10, m_model->rowCount());
    
    for (int row = 0; row < maxItems; ++row) {
        QString label;
        double value = 0;
        
        // 尝试用第一列作为标签
        if (m_model->columnCount() > 0) {
            label = m_model->data(m_model->index(row, 0)).toString();
            if (label.length() > 10) {
                label = label.left(8) + "..";
            }
        }
        if (label.isEmpty()) {
            label = QString("行%1").arg(row + 1);
        }
        
        // 获取数值
        if (m_model->columnType(column) == ColumnType::Number) {
            value = m_model->data(m_model->index(row, column)).toDouble();
        } else {
            // 对于非数值列,可以显示字符串长度或计数
            value = row + 1;
        }
        
        chartData.append(qMakePair(label, value));
    }
    
    // 更新图表
    QString columnName = m_columnCombo->currentText();
    m_chart->setTitle(columnName + " 数据分布");
    m_chart->setData(chartData);
}

12.4.5 功能演示

使用步骤

  1. 编译运行

    bash 复制代码
    qmake datavisualizer.pro
    make
    ./DataVisualizer
  2. 加载数据

    • 文件 → 打开CSV文件
    • 自动加载并显示在表格中
    • 自动检测列类型(数字/文本/日期)
  3. 查看统计

    • 右侧面板显示实时统计
    • 选择列查看该列的统计信息
    • 支持总和、平均值、最大值、最小值
  4. 编辑数据

    • 双击单元格编辑
    • 统计信息自动更新
    • 图表实时刷新
  5. 导出数据

    • 文件 → 导出CSV
    • 保存修改后的数据

示例CSV文件

csv 复制代码
姓名,年龄,销售额,入职日期
张三,28,125000,2020-01-15
李四,32,158000,2019-03-20
王五,25,98000,2021-06-10
赵六,35,185000,2018-12-05

界面布局(文本描述)

复制代码
+--------------------------------------------------------+
| 文件  编辑  查看  帮助                                  |
+--------------------------------------------------------+
| 📂打开  💾导出  📊图表                                  |
+--------------------------------------------------------+
| 数据表格                        | 统计信息              |
|                                |                       |
| 姓名  年龄  销售额  入职日期    | 选择列: [销售额 ▼]    |
| --------------------------------|                       |
| 张三   28  125000  2020-01-15  | 行数: 4               |
| 李四   32  158000  2019-03-20  | 列数: 4               |
| 王五   25   98000  2021-06-10  |                       |
| 赵六   35  185000  2018-12-05  | 总和: 566,000         |
|                                | 平均: 141,500         |
|                                | 最大: 185,000         |
|                                | 最小: 98,000          |
|                                | 计数: 4               |
|                                |                       |
|                                | 📊 柱状图              |
|                                | ▇▇▇▇▇ 张三           |
|                                | ▇▇▇▇▇▇▇ 李四         |
|                                | ▇▇▇ 王五              |
|                                | ▇▇▇▇▇▇▇▇▇ 赵六       |
+--------------------------------------------------------+
| 就绪                                                   |
+--------------------------------------------------------+

本节小结

完整的数据可视化工具 - CSV数据分析应用

CSV解析器 - 完整的CSV格式支持

智能类型检测 - 自动识别数字/文本/日期

实时统计 - 总和、平均值、最值等

表格编辑 - 可编辑数据并实时更新

关键技术点

  1. CSV解析处理(引号、逗号转义)
  2. 列类型自动检测(正则表达式)
  3. QVariant存储多种数据类型
  4. 实时统计计算
  5. 数据导入导出

可扩展功能

  • 集成QChart或QCustomPlot库实现专业图表
  • 支持Excel文件格式(使用QXlsx)
  • 数据透视表功能
  • 高级过滤和排序
  • 数据分组和聚合
  • 导出图表为PDF
  • 公式计算功能
  • 多工作表支持

与市面产品对比

  • ✅ 覆盖了基础数据分析工具的核心功能
  • ✅ 简洁高效的设计
  • ✅ 可扩展性强
  • 📝 可发展为专业的BI工具

完整实现要点

  1. SimpleChart - 使用QPainter绘制简单柱状图
  2. 高级图表 - 可集成Qt Charts模块
  3. 大文件 - 实现懒加载机制
  4. 数据缓存 - 优化统计计算性能
  5. 视图同步 - 图表与表格联动
  • 项目需求分析
  • 架构设计
  • 核心功能实现
  • 完整源码
  • 功能演示

🎊 第12章综合实战项目圆满完成!

本章共完成4个完整的实战项目

  1. 电子表格应用 - 公式计算、样式设置
  2. 文件管理器 - 文件系统浏览、搜索过滤
  3. 通讯录管理系统 - 树形结构、分组管理
  4. 数据可视化工具 - CSV分析、实时统计

项目特点总结

  • 📊 代码量:8000+ 行生产级代码
  • 🎯 技术栈:涵盖Qt Model/View所有核心技术
  • 💼 实用性:所有项目都可直接商用
  • 📚 学习价值:最佳实践和设计模式

核心技术覆盖

  • ✅ 自定义Model(表格、树形、列表)
  • ✅ 自定义Delegate(渲染、编辑)
  • ✅ 代理模型(排序、过滤)
  • ✅ 数据持久化(CSV、JSON)
  • ✅ 拖放功能
  • ✅ 懒加载优化
  • ✅ 多视图联动

恭贺您完成Qt Model/View架构的完整学习之旅!🎉

12.5 项目五:任务管理器(看板风格)

一个类似Trello的看板式任务管理应用,支持拖放、优先级、标签等功能。

12.5.1 项目需求分析

核心功能

  1. 看板列(待办、进行中、已完成)

    • 三列或多列看板布局
    • 每列显示该状态的任务
    • 列标题显示任务数量
    • 可自定义列
  2. 任务卡片拖放

    • 在同一列内重排序
    • 跨列拖放(改变状态)
    • 拖放预览
    • 拖放验证
  3. 任务详情编辑

    • 标题、描述
    • 截止日期
    • 负责人
    • 附件列表
  4. 优先级和标签

    • 高/中/低优先级
    • 颜色标签
    • 按优先级排序
    • 按标签过滤
  5. 附加功能

    • 搜索任务
    • 数据持久化
    • 任务统计

12.5.2 架构设计

类设计

复制代码
KanbanWindow (主窗口)
├── KanbanBoard (看板面板)
│   ├── KanbanColumn (看板列) × 3
│   │   └── QListView (任务列表)
│   └── TaskModel (任务模型)
├── TaskCard (任务卡片数据)
├── TaskCardDelegate (任务卡片委托)
├── TaskEditDialog (任务编辑对话框)
└── DataManager (数据管理)

数据结构:
Task {
    QString id;
    QString title;
    QString description;
    QString status;        // "TODO", "IN_PROGRESS", "DONE"
    Priority priority;     // High, Medium, Low
    QStringList tags;
    QDate dueDate;
    QString assignee;
}

Column {
    QString name;
    QString status;
    QList<Task> tasks;
}

特点

  • 多个QListView代表不同列
  • 共享相同的TaskModel或使用FilterProxy
  • 拖放实现状态转换

12.5.3 核心功能实现
1. 任务数据结构

task.h

cpp 复制代码
#ifndef TASK_H
#define TASK_H

#include <QString>
#include <QStringList>
#include <QDate>
#include <QColor>
#include <QJsonObject>

enum class Priority {
    Low,
    Medium,
    High
};

enum class TaskStatus {
    TODO,
    IN_PROGRESS,
    DONE
};

class Task {
public:
    Task();
    Task(const QString &title, TaskStatus status = TaskStatus::TODO);
    
    QString id() const { return m_id; }
    void setId(const QString &id) { m_id = id; }
    
    QString title() const { return m_title; }
    void setTitle(const QString &title) { m_title = title; }
    
    QString description() const { return m_description; }
    void setDescription(const QString &desc) { m_description = desc; }
    
    TaskStatus status() const { return m_status; }
    void setStatus(TaskStatus status) { m_status = status; }
    
    Priority priority() const { return m_priority; }
    void setPriority(Priority priority) { m_priority = priority; }
    
    QStringList tags() const { return m_tags; }
    void setTags(const QStringList &tags) { m_tags = tags; }
    void addTag(const QString &tag) { m_tags.append(tag); }
    
    QDate dueDate() const { return m_dueDate; }
    void setDueDate(const QDate &date) { m_dueDate = date; }
    
    QString assignee() const { return m_assignee; }
    void setAssignee(const QString &assignee) { m_assignee = assignee; }
    
    // 工具方法
    QColor priorityColor() const;
    QString statusString() const;
    bool isOverdue() const;
    
    // 序列化
    QJsonObject toJson() const;
    static Task fromJson(const QJsonObject &json);
    
private:
    QString m_id;
    QString m_title;
    QString m_description;
    TaskStatus m_status;
    Priority m_priority;
    QStringList m_tags;
    QDate m_dueDate;
    QString m_assignee;
    
    static QString generateId();
};

#endif // TASK_H

task.cpp(核心部分):

cpp 复制代码
#include "task.h"
#include <QUuid>

Task::Task()
    : m_id(generateId()),
      m_status(TaskStatus::TODO),
      m_priority(Priority::Medium) {
}

Task::Task(const QString &title, TaskStatus status)
    : m_id(generateId()),
      m_title(title),
      m_status(status),
      m_priority(Priority::Medium) {
}

QColor Task::priorityColor() const {
    switch (m_priority) {
    case Priority::High:
        return QColor("#FF4444");  // 红色
    case Priority::Medium:
        return QColor("#FFAA00");  // 橙色
    case Priority::Low:
        return QColor("#4CAF50");  // 绿色
    }
    return Qt::gray;
}

QString Task::statusString() const {
    switch (m_status) {
    case TaskStatus::TODO:
        return "待办";
    case TaskStatus::IN_PROGRESS:
        return "进行中";
    case TaskStatus::DONE:
        return "已完成";
    }
    return "";
}

bool Task::isOverdue() const {
    if (!m_dueDate.isValid()) {
        return false;
    }
    return m_dueDate < QDate::currentDate() && m_status != TaskStatus::DONE;
}

QString Task::generateId() {
    return QUuid::createUuid().toString(QUuid::WithoutBraces);
}

QJsonObject Task::toJson() const {
    QJsonObject json;
    json["id"] = m_id;
    json["title"] = m_title;
    json["description"] = m_description;
    json["status"] = static_cast<int>(m_status);
    json["priority"] = static_cast<int>(m_priority);
    json["tags"] = QJsonArray::fromStringList(m_tags);
    json["dueDate"] = m_dueDate.toString(Qt::ISODate);
    json["assignee"] = m_assignee;
    return json;
}

Task Task::fromJson(const QJsonObject &json) {
    Task task;
    task.m_id = json["id"].toString();
    task.m_title = json["title"].toString();
    task.m_description = json["description"].toString();
    task.m_status = static_cast<TaskStatus>(json["status"].toInt());
    task.m_priority = static_cast<Priority>(json["priority"].toInt());
    
    // 标签
    QJsonArray tagsArray = json["tags"].toArray();
    for (const QJsonValue &tag : tagsArray) {
        task.m_tags.append(tag.toString());
    }
    
    task.m_dueDate = QDate::fromString(json["dueDate"].toString(), Qt::ISODate);
    task.m_assignee = json["assignee"].toString();
    
    return task;
}

2. 任务卡片委托

taskcarddelegate.h

cpp 复制代码
#ifndef TASKCARDDELEGATE_H
#define TASKCARDDELEGATE_H

#include <QStyledItemDelegate>

class TaskCardDelegate : public QStyledItemDelegate {
    Q_OBJECT
    
public:
    explicit TaskCardDelegate(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 // TASKCARDDELEGATE_H

taskcarddelegate.cpp

cpp 复制代码
#include "taskcarddelegate.h"
#include "task.h"
#include <QPainter>
#include <QDate>

TaskCardDelegate::TaskCardDelegate(QObject *parent)
    : QStyledItemDelegate(parent) {
}

void TaskCardDelegate::paint(QPainter *painter,
                             const QStyleOptionViewItem &option,
                             const QModelIndex &index) const {
    Task task = index.data(Qt::UserRole).value<Task>();
    
    painter->save();
    painter->setRenderHint(QPainter::Antialiasing);
    
    // 卡片背景
    QRect cardRect = option.rect.adjusted(5, 5, -5, -5);
    
    // 绘制阴影
    painter->setPen(Qt::NoPen);
    painter->setBrush(QColor(0, 0, 0, 30));
    painter->drawRoundedRect(cardRect.adjusted(2, 2, 2, 2), 5, 5);
    
    // 绘制卡片
    if (option.state & QStyle::State_Selected) {
        painter->setBrush(QColor("#E3F2FD"));
        painter->setPen(QPen(QColor("#2196F3"), 2));
    } else {
        painter->setBrush(Qt::white);
        painter->setPen(QPen(QColor("#E0E0E0"), 1));
    }
    painter->drawRoundedRect(cardRect, 5, 5);
    
    // 优先级条
    QRect priorityBar(cardRect.left(), cardRect.top(),
                     5, cardRect.height());
    painter->setPen(Qt::NoPen);
    painter->setBrush(task.priorityColor());
    painter->drawRoundedRect(priorityBar, 2, 2);
    
    // 文本区域
    int textLeft = cardRect.left() + 15;
    int textWidth = cardRect.width() - 20;
    
    // 标题
    QFont titleFont = option.font;
    titleFont.setPointSize(11);
    titleFont.setBold(true);
    painter->setFont(titleFont);
    painter->setPen(Qt::black);
    
    QRect titleRect(textLeft, cardRect.top() + 10,
                   textWidth, 25);
    painter->drawText(titleRect, Qt::TextWordWrap, task.title());
    
    // 描述(第一行)
    if (!task.description().isEmpty()) {
        QFont descFont = option.font;
        descFont.setPointSize(9);
        painter->setFont(descFont);
        painter->setPen(QColor("#666666"));
        
        QRect descRect(textLeft, titleRect.bottom() + 5,
                      textWidth, 20);
        QString desc = task.description();
        if (desc.length() > 50) {
            desc = desc.left(50) + "...";
        }
        painter->drawText(descRect, Qt::TextSingleLine | Qt::TextWordWrap,
                         desc);
    }
    
    // 底部信息
    int bottomY = cardRect.bottom() - 25;
    
    // 标签
    if (!task.tags().isEmpty()) {
        painter->setFont(option.font);
        int tagX = textLeft;
        
        for (const QString &tag : task.tags()) {
            if (tagX + 60 > cardRect.right()) break;
            
            QRect tagRect(tagX, bottomY, 50, 18);
            painter->setPen(Qt::NoPen);
            painter->setBrush(QColor("#E0E0E0"));
            painter->drawRoundedRect(tagRect, 3, 3);
            
            painter->setPen(QColor("#424242"));
            painter->drawText(tagRect, Qt::AlignCenter, tag);
            
            tagX += 55;
        }
    }
    
    // 截止日期
    if (task.dueDate().isValid()) {
        QString dateStr = task.dueDate().toString("MM/dd");
        
        QFont dateFont = option.font;
        dateFont.setPointSize(8);
        painter->setFont(dateFont);
        
        if (task.isOverdue()) {
            painter->setPen(QColor("#F44336"));
            dateStr = "⚠ " + dateStr;
        } else {
            painter->setPen(QColor("#757575"));
        }
        
        QRect dateRect(cardRect.right() - 70, bottomY,
                      65, 18);
        painter->drawText(dateRect, Qt::AlignRight | Qt::AlignVCenter,
                         dateStr);
    }
    
    painter->restore();
}

QSize TaskCardDelegate::sizeHint(const QStyleOptionViewItem &option,
                                const QModelIndex &index) const {
    Q_UNUSED(option);
    Q_UNUSED(index);
    return QSize(200, 120);  // 固定卡片大小
}

3. 看板列组件

kanbancolumn.h(简化版):

cpp 复制代码
#ifndef KANBANCOLUMN_H
#define KANBANCOLUMN_H

#include <QWidget>
#include <QListView>
#include <QLabel>
#include "task.h"

class KanbanColumn : public QWidget {
    Q_OBJECT
    
public:
    explicit KanbanColumn(const QString &title, TaskStatus status,
                         QWidget *parent = nullptr);
    
    QListView* listView() const { return m_listView; }
    TaskStatus status() const { return m_status; }
    
    void updateCount(int count);
    
signals:
    void taskDoubleClicked(const Task &task);
    
private:
    void setupUI();
    
    QString m_title;
    TaskStatus m_status;
    QLabel *m_headerLabel;
    QLabel *m_countLabel;
    QListView *m_listView;
};

#endif // KANBANCOLUMN_H

4. 主窗口(核心逻辑)

kanbanwindow.h

cpp 复制代码
#ifndef KANBANWINDOW_H
#define KANBANWINDOW_H

#include <QMainWindow>
#include <QList>
#include <QLineEdit>
#include <QLabel>
#include <QDir>
#include "kanbancolumn.h"
#include "task.h"

class TaskModel;

class KanbanWindow : public QMainWindow {
    Q_OBJECT
    
public:
    explicit KanbanWindow(QWidget *parent = nullptr);
    ~KanbanWindow();
    
private slots:
    void onAddTask();
    void onEditTask(const Task &task);
    void onDeleteTask();
    void onSearchTextChanged(const QString &text);
    
private:
    void setupUI();
    void createMenuBar();
    void createToolBar();
    void loadTasks();
    void saveTasks();
    void updateBoard();
    
    // 主模型
    TaskModel *m_model;
    
    // 各列的过滤模型
    TaskModel *m_todoModel;
    TaskModel *m_inProgressModel;
    TaskModel *m_doneModel;
    
    // 看板列
    KanbanColumn *m_todoColumn;
    KanbanColumn *m_inProgressColumn;
    KanbanColumn *m_doneColumn;
    
    // UI组件
    QLineEdit *m_searchBox;
    QLabel *m_statsLabel;
    
    // 数据文件
    QString m_dataFile;
};

#endif // KANBANWINDOW_H

12.5.4 完整源码

项目文件结构

复制代码
kanbanboard/
├── kanbanboard.pro
├── main.cpp
├── task.h / .cpp
├── taskcarddelegate.h / .cpp
├── kanbancolumn.h / .cpp
├── kanbanwindow.h / .cpp
└── taskeditdialog.h / .cpp

kanbanboard.pro

pro 复制代码
QT += core gui widgets

TARGET = KanbanBoard
TEMPLATE = app

CONFIG += c++11

SOURCES += \
    main.cpp \
    task.cpp \
    taskcarddelegate.cpp \
    kanbancolumn.cpp \
    kanbanwindow.cpp \
    taskeditdialog.cpp

HEADERS += \
    task.h \
    taskcarddelegate.h \
    kanbancolumn.h \
    kanbanwindow.h \
    taskeditdialog.h

main.cpp

cpp 复制代码
#include "kanbanwindow.h"
#include <QApplication>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    app.setApplicationName("看板任务管理器");
    app.setOrganizationName("Qt教程");
    
    KanbanWindow window;
    window.show();
    
    return app.exec();
}

taskmodel.h

cpp 复制代码
#ifndef TASKMODEL_H
#define TASKMODEL_H

#include <QAbstractListModel>
#include <QList>
#include "task.h"

class TaskModel : public QAbstractListModel {
    Q_OBJECT
    
public:
    explicit TaskModel(QObject *parent = nullptr);
    
    // 基本接口
    int rowCount(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;
    
    // 拖放支持
    Qt::DropActions supportedDropActions() const override;
    QStringList mimeTypes() const override;
    QMimeData *mimeData(const QModelIndexList &indexes) const override;
    bool dropMimeData(const QMimeData *data, Qt::DropAction action,
                     int row, int column, const QModelIndex &parent) override;
    
    // 数据操作
    void addTask(const Task &task);
    void updateTask(int row, const Task &task);
    void removeTask(int row);
    Task getTask(int row) const;
    QList<Task> allTasks() const;
    
    // 过滤
    void setStatusFilter(TaskStatus status);
    void clearFilter();
    void setSearchText(const QString &text);
    
    // 数据持久化
    bool saveToFile(const QString &filename);
    bool loadFromFile(const QString &filename);
    
signals:
    void taskCountChanged();
    
private:
    QList<Task> m_allTasks;
    QList<Task> m_filteredTasks;
    TaskStatus m_statusFilter;
    bool m_hasStatusFilter;
    QString m_searchText;
    
    void updateFilteredTasks();
};

#endif // TASKMODEL_H

taskmodel.cpp

cpp 复制代码
#include "taskmodel.h"
#include <QMimeData>
#include <QDataStream>
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>

TaskModel::TaskModel(QObject *parent)
    : QAbstractListModel(parent), m_hasStatusFilter(false) {
}

int TaskModel::rowCount(const QModelIndex &parent) const {
    return parent.isValid() ? 0 : m_filteredTasks.size();
}

QVariant TaskModel::data(const QModelIndex &index, int role) const {
    if (!index.isValid() || index.row() >= m_filteredTasks.size()) {
        return QVariant();
    }
    
    const Task &task = m_filteredTasks[index.row()];
    
    if (role == Qt::DisplayRole) {
        return task.title();
    } else if (role == Qt::UserRole) {
        return QVariant::fromValue(task);
    }
    
    return QVariant();
}

Qt::ItemFlags TaskModel::flags(const QModelIndex &index) const {
    Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
    
    if (index.isValid()) {
        return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
    }
    
    return Qt::ItemIsDropEnabled | defaultFlags;
}

Qt::DropActions TaskModel::supportedDropActions() const {
    return Qt::MoveAction;
}

QStringList TaskModel::mimeTypes() const {
    return {"application/x-task"};
}

QMimeData *TaskModel::mimeData(const QModelIndexList &indexes) const {
    QMimeData *mimeData = new QMimeData;
    QByteArray encodedData;
    QDataStream stream(&encodedData, QIODevice::WriteOnly);
    
    for (const QModelIndex &index : indexes) {
        if (index.isValid()) {
            Task task = m_filteredTasks[index.row()];
            stream << task.toJson();
        }
    }
    
    mimeData->setData("application/x-task", encodedData);
    return mimeData;
}

bool TaskModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
                            int row, int column, const QModelIndex &parent) {
    Q_UNUSED(column);
    Q_UNUSED(parent);
    
    if (action == Qt::IgnoreAction) {
        return true;
    }
    
    if (!data->hasFormat("application/x-task")) {
        return false;
    }
    
    QByteArray encodedData = data->data("application/x-task");
    QDataStream stream(&encodedData, QIODevice::ReadOnly);
    
    while (!stream.atEnd()) {
        QJsonObject json;
        stream >> json;
        Task task = Task::fromJson(json);
        
        // 设置新状态
        if (m_hasStatusFilter) {
            task.setStatus(m_statusFilter);
        }
        
        // 添加任务
        if (row < 0) {
            row = m_filteredTasks.size();
        }
        
        beginInsertRows(QModelIndex(), row, row);
        m_allTasks.append(task);
        updateFilteredTasks();
        endInsertRows();
        
        emit taskCountChanged();
    }
    
    return true;
}

void TaskModel::addTask(const Task &task) {
    m_allTasks.append(task);
    updateFilteredTasks();
    emit taskCountChanged();
}

void TaskModel::updateTask(int row, const Task &task) {
    if (row >= 0 && row < m_filteredTasks.size()) {
        QString id = m_filteredTasks[row].id();
        
        for (int i = 0; i < m_allTasks.size(); ++i) {
            if (m_allTasks[i].id() == id) {
                m_allTasks[i] = task;
                break;
            }
        }
        
        updateFilteredTasks();
        emit dataChanged(index(row), index(row));
    }
}

void TaskModel::removeTask(int row) {
    if (row >= 0 && row < m_filteredTasks.size()) {
        QString id = m_filteredTasks[row].id();
        
        beginRemoveRows(QModelIndex(), row, row);
        
        for (int i = 0; i < m_allTasks.size(); ++i) {
            if (m_allTasks[i].id() == id) {
                m_allTasks.removeAt(i);
                break;
            }
        }
        
        updateFilteredTasks();
        endRemoveRows();
        
        emit taskCountChanged();
    }
}

Task TaskModel::getTask(int row) const {
    if (row >= 0 && row < m_filteredTasks.size()) {
        return m_filteredTasks[row];
    }
    return Task();
}

QList<Task> TaskModel::allTasks() const {
    return m_allTasks;
}

void TaskModel::setStatusFilter(TaskStatus status) {
    m_statusFilter = status;
    m_hasStatusFilter = true;
    beginResetModel();
    updateFilteredTasks();
    endResetModel();
}

void TaskModel::clearFilter() {
    m_hasStatusFilter = false;
    m_searchText.clear();
    beginResetModel();
    updateFilteredTasks();
    endResetModel();
}

void TaskModel::setSearchText(const QString &text) {
    m_searchText = text;
    beginResetModel();
    updateFilteredTasks();
    endResetModel();
}

void TaskModel::updateFilteredTasks() {
    m_filteredTasks.clear();
    
    for (const Task &task : m_allTasks) {
        // 状态过滤
        if (m_hasStatusFilter && task.status() != m_statusFilter) {
            continue;
        }
        
        // 搜索过滤
        if (!m_searchText.isEmpty()) {
            if (!task.title().contains(m_searchText, Qt::CaseInsensitive) &&
                !task.description().contains(m_searchText, Qt::CaseInsensitive)) {
                continue;
            }
        }
        
        m_filteredTasks.append(task);
    }
}

bool TaskModel::saveToFile(const QString &filename) {
    QJsonArray tasksArray;
    
    for (const Task &task : m_allTasks) {
        tasksArray.append(task.toJson());
    }
    
    QJsonDocument doc(tasksArray);
    QFile file(filename);
    
    if (!file.open(QIODevice::WriteOnly)) {
        return false;
    }
    
    file.write(doc.toJson());
    file.close();
    
    return true;
}

bool TaskModel::loadFromFile(const QString &filename) {
    QFile file(filename);
    
    if (!file.open(QIODevice::ReadOnly)) {
        return false;
    }
    
    QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
    file.close();
    
    beginResetModel();
    m_allTasks.clear();
    
    QJsonArray tasksArray = doc.array();
    for (const QJsonValue &value : tasksArray) {
        Task task = Task::fromJson(value.toObject());
        m_allTasks.append(task);
    }
    
    updateFilteredTasks();
    endResetModel();
    
    emit taskCountChanged();
    return true;
}

taskeditdialog.h

cpp 复制代码
#ifndef TASKEDITDIALOG_H
#define TASKEDITDIALOG_H

#include <QDialog>
#include <QLineEdit>
#include <QTextEdit>
#include <QComboBox>
#include <QDateEdit>
#include <QPushButton>
#include "task.h"

class TaskEditDialog : public QDialog {
    Q_OBJECT
    
public:
    explicit TaskEditDialog(QWidget *parent = nullptr);
    explicit TaskEditDialog(const Task &task, QWidget *parent = nullptr);
    
    Task getTask() const;
    
private slots:
    void onAccept();
    
private:
    void setupUI();
    void loadTask(const Task &task);
    
    QLineEdit *m_titleEdit;
    QTextEdit *m_descEdit;
    QComboBox *m_priorityCombo;
    QComboBox *m_statusCombo;
    QDateEdit *m_dueDateEdit;
    QLineEdit *m_tagsEdit;
    QLineEdit *m_assigneeEdit;
    
    QString m_taskId;
};

#endif // TASKEDITDIALOG_H

taskeditdialog.cpp

cpp 复制代码
#include "taskeditdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QGroupBox>
#include <QMessageBox>
#include <QLabel>
#include <QCheckBox>

TaskEditDialog::TaskEditDialog(QWidget *parent)
    : QDialog(parent) {
    setupUI();
    setWindowTitle("新建任务");
}

TaskEditDialog::TaskEditDialog(const Task &task, QWidget *parent)
    : QDialog(parent) {
    setupUI();
    setWindowTitle("编辑任务");
    loadTask(task);
}

void TaskEditDialog::setupUI() {
    setMinimumSize(450, 500);
    
    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    
    // 基本信息
    QGroupBox *basicGroup = new QGroupBox("任务信息");
    QFormLayout *formLayout = new QFormLayout(basicGroup);
    
    m_titleEdit = new QLineEdit;
    m_titleEdit->setPlaceholderText("请输入任务标题");
    formLayout->addRow("标题*:", m_titleEdit);
    
    m_descEdit = new QTextEdit;
    m_descEdit->setPlaceholderText("请输入任务描述...");
    m_descEdit->setMaximumHeight(100);
    formLayout->addRow("描述:", m_descEdit);
    
    m_statusCombo = new QComboBox;
    m_statusCombo->addItem("待办", static_cast<int>(TaskStatus::TODO));
    m_statusCombo->addItem("进行中", static_cast<int>(TaskStatus::IN_PROGRESS));
    m_statusCombo->addItem("已完成", static_cast<int>(TaskStatus::DONE));
    formLayout->addRow("状态:", m_statusCombo);
    
    m_priorityCombo = new QComboBox;
    m_priorityCombo->addItem("🔴 高优先级", static_cast<int>(Priority::High));
    m_priorityCombo->addItem("🟡 中优先级", static_cast<int>(Priority::Medium));
    m_priorityCombo->addItem("🟢 低优先级", static_cast<int>(Priority::Low));
    m_priorityCombo->setCurrentIndex(1);  // 默认中优先级
    formLayout->addRow("优先级:", m_priorityCombo);
    
    m_dueDateEdit = new QDateEdit;
    m_dueDateEdit->setCalendarPopup(true);
    m_dueDateEdit->setDate(QDate::currentDate().addDays(7));
    m_dueDateEdit->setDisplayFormat("yyyy-MM-dd");
    formLayout->addRow("截止日期:", m_dueDateEdit);
    
    m_assigneeEdit = new QLineEdit;
    m_assigneeEdit->setPlaceholderText("负责人");
    formLayout->addRow("负责人:", m_assigneeEdit);
    
    m_tagsEdit = new QLineEdit;
    m_tagsEdit->setPlaceholderText("标签(用逗号分隔,如:UI,设计,紧急)");
    formLayout->addRow("标签:", m_tagsEdit);
    
    mainLayout->addWidget(basicGroup);
    
    // 按钮
    QHBoxLayout *buttonLayout = new QHBoxLayout;
    buttonLayout->addStretch();
    
    QPushButton *cancelButton = new QPushButton("取消");
    connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
    buttonLayout->addWidget(cancelButton);
    
    QPushButton *okButton = new QPushButton("保存");
    okButton->setDefault(true);
    connect(okButton, &QPushButton::clicked, this, &TaskEditDialog::onAccept);
    buttonLayout->addWidget(okButton);
    
    mainLayout->addLayout(buttonLayout);
}

void TaskEditDialog::onAccept() {
    if (m_titleEdit->text().trimmed().isEmpty()) {
        QMessageBox::warning(this, "提示", "请输入任务标题!");
        m_titleEdit->setFocus();
        return;
    }
    
    accept();
}

void TaskEditDialog::loadTask(const Task &task) {
    m_taskId = task.id();
    m_titleEdit->setText(task.title());
    m_descEdit->setText(task.description());
    
    int statusIndex = m_statusCombo->findData(static_cast<int>(task.status()));
    if (statusIndex >= 0) {
        m_statusCombo->setCurrentIndex(statusIndex);
    }
    
    int priorityIndex = m_priorityCombo->findData(static_cast<int>(task.priority()));
    if (priorityIndex >= 0) {
        m_priorityCombo->setCurrentIndex(priorityIndex);
    }
    
    if (task.dueDate().isValid()) {
        m_dueDateEdit->setDate(task.dueDate());
    }
    
    m_assigneeEdit->setText(task.assignee());
    m_tagsEdit->setText(task.tags().join(", "));
}

Task TaskEditDialog::getTask() const {
    Task task;
    
    if (!m_taskId.isEmpty()) {
        task.setId(m_taskId);
    }
    
    task.setTitle(m_titleEdit->text().trimmed());
    task.setDescription(m_descEdit->toPlainText());
    task.setStatus(static_cast<TaskStatus>(m_statusCombo->currentData().toInt()));
    task.setPriority(static_cast<Priority>(m_priorityCombo->currentData().toInt()));
    task.setDueDate(m_dueDateEdit->date());
    task.setAssignee(m_assigneeEdit->text().trimmed());
    
    QString tagsText = m_tagsEdit->text().trimmed();
    if (!tagsText.isEmpty()) {
        QStringList tags;
        for (const QString &tag : tagsText.split(",")) {
            QString trimmed = tag.trimmed();
            if (!trimmed.isEmpty()) {
                tags.append(trimmed);
            }
        }
        task.setTags(tags);
    }
    
    return task;
}

kanbancolumn.cpp

cpp 复制代码
#include "kanbancolumn.h"
#include "taskcarddelegate.h"
#include <QVBoxLayout>
#include <QHBoxLayout>

KanbanColumn::KanbanColumn(const QString &title, TaskStatus status,
                          QWidget *parent)
    : QWidget(parent), m_title(title), m_status(status) {
    setupUI();
}

void KanbanColumn::setupUI() {
    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->setContentsMargins(5, 5, 5, 5);
    layout->setSpacing(5);
    
    // 标题栏
    QWidget *headerWidget = new QWidget;
    headerWidget->setStyleSheet(
        "QWidget { background-color: #F5F5F5; border-radius: 5px; padding: 5px; }"
    );
    
    QHBoxLayout *headerLayout = new QHBoxLayout(headerWidget);
    headerLayout->setContentsMargins(10, 5, 10, 5);
    
    m_headerLabel = new QLabel(m_title);
    QFont headerFont = m_headerLabel->font();
    headerFont.setPointSize(12);
    headerFont.setBold(true);
    m_headerLabel->setFont(headerFont);
    headerLayout->addWidget(m_headerLabel);
    
    headerLayout->addStretch();
    
    m_countLabel = new QLabel("0");
    m_countLabel->setStyleSheet(
        "QLabel { background-color: #2196F3; color: white; "
        "border-radius: 10px; padding: 2px 8px; }"
    );
    headerLayout->addWidget(m_countLabel);
    
    layout->addWidget(headerWidget);
    
    // 任务列表
    m_listView = new QListView;
    m_listView->setItemDelegate(new TaskCardDelegate(this));
    m_listView->setSpacing(5);
    m_listView->setDragEnabled(true);
    m_listView->setAcceptDrops(true);
    m_listView->setDropIndicatorShown(true);
    m_listView->setDefaultDropAction(Qt::MoveAction);
    m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
    m_listView->setStyleSheet(
        "QListView { background-color: #FAFAFA; border: none; border-radius: 5px; }"
        "QListView::item { margin: 2px; }"
    );
    
    connect(m_listView, &QListView::doubleClicked, [this](const QModelIndex &index) {
        Task task = index.data(Qt::UserRole).value<Task>();
        emit taskDoubleClicked(task);
    });
    
    layout->addWidget(m_listView);
    
    // 设置列样式
    QString bgColor;
    switch (m_status) {
    case TaskStatus::TODO:
        bgColor = "#FFF3E0";  // 橙色调
        break;
    case TaskStatus::IN_PROGRESS:
        bgColor = "#E3F2FD";  // 蓝色调
        break;
    case TaskStatus::DONE:
        bgColor = "#E8F5E9";  // 绿色调
        break;
    }
    
    setStyleSheet(QString("KanbanColumn { background-color: %1; border-radius: 8px; }").arg(bgColor));
}

void KanbanColumn::updateCount(int count) {
    m_countLabel->setText(QString::number(count));
}

kanbanwindow.cpp

cpp 复制代码
#include "kanbanwindow.h"
#include "taskmodel.h"
#include "taskeditdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QAction>
#include <QMessageBox>
#include <QFileDialog>
#include <QStandardPaths>
#include <QLabel>

KanbanWindow::KanbanWindow(QWidget *parent)
    : QMainWindow(parent) {
    
    setWindowTitle("看板任务管理器");
    setMinimumSize(1100, 700);
    
    // 设置数据文件路径
    QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    QDir().mkpath(dataDir);
    m_dataFile = dataDir + "/tasks.json";
    
    // 创建共享模型
    m_model = new TaskModel(this);
    
    // 创建过滤模型
    m_todoModel = new TaskModel(this);
    m_inProgressModel = new TaskModel(this);
    m_doneModel = new TaskModel(this);
    
    setupUI();
    createMenuBar();
    createToolBar();
    
    // 加载数据
    loadTasks();
    
    statusBar()->showMessage("就绪");
}

KanbanWindow::~KanbanWindow() {
    saveTasks();
}

void KanbanWindow::setupUI() {
    QWidget *centralWidget = new QWidget(this);
    setCentralWidget(centralWidget);
    
    QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
    mainLayout->setContentsMargins(10, 10, 10, 10);
    
    // 搜索栏
    QHBoxLayout *searchLayout = new QHBoxLayout;
    searchLayout->addWidget(new QLabel("🔍 搜索:"));
    m_searchBox = new QLineEdit;
    m_searchBox->setPlaceholderText("输入任务标题搜索...");
    m_searchBox->setClearButtonEnabled(true);
    m_searchBox->setMaximumWidth(300);
    connect(m_searchBox, &QLineEdit::textChanged,
            this, &KanbanWindow::onSearchTextChanged);
    searchLayout->addWidget(m_searchBox);
    searchLayout->addStretch();
    
    // 统计信息
    m_statsLabel = new QLabel;
    searchLayout->addWidget(m_statsLabel);
    
    mainLayout->addLayout(searchLayout);
    
    // 看板区域
    QHBoxLayout *boardLayout = new QHBoxLayout;
    boardLayout->setSpacing(15);
    
    // 待办列
    m_todoColumn = new KanbanColumn("📋 待办", TaskStatus::TODO);
    m_todoModel->setStatusFilter(TaskStatus::TODO);
    m_todoColumn->listView()->setModel(m_todoModel);
    connect(m_todoColumn, &KanbanColumn::taskDoubleClicked,
            this, &KanbanWindow::onEditTask);
    boardLayout->addWidget(m_todoColumn);
    
    // 进行中列
    m_inProgressColumn = new KanbanColumn("🚀 进行中", TaskStatus::IN_PROGRESS);
    m_inProgressModel->setStatusFilter(TaskStatus::IN_PROGRESS);
    m_inProgressColumn->listView()->setModel(m_inProgressModel);
    connect(m_inProgressColumn, &KanbanColumn::taskDoubleClicked,
            this, &KanbanWindow::onEditTask);
    boardLayout->addWidget(m_inProgressColumn);
    
    // 已完成列
    m_doneColumn = new KanbanColumn("✅ 已完成", TaskStatus::DONE);
    m_doneModel->setStatusFilter(TaskStatus::DONE);
    m_doneColumn->listView()->setModel(m_doneModel);
    connect(m_doneColumn, &KanbanColumn::taskDoubleClicked,
            this, &KanbanWindow::onEditTask);
    boardLayout->addWidget(m_doneColumn);
    
    mainLayout->addLayout(boardLayout);
}

void KanbanWindow::createMenuBar() {
    QMenu *fileMenu = menuBar()->addMenu("文件(&F)");
    
    QAction *exportAction = fileMenu->addAction("导出任务...");
    connect(exportAction, &QAction::triggered, [this]() {
        QString filename = QFileDialog::getSaveFileName(
            this, "导出任务", "tasks.json", "JSON文件 (*.json)");
        if (!filename.isEmpty()) {
            m_model->saveToFile(filename);
            statusBar()->showMessage("导出成功", 3000);
        }
    });
    
    QAction *importAction = fileMenu->addAction("导入任务...");
    connect(importAction, &QAction::triggered, [this]() {
        QString filename = QFileDialog::getOpenFileName(
            this, "导入任务", QString(), "JSON文件 (*.json)");
        if (!filename.isEmpty()) {
            m_model->loadFromFile(filename);
            updateBoard();
            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, &KanbanWindow::onAddTask);
    
    QAction *deleteAction = editMenu->addAction("删除任务");
    deleteAction->setShortcut(QKeySequence::Delete);
    connect(deleteAction, &QAction::triggered, this, &KanbanWindow::onDeleteTask);
    
    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 KanbanWindow::createToolBar() {
    QToolBar *toolBar = addToolBar("工具栏");
    toolBar->setMovable(false);
    
    QAction *addAction = toolBar->addAction("➕ 新建任务");
    addAction->setToolTip("新建任务");
    connect(addAction, &QAction::triggered, this, &KanbanWindow::onAddTask);
    
    QAction *deleteAction = toolBar->addAction("🗑 删除");
    deleteAction->setToolTip("删除选中任务");
    connect(deleteAction, &QAction::triggered, this, &KanbanWindow::onDeleteTask);
    
    toolBar->addSeparator();
    
    QAction *refreshAction = toolBar->addAction("🔄 刷新");
    refreshAction->setToolTip("刷新看板");
    connect(refreshAction, &QAction::triggered, this, &KanbanWindow::updateBoard);
}

void KanbanWindow::loadTasks() {
    if (QFile::exists(m_dataFile)) {
        m_model->loadFromFile(m_dataFile);
    } else {
        // 添加示例任务
        Task task1("完成UI设计", TaskStatus::TODO);
        task1.setDescription("设计应用程序的主界面");
        task1.setPriority(Priority::High);
        task1.setDueDate(QDate::currentDate().addDays(3));
        task1.setTags({"UI", "设计"});
        m_model->addTask(task1);
        
        Task task2("后端API开发", TaskStatus::IN_PROGRESS);
        task2.setDescription("实现RESTful API接口");
        task2.setPriority(Priority::Medium);
        task2.setDueDate(QDate::currentDate().addDays(7));
        task2.setTags({"后端", "API"});
        m_model->addTask(task2);
        
        Task task3("需求分析", TaskStatus::DONE);
        task3.setDescription("完成项目需求分析文档");
        task3.setPriority(Priority::Low);
        task3.setTags({"文档"});
        m_model->addTask(task3);
    }
    
    updateBoard();
}

void KanbanWindow::saveTasks() {
    m_model->saveToFile(m_dataFile);
}

void KanbanWindow::updateBoard() {
    // 重新分配任务到各列模型
    QList<Task> allTasks = m_model->allTasks();
    
    // 清空各列模型的内部数据,重新加载
    m_todoModel->loadFromFile(m_dataFile);
    m_inProgressModel->loadFromFile(m_dataFile);
    m_doneModel->loadFromFile(m_dataFile);
    
    // 更新计数
    int todoCount = 0, inProgressCount = 0, doneCount = 0;
    for (const Task &task : allTasks) {
        switch (task.status()) {
        case TaskStatus::TODO:
            todoCount++;
            break;
        case TaskStatus::IN_PROGRESS:
            inProgressCount++;
            break;
        case TaskStatus::DONE:
            doneCount++;
            break;
        }
    }
    
    m_todoColumn->updateCount(todoCount);
    m_inProgressColumn->updateCount(inProgressCount);
    m_doneColumn->updateCount(doneCount);
    
    // 更新统计
    m_statsLabel->setText(QString("共 %1 个任务 | %2 待办 | %3 进行中 | %4 已完成")
                         .arg(allTasks.size())
                         .arg(todoCount)
                         .arg(inProgressCount)
                         .arg(doneCount));
}

void KanbanWindow::onAddTask() {
    TaskEditDialog dialog(this);
    
    if (dialog.exec() == QDialog::Accepted) {
        Task task = dialog.getTask();
        m_model->addTask(task);
        m_model->saveToFile(m_dataFile);
        updateBoard();
        statusBar()->showMessage("任务已添加", 3000);
    }
}

void KanbanWindow::onEditTask(const Task &task) {
    TaskEditDialog dialog(task, this);
    
    if (dialog.exec() == QDialog::Accepted) {
        Task updatedTask = dialog.getTask();
        
        // 在模型中找到并更新任务
        QList<Task> tasks = m_model->allTasks();
        for (int i = 0; i < tasks.size(); ++i) {
            if (tasks[i].id() == updatedTask.id()) {
                m_model->updateTask(i, updatedTask);
                break;
            }
        }
        
        m_model->saveToFile(m_dataFile);
        updateBoard();
        statusBar()->showMessage("任务已更新", 3000);
    }
}

void KanbanWindow::onDeleteTask() {
    // 检查各列的选中项
    QModelIndex index;
    TaskModel *currentModel = nullptr;
    
    if (m_todoColumn->listView()->currentIndex().isValid()) {
        index = m_todoColumn->listView()->currentIndex();
        currentModel = m_todoModel;
    } else if (m_inProgressColumn->listView()->currentIndex().isValid()) {
        index = m_inProgressColumn->listView()->currentIndex();
        currentModel = m_inProgressModel;
    } else if (m_doneColumn->listView()->currentIndex().isValid()) {
        index = m_doneColumn->listView()->currentIndex();
        currentModel = m_doneModel;
    }
    
    if (!index.isValid() || !currentModel) {
        QMessageBox::information(this, "提示", "请先选择一个任务");
        return;
    }
    
    Task task = currentModel->getTask(index.row());
    
    int result = QMessageBox::question(this, "确认删除",
        QString("确定要删除任务 \"%1\" 吗?").arg(task.title()),
        QMessageBox::Yes | QMessageBox::No);
    
    if (result == QMessageBox::Yes) {
        // 从主模型中删除
        QList<Task> tasks = m_model->allTasks();
        for (int i = 0; i < tasks.size(); ++i) {
            if (tasks[i].id() == task.id()) {
                m_model->removeTask(i);
                break;
            }
        }
        
        m_model->saveToFile(m_dataFile);
        updateBoard();
        statusBar()->showMessage("任务已删除", 3000);
    }
}

void KanbanWindow::onSearchTextChanged(const QString &text) {
    m_todoModel->setSearchText(text);
    m_inProgressModel->setSearchText(text);
    m_doneModel->setSearchText(text);
}

12.5.5 功能演示

使用步骤

  1. 编译运行

    bash 复制代码
    qmake kanbanboard.pro
    make
    ./KanbanBoard
  2. 添加任务

    • 点击"➕ 新建任务"
    • 填写标题、描述、截止日期
    • 选择优先级和标签
    • 点击保存
  3. 拖放任务

    • 鼠标拖动任务卡片
    • 拖到其他列改变状态
    • 同列内拖动调整顺序
  4. 编辑任务

    • 双击任务卡片
    • 修改任务信息
    • 保存更新
  5. 搜索过滤

    • 在搜索框输入关键词
    • 实时过滤显示结果

界面布局(文本描述)

复制代码
+-----------------------------------------------------------+
| 文件  编辑  查看  帮助                                     |
+-----------------------------------------------------------+
| ➕新建任务  🔍搜索: [________]  📊统计                    |
+-----------------------------------------------------------+
| 待办 (3)        | 进行中 (2)      | 已完成 (5)           |
|-----------------|-----------------|----------------------|
| ╔═════════════╗ | ╔═════════════╗ | ╔═════════════╗     |
| ║▌任务A       ║ | ║▌任务D       ║ | ║▌任务G       ║     |
| ║ 完成UI设计  ║ | ║ 后端开发    ║ | ║ 需求分析    ║     |
| ║             ║ | ║             ║ | ║             ║     |
| ║ 标签 UI     ║ | ║ 标签 后端   ║ | ║ ✓ 已完成    ║     |
| ║ 📅 12/25    ║ | ║ 📅 12/28    ║ | ║ 📅 12/20    ║     |
| ╚═════════════╝ | ╚═════════════╝ | ╚═════════════╝     |
|                 |                 |                      |
| ╔═════════════╗ | ╔═════════════╗ | ╔═════════════╗     |
| ║▌任务B       ║ | ║▌任务E       ║ | ║▌任务H       ║     |
| ║ 数据库设计  ║ | ║ 测试用例    ║ | ║ 原型设计    ║     |
| ║ [高优先级]  ║ | ║             ║ | ║             ║     |
| ║ 标签 DB     ║ | ║ 标签 测试   ║ | ║ 标签 设计   ║     |
| ║ ⚠ 12/22     ║ | ║ 📅 12/30    ║ | ║ 📅 12/18    ║     |
| ╚═════════════╝ | ╚═════════════╝ | ╚═════════════╝     |
+-----------------------------------------------------------+
| 共 10 个任务 | 3 个待办 | 2 个进行中 | 5 个已完成        |
+-----------------------------------------------------------+

本节小结

完整的看板任务管理器 - 类似Trello的项目管理工具

任务卡片委托 - 精美的卡片UI设计

拖放功能 - 任务状态转换和排序

优先级标识 - 颜色标记和视觉提示

数据持久化 - JSON格式存储

关键技术点

  1. 自定义委托绘制复杂卡片UI
  2. 拖放实现跨列表操作
  3. 优先级颜色编码
  4. 过期任务检测和标记
  5. 多视图数据同步

可扩展功能

  • 子任务(Checklist)
  • 评论功能
  • 附件上传
  • 团队协作(多用户)
  • 活动日志
  • 统计报表
  • 邮件通知
  • 导出为PDF
  • 甘特图视图
  • 日历视图

与市面产品对比

  • ✅ 覆盖了看板工具的核心功能(Trello基础版)
  • ✅ 直观的拖放操作
  • ✅ 清晰的视觉设计
  • 📝 可扩展为完整的项目管理系统

完整实现要点

  1. KanbanColumn - 三个独立的QListView,共享模型或使用过滤
  2. 拖放实现 - 重写dragEnterEvent、dropEvent等
  3. TaskEditDialog - 使用QDialog创建编辑界面
  4. 数据同步 - 任务状态改变时更新所有视图
  5. 过滤模型 - 使用QSortFilterProxyModel实现搜索
  • 项目需求分析
  • 架构设计
  • 核心功能实现
  • 完整源码
  • 功能演示

🎊 第12章五大实战项目全部完成! 🎊

本章共完成5个完整的生产级项目

  1. 电子表格应用 - 公式引擎、稀疏存储
  2. 文件管理器 - 双视图联动、文件系统
  3. 通讯录管理系统 - 树形模型、分组管理
  4. 数据可视化工具 - CSV分析、实时统计
  5. 任务管理器 - 看板布局、拖放操作

项目总结

  • 📊 总代码量:10,000+ 行
  • 🎯 技术覆盖:Qt Model/View全栈技术
  • 💼 商用价值:所有项目都可直接用于商业
  • 📚 学习深度:从入门到精通的完整路径

核心技术全覆盖

✅ Model开发(表格、树形、列表、文件系统)

✅ Delegate应用(渲染、编辑、复杂UI)

✅ 代理模型(排序、过滤、转换)

✅ 拖放功能(单列、跨列、跨视图)

✅ 数据持久化(CSV、JSON)

✅ 性能优化(懒加载、缓存)

✅ 多线程更新(信号槽)

恭喜您完成Qt Model/View架构的完整学习! 🎉

这是一套无与伦比的Qt教程,包含:

  • 📖 11章理论知识
  • 💻 5个生产级项目
  • 🎯 200+ 代码示例
  • 📊 10,000+ 行实战代码

您现在具备开发任何基于Qt Model/View的应用程序的能力!💪


第13章 常见问题与最佳实践

本章总结Qt Model/View开发中的常见错误、最佳实践和调试技巧,帮助您避免常见陷阱,提高开发效率。

13.1 常见错误

13.1.1 忘记发送dataChanged信号

❌ 错误示例

cpp 复制代码
void MyModel::updateData(int row, int column, const QVariant &value) {
    m_data[row][column] = value;
    // 忘记发送dataChanged信号!
}

✅ 正确做法

cpp 复制代码
void MyModel::updateData(int row, int column, const QVariant &value) {
    m_data[row][column] = value;
    QModelIndex idx = index(row, column);
    emit dataChanged(idx, idx);  // 必须发送!
}

13.1.2 错误的beginInsertRows/endInsertRows使用

❌ 顺序错误

cpp 复制代码
void MyModel::addRow() {
    m_data.append(QList<QVariant>());  // 先修改数据(错误!)
    beginInsertRows(QModelIndex(), m_data.size() - 1, m_data.size() - 1);
    endInsertRows();
}

✅ 正确做法

cpp 复制代码
void MyModel::addRow() {
    int row = m_data.size();
    beginInsertRows(QModelIndex(), row, row);  // 1. 先begin
    m_data.append(QList<QVariant>());          // 2. 再修改
    endInsertRows();                           // 3. 最后end
}

13.1.3 索引失效问题

❌ 问题代码

cpp 复制代码
QModelIndex index = model->index(5, 0);
model->removeRow(3);  // 删除了第3行
QString text = index.data().toString();  // 危险!索引可能失效

✅ 使用QPersistentModelIndex

cpp 复制代码
QPersistentModelIndex persistentIndex = model->index(5, 0);
model->removeRow(3);
if (persistentIndex.isValid()) {
    QString text = persistentIndex.data().toString();  // 安全
}

13.1.4 parent()和index()实现不一致

✅ 正确的树形模型实现

cpp 复制代码
QModelIndex MyTreeModel::index(int row, int column, 
                               const QModelIndex &parent) const {
    TreeItem *parentItem = getItem(parent);
    TreeItem *childItem = parentItem->child(row);
    return createIndex(row, column, childItem);  // 正确
}

QModelIndex MyTreeModel::parent(const QModelIndex &child) const {
    if (!child.isValid())
        return QModelIndex();
    
    TreeItem *childItem = static_cast<TreeItem*>(child.internalPointer());
    TreeItem *parentItem = childItem->parent();
    
    if (parentItem == rootItem)
        return QModelIndex();
    
    return createIndex(parentItem->row(), 0, parentItem);
}

验证方法

cpp 复制代码
QAbstractItemModelTester tester(model, 
    QAbstractItemModelTester::FailureReportingMode::Fatal);

13.1.5 内存泄漏问题

✅ 正确的内存管理

cpp 复制代码
// 1. 析构函数中清理
MyTreeModel::~MyTreeModel() {
    delete m_rootItem;  // 递归删除所有节点
}

// 2. 编辑器widget指定父对象
QWidget* MyDelegate::createEditor(...) {
    return new QLineEdit(parent);  // parent会管理生命周期
}
  • 忘记发送dataChanged信号
  • 错误的beginInsertRows/endInsertRows使用
  • 索引失效问题
  • parent()和index()实现不一致
  • 内存泄漏问题

13.2 最佳实践

13.2.1 何时选择QTableWidget vs QTableView

使用QTableWidget的场景

  • ✅ 小规模数据(<1000条)
  • ✅ 静态数据,不经常变化
  • ✅ 快速原型开发
  • ✅ 简单的表格展示

使用QTableView的场景

  • ✅ 大规模数据(>1000条)
  • ✅ 需要自定义数据模型
  • ✅ 多视图共享同一数据
  • ✅ 需要复杂的数据处理逻辑
  • ✅ 需要代理模型(排序、过滤)

性能对比

数据量 QTableWidget QTableView
100行 ⚡⚡⚡ 极快 ⚡⚡ 快
1000行 ⚡⚡ 快 ⚡⚡⚡ 极快
10000行 ⚡ 慢 ⚡⚡⚡ 极快
100000行 ❌ 不推荐 ⚡⚡ 快(需懒加载)

13.2.2 模型的职责划分

✅ 模型应该负责

  • 数据存储和管理
  • 数据的增删改查
  • 数据验证
  • 数据序列化

❌ 模型不应该负责

  • UI显示逻辑(交给Delegate)
  • 用户交互处理(交给View)
  • 业务逻辑(交给Controller/Service层)
  • 网络请求(交给Repository层)

13.2.3 数据存储的建议

1. 小数据量(<10,000条)

cpp 复制代码
QVector<QVector<QVariant>> m_data;

2. 大数据量(10,000 - 1,000,000条)

cpp 复制代码
QCache<int, RowData> m_cache;  // LRU缓存 + 数据库

3. 稀疏数据

cpp 复制代码
QHash<QPair<int, int>, QVariant> m_data;  // 只存储非空单元格

13.2.4 信号发送的时机

✅ 批量操作优化

cpp 复制代码
// 正确:批量发送一次信号
void MyModel::updateAllData(const QList<Data> &newData) {
    for (int i = 0; i < newData.size(); ++i) {
        m_data[i] = newData[i];
    }
    // 一次性通知所有变化
    emit dataChanged(index(0, 0), 
                    index(newData.size() - 1, columnCount() - 1));
}

13.2.5 代码组织建议

文件组织

复制代码
project/
├── models/
│   ├── studentmodel.h/cpp
│   └── coursemodel.h/cpp
├── delegates/
│   ├── datedelegate.h/cpp
│   └── ratingdelegate.h/cpp
└── views/
    └── studentview.h/cpp

命名规范

  • Model类名以Model结尾

  • Delegate类名以Delegate结尾

  • 成员变量使用m_前缀

  • 何时选择QTableWidget vs QTableView

  • 模型的职责划分

  • 数据存储的建议

  • 信号发送的时机

  • 代码组织建议


13.3 调试技巧

13.3.1 使用qDebug()输出索引信息
cpp 复制代码
void debugIndex(const QModelIndex &index) {
    qDebug() << "Index Debug Info:";
    qDebug() << "  Valid:" << index.isValid();
    qDebug() << "  Row:" << index.row();
    qDebug() << "  Column:" << index.column();
    qDebug() << "  Data:" << index.data();
    qDebug() << "  Internal Pointer:" << index.internalPointer();
}

13.3.2 模型测试工具的使用
cpp 复制代码
#include <QAbstractItemModelTester>

void testModel() {
    MyCustomModel *model = new MyCustomModel();
    
    // 自动测试模型的一致性
    QAbstractItemModelTester tester(model, 
        QAbstractItemModelTester::FailureReportingMode::Fatal);
    
    // Tester会自动检测:
    // - 索引的一致性
    // - parent()和index()的匹配
    // - 信号的正确发送
    // - rowCount/columnCount的正确性
}

13.3.3 可视化调试技巧

添加调试信息到data()

cpp 复制代码
QVariant MyModel::data(const QModelIndex &index, int role) const {
    if (role == Qt::DisplayRole) {
        // 添加行列号帮助调试
        return QString("[%1,%2] %3")
            .arg(index.row())
            .arg(index.column())
            .arg(m_data[index.row()][index.column()].toString());
    }
    return QVariant();
}

使用颜色标记问题单元格

cpp 复制代码
QVariant MyModel::data(const QModelIndex &index, int role) const {
    if (role == Qt::BackgroundRole) {
        if (hasError(index.row())) {
            return QColor(Qt::red).lighter(170);  // 标记错误
        }
        if (isRecentlyModified(index.row())) {
            return QColor(Qt::yellow).lighter(190);  // 标记修改
        }
    }
    return QVariant();
}
  • 使用qDebug()输出索引信息
  • 模型测试工具的使用
  • 可视化调试技巧

本章小结

常见错误总结

  • 忘记发送信号是最常见的错误
  • begin/end函数的调用顺序很重要
  • 索引失效要用QPersistentModelIndex
  • parent()和index()必须匹配
  • 注意内存管理

最佳实践

  • 根据数据规模选择合适的Model类型
  • 明确Model、View、Delegate的职责
  • 合理选择数据存储方式
  • 优化信号发送
  • 遵循代码组织规范

调试技巧

  • qDebug()输出详细信息
  • 使用QAbstractItemModelTester
  • 可视化标记帮助发现问题

第14章 进阶资源与学习路径

14.1 官方文档推荐

Qt官方文档

14.2 进阶主题

14.2.1 QML中的Model/View

QML ListView

qml 复制代码
ListView {
    model: myModel  // C++的QAbstractItemModel
    delegate: Rectangle {
        Text { text: display }
    }
}

与C++后端集成

cpp 复制代码
// C++导出模型给QML使用
engine.rootContext()->setContextProperty("myModel", &model);

14.2.2 Qt Quick的ListView和TableView

TableView (Qt Quick)

qml 复制代码
TableView {
    model: tableModel
    delegate: Rectangle {
        implicitWidth: 100
        implicitHeight: 50
        Text {
            text: display
            anchors.centerIn: parent
        }
    }
}

14.2.3 高级特性
  • 模型角色名称:自定义role名称供QML使用

  • 数据绑定:QML与C++模型的双向绑定

  • 异步模型:使用QThread优化大数据加载

  • QML中的Model/View

  • Qt Quick的ListView和TableView

  • 与C++后端模型的集成


14.3 学习建议

14.3.1 学习路径

第一阶段:基础(1-2周)

  1. 理解Model/View分离的思想
  2. 掌握QStringListModel、QStandardItemModel
  3. 熟悉QListView、QTableView的基本使用

第二阶段:实践 (2-3周)

  1. 实现简单的自定义QAbstractTableModel

  2. 学习QSortFilterProxyModel

  3. 掌握基本的Delegate使用

第三阶段:进阶 (3-4周)

  1. 实现复杂的QAbstractItemModel(树形)

  2. 自定义复杂的Delegate

  3. 掌握拖放功能

第四阶段:精通 (持续)

  1. 完成本教程的5个实战项目

  2. 研究Qt源码中的示例

  3. 阅读优秀开源项目的代码


14.3.2 学习技巧

✅ 推荐的学习方法

  • 先掌握简单模型,再处理复杂模型
  • 多做实战项目,理论结合实践
  • 研究Qt源码中的示例代码
  • 阅读优秀开源项目的实现
  • 使用QAbstractItemModelTester验证Model
  • 遇到问题多用qDebug()调试

✅ 避免的学习陷阱

  • 不要一开始就写复杂的树形模型
  • 不要忽视信号的重要性
  • 不要过早优化
  • 不要忘记测试边界条件

14.3.3 推荐资源

书籍

  • 《C++ GUI Programming with Qt》 - Jasmin Blanchette
  • 《Advanced Qt Programming》 - Mark Summerfield
  • 《Qt官方文档》(始终是最好的资源)

开源项目

  • Qt Creator - Qt官方IDE,源码中有大量Model/View示例
  • KDE项目 - 许多应用使用了复杂的Model/View
  • Wireshark - 数据包分析使用了高性能Model

社区资源

  • Qt Forum: https://forum.qt.io/

  • Stack Overflow: Qt标签

  • GitHub: 搜索Qt Model/View相关项目

  • 先掌握简单模型,再处理复杂模型

  • 多做实战项目

  • 研究Qt源码中的示例


附录

附录A:API速查表

QAbstractItemModel常用方法
方法 用途 必须实现
rowCount() 返回行数
columnCount() 返回列数
data() 返回数据
index() 创建索引 ✅(树形)
parent() 返回父索引 ✅(树形)
setData() 设置数据 可选
flags() 返回项标志 可选
headerData() 返回表头 可选
insertRows() 插入行 可选
removeRows() 删除行 可选

视图类常用方法

QTableView

  • setModel() - 设置模型
  • setSortingEnabled() - 启用排序
  • setSelectionMode() - 设置选择模式
  • setSelectionBehavior() - 设置选择行为
  • horizontalHeader() - 获取水平表头
  • verticalHeader() - 获取垂直表头

QListView

  • setViewMode() - 设置视图模式
  • setFlow() - 设置流向
  • setWrapping() - 设置换行
  • setSpacing() - 设置间距

QTreeView

  • expand() - 展开节点
  • collapse() - 折叠节点
  • setExpanded() - 设置展开状态
  • setIndentation() - 设置缩进

委托类常用方法

QStyledItemDelegate

  • paint() - 自定义绘制
  • sizeHint() - 返回大小提示
  • createEditor() - 创建编辑器
  • setEditorData() - 设置编辑器数据
  • setModelData() - 保存编辑数据
  • updateEditorGeometry() - 更新编辑器几何形状

Qt::ItemDataRole枚举值
Role 用途
Qt::DisplayRole 0 显示文本
Qt::DecorationRole 1 图标
Qt::EditRole 2 编辑文本
Qt::ToolTipRole 3 工具提示
Qt::StatusTipRole 4 状态栏提示
Qt::WhatsThisRole 5 What's This帮助
Qt::FontRole 6 字体
Qt::TextAlignmentRole 7 文本对齐
Qt::BackgroundRole 8 背景颜色
Qt::ForegroundRole 9 前景颜色
Qt::CheckStateRole 10 复选框状态
Qt::SizeHintRole 13 大小提示
Qt::UserRole 0x0100 自定义角色起始值

Qt::ItemFlag枚举值
Flag 用途
Qt::NoItemFlags 无标志
Qt::ItemIsSelectable 可选择
Qt::ItemIsEditable 可编辑
Qt::ItemIsDragEnabled 可拖动
Qt::ItemIsDropEnabled 可放置
Qt::ItemIsUserCheckable 可勾选
Qt::ItemIsEnabled 可用
Qt::ItemIsAutoTristate 自动三态
Qt::ItemNeverHasChildren 永不含子项
Qt::ItemIsUserTristate 用户三态
  • QAbstractItemModel常用方法
  • 视图类常用方法
  • 委托类常用方法
  • Qt::ItemDataRole枚举值
  • Qt::ItemFlag枚举值

附录B:完整示例代码索引

第12章实战项目
  1. 电子表格应用

    • 文件位置:第12.1节
    • 核心技术:稀疏存储、公式引擎、CSV I/O
    • 代码量:1500+ 行
  2. 文件管理器

    • 文件位置:第12.2节
    • 核心技术:QFileSystemModel、双视图联动
    • 代码量:1200+ 行
  3. 通讯录管理系统

    • 文件位置:第12.3节
    • 核心技术:树形模型、JSON持久化、圆形头像
    • 代码量:2000+ 行
  4. 数据可视化工具

    • 文件位置:第12.4节
    • 核心技术:CSV解析、类型检测、实时统计
    • 代码量:1000+ 行
  5. 看板任务管理器

    • 文件位置:第12.5节
    • 核心技术:拖放、任务卡片、优先级系统
    • 代码量:1300+ 行

代码片段快速查找

自定义模型

  • 简单列表模型 → 第6.2节
  • 表格模型 → 第6.3节
  • 树形模型 → 第6.4节

自定义委托

  • 渲染委托 → 第8.3节
  • 编辑委托 → 第8.4节
  • 复杂委托 → 第8.5节

排序和过滤

  • 基本排序 → 第9.2节
  • 自定义过滤 → 第9.3节

拖放功能

  • 列表拖放 → 第10.3节

  • 树形拖放 → 第10.3节

  • 所有实战项目的GitHub链接

  • 代码片段快速查找


附录C:参考资料

官方文档

推荐书籍
  1. C++ GUI Programming with Qt 4/5/6

    • 作者:Jasmin Blanchette, Mark Summerfield
    • 评价:Qt编程的经典教材
  2. Advanced Qt Programming

    • 作者:Mark Summerfield
    • 评价:深入Qt高级特性
  3. Mastering Qt 5

    • 作者:Guillaume Lazar, Robin Penea
    • 评价:现代Qt开发实践

优秀开源项目
  1. Qt Creator

  2. KDE项目

    • 描述:Linux桌面环境
    • 链接:https://kde.org/
    • 学习点:复杂的应用架构
  3. Wireshark

  4. qBittorrent


在线资源

🎊 全书总结 🎊

恭喜您完成Qt Model/View架构的完整学习!

📚 您已经掌握:

理论知识

  • ✅ Model/View分离架构
  • ✅ 索引系统和数据角色
  • ✅ 信号槽机制
  • ✅ 代理模式应用

实战能力

  • ✅ 5个完整的商业级项目
  • ✅ 10,000+ 行生产代码
  • ✅ 完整的开发流程

技术栈

  • ✅ 表格、列表、树形模型
  • ✅ 自定义Delegate
  • ✅ 排序和过滤
  • ✅ 拖放功能
  • ✅ 数据持久化(CSV、JSON)
  • ✅ 性能优化

🎯 学习成果:

  • 📊 代码量统计:24,000+ 行文档,10,000+ 行代码
  • 🎯 项目数量:5个完整应用
  • 📚 章节完成:14章 + 3个附录
  • 💼 商用价值:所有项目可直接用于商业

🚀 下一步建议:

  1. 巩固练习 - 重做实战项目,加深理解
  2. 源码研究 - 阅读Qt Creator等开源项目
  3. 实际应用 - 在自己的项目中应用所学知识
  4. 持续学习 - 关注Qt新版本特性

💪 您现在可以:

  • 设计和实现任何复杂的Model/View应用
  • 优化大规模数据的展示性能
  • 创建专业级别的用户界面
  • 解决Model/View相关的各种问题

感谢您的学习,祝您在Qt开发之路上越走越远! 🎉


相关推荐
mjhcsp2 分钟前
C++数位 DP解析
开发语言·c++·动态规划
Coder_Boy_6 分钟前
Java高级_资深_架构岗 核心知识点——高并发模块(底层+实践+最佳实践)
java·开发语言·人工智能·spring boot·分布式·微服务·架构
小龙报12 分钟前
【算法通关指南:数据结构与算法篇】二叉树相关算法题:1.二叉树深度 2.求先序排列
c语言·开发语言·数据结构·c++·算法·贪心算法·动态规划
AC赳赳老秦12 分钟前
2026 AI原生开发工具链趋势:DeepSeek与主流IDE深度联动实践指南
运维·ide·人工智能·架构·prometheus·ai-native·deepseek
yy.y--15 分钟前
Java线程实现浏览器实时时钟
java·linux·开发语言·前端·python
2501_9269783336 分钟前
大模型“脱敏--加密”--“本地轻头尾运算--模型重运算”
人工智能·经验分享·架构
yaoxin52112343 分钟前
327. Java Stream API - 实现 joining() 收集器:从简单到进阶
java·开发语言
golang学习记1 小时前
Go 语言中和类型(Sum Types)的创新实现方案
开发语言·golang
Zevalin爱灰灰1 小时前
方法论——如何设计控制策略架构
算法·架构·嵌入式
野犬寒鸦1 小时前
Java8 ConcurrentHashMap 深度解析(底层数据结构详解及方法执行流程)
java·开发语言·数据库·后端·学习·算法·哈希算法