<think>
我们使用Qt的QListView来实现一个垂直时间线列表控件。时间线通常用于展示一系列按时间顺序排列的事件,每个事件可能包含时间戳、标题、描述等信息。我们将通过自定义数据模型(继承自QAbstractListModel)和自定义委托(继承自QStyledItemDelegate)来实现复杂的显示和交互。
主要功能点:
1. 数据模型:存储事件列表,每个事件包含时间、标题、描述等信息。
2. 自定义委托:绘制时间线样式,包括时间轴线条、事件节点图标、文本等。
3. 交互事件:支持点击事件展开/折叠详细信息,可能还支持其他自定义交互。
步骤:
1. 定义事件数据结构(TimelineEvent)。
2. 创建自定义模型(TimelineModel)继承自QAbstractListModel。
3. 创建自定义委托(TimelineDelegate)继承自QStyledItemDelegate,并重写paint()和sizeHint()方法,可能还需要重写editorEvent()来处理交互。
4. 在QListView中使用这个模型和委托。
注意:为了支持复杂交互(如点击展开/折叠),我们可以在模型内部存储每个事件的展开状态,并在委托中根据状态调整绘制。当用户点击某个区域时,通过委托的editorEvent()捕获事件,然后更新模型数据,并刷新视图。
下面是一个简化的实现示例:
首先,定义事件的数据结构:
```cpp
struct TimelineEvent {
QString time; // 时间,如“2023-01-01”
QString title; // 标题
QString description; // 描述
bool expanded; // 是否展开显示详细信息
};
```
然后,创建自定义模型:
```cpp
class TimelineModel : public QAbstractListModel {
Q_OBJECT
public:
explicit TimelineModel(QObject *parent = nullptr) : QAbstractListModel(parent) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
if (parent.isValid())
return 0;
return events.size();
}
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid() || index.row() >= events.size())
return QVariant();
const auto &event = events.at(index.row());
switch (role) {
case Qt::DisplayRole: // 默认显示角色,我们可以返回标题
return event.title;
case TimeRole:
return event.time;
case TitleRole:
return event.title;
case DescriptionRole:
return event.description;
case ExpandedRole:
return event.expanded;
}
return QVariant();
}
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (!index.isValid() || index.row() >= events.size())
return false;
auto &event = events[index.row()];
if (role == ExpandedRole) {
event.expanded = value.toBool();
emit dataChanged(index, index, {role});
return true;
}
return false;
}
QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles;
roles[TimeRole] = "time";
roles[TitleRole] = "title";
roles[DescriptionRole] = "description";
roles[ExpandedRole] = "expanded";
return roles;
}
void addEvent(const TimelineEvent &event) {
beginInsertRows(QModelIndex(), events.size(), events.size());
events.append(event);
endInsertRows();
}
private:
QVector<TimelineEvent> events;
enum Roles {
TimeRole = Qt::UserRole + 1,
TitleRole,
DescriptionRole,
ExpandedRole
};
};
```
接下来,创建自定义委托。在委托中,我们将绘制时间线、事件节点以及文本。同时,我们将处理鼠标点击事件来切换事件的展开状态。
```cpp
class TimelineDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
TimelineDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {}
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
if (!index.isValid())
return;
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
// 获取数据
QString time = index.data(TimelineModel::TimeRole).toString();
QString title = index.data(TimelineModel::TitleRole).toString();
QString description = index.data(TimelineModel::DescriptionRole).toString();
bool expanded = index.data(TimelineModel::ExpandedRole).toBool();
// 设置背景色
painter->fillRect(option.rect, option.palette.base());
// 绘制时间轴垂直线
int lineX = option.rect.left() + 30; // 时间轴位置
painter->setPen(QPen(Qt::gray, 2));
painter->drawLine(lineX, option.rect.top(), lineX, option.rect.bottom());
// 绘制时间点(圆形节点)
int centerY = option.rect.top() + option.rect.height() / 2;
int radius = 6;
painter->setBrush(Qt::blue);
painter->drawEllipse(QPoint(lineX, centerY), radius, radius);
// 绘制时间文本
QRect timeRect(option.rect.left(), centerY - 15, 50, 20);
painter->drawText(timeRect, Qt::AlignRight | Qt::AlignVCenter, time);
// 绘制标题
QRect titleRect(lineX + 15, option.rect.top() + 5, option.rect.width() - lineX - 15, 20);
painter->drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, title);
// 如果展开,绘制描述
if (expanded) {
QRect descRect(lineX + 15, titleRect.bottom() + 5, titleRect.width(), option.rect.height() - titleRect.height() - 10);
painter->drawText(descRect, Qt::TextWordWrap, description);
}
painter->restore();
}
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override {
// 基础高度
int height = 50; // 最小高度
bool expanded = index.data(TimelineModel::ExpandedRole).toBool();
if (expanded) {
QString description = index.data(TimelineModel::DescriptionRole).toString();
// 计算描述文本所需高度
QRect textRect(0, 0, option.rect.width() - 45, 0); // 减去时间轴和边距
textRect = option.fontMetrics.boundingRect(textRect, Qt::TextWordWrap, description);
height += textRect.height() + 10; // 加上标题和间距
}
return QSize(option.rect.width(), height);
}
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override {
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
// 检查点击位置是否在标题区域(简单示例,实际可能需要更复杂的区域判断)
if (mouseEvent->button() == Qt::LeftButton) {
// 假设点击标题区域切换展开状态
QRect titleRect = ...; // 根据绘制时的坐标计算标题区域
if (titleRect.contains(mouseEvent->pos())) {
bool expanded = index.data(TimelineModel::ExpandedRole).toBool();
model->setData(index, !expanded, TimelineModel::ExpandedRole);
return true; // 事件已处理
}
}
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
};
```
最后,在QListView中使用:
```cpp
QListView *listView = new QListView;
TimelineModel *model = new TimelineModel;
TimelineDelegate *delegate = new TimelineDelegate;
// 添加一些事件
TimelineEvent event1{"2023-01-01", "Event 1", "Description of event 1", false};
TimelineEvent event2{"2023-01-02", "Event 2", "Description of event 2", false};
model->addEvent(event1);
model->addEvent(event2);
listView->setModel(model);
listView->setItemDelegate(delegate);
```
注意:以上代码是一个简化示例,实际应用中需要完善细节,比如:
- 在委托中,计算标题区域和点击事件处理需要根据实际绘制的位置和大小进行。
- 可能需要根据需求调整绘制样式和布局。
- 在sizeHint中,计算展开后的高度可能需要更精确的计算,特别是当描述文本很长时。
另外,为了支持更复杂的交互(如右键菜单、拖拽等),可能需要重写更多的方法。
以上代码仅提供了一个基本的实现框架,可以根据具体需求进行扩展。
§§相关问题§§:
1. 如何在时间线中实现不同颜色和样式的节点?
2. 如何优化自定义委托的绘制性能,特别是当事件很多的时候?
3. 除了展开/折叠,如何实现事件的其他交互,比如删除、编辑事件?
4. 如何实现时间线的动态添加和删除事件?
</think>下面我将详细介绍如何使用C++和Qt的QListView实现一个支持复杂交互的垂直时间线列表控件。
### 解决方案
我们将创建一个自定义的`TimelineListWidget`,包含:
1. 自定义模型(继承`QAbstractListModel`)管理时间线数据
2. 自定义委托(继承`QStyledItemDelegate`)处理绘制和交互
3. 自定义视图(继承`QListView`)提供特殊功能
#### 1. 时间线事件数据结构
```cpp
// timelineevent.h
#pragma once
#include <QString>
#include <QDateTime>
#include <QIcon>
struct TimelineEvent {
QDateTime timestamp;
QString title;
QString description;
QIcon icon;
bool isActive;
QString category;
TimelineEvent(QDateTime ts, QString t, QString d, QIcon i, bool active = false)
: timestamp(ts), title(t), description(d), icon(i), isActive(active) {}
};
```
#### 2. 自定义模型
```cpp
// timelinemodel.h
#pragma once
#include <QAbstractListModel>
#include "timelineevent.h"
class TimelineModel : public QAbstractListModel {
Q_OBJECT
public:
enum EventRoles {
TimestampRole = Qt::UserRole + 1,
TitleRole,
DescriptionRole,
IconRole,
ActiveRole,
CategoryRole
};
explicit TimelineModel(QObject *parent = nullptr);
int rowCount(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) override;
void addEvent(const TimelineEvent &event);
void removeEvent(int index);
void toggleActive(int index);
QHash<int, QByteArray> roleNames() const override;
private:
QVector<TimelineEvent> events;
};
```
```cpp
// timelinemodel.cpp
#include "timelinemodel.h"
TimelineModel::TimelineModel(QObject *parent) : QAbstractListModel(parent) {}
int TimelineModel::rowCount(const QModelIndex &) const {
return events.size();
}
QVariant TimelineModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || index.row() >= events.size())
return QVariant();
const TimelineEvent &event = events[index.row()];
switch (role) {
case TimestampRole:
return event.timestamp;
case TitleRole:
return event.title;
case DescriptionRole:
return event.description;
case IconRole:
return event.icon;
case ActiveRole:
return event.isActive;
case CategoryRole:
return event.category;
case Qt::DisplayRole:
return event.title; // Default display
default:
return QVariant();
}
}
bool TimelineModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (!index.isValid() || index.row() >= events.size())
return false;
TimelineEvent &event = events[index.row()];
switch (role) {
case ActiveRole:
event.isActive = value.toBool();
break;
case DescriptionRole:
event.description = value.toString();
break;
default:
return false;
}
emit dataChanged(index, index, {role});
return true;
}
void TimelineModel::addEvent(const TimelineEvent &event) {
beginInsertRows(QModelIndex(), events.size(), events.size());
events.append(event);
endInsertRows();
}
void TimelineModel::removeEvent(int index) {
if (index < 0 || index >= events.size()) return;
beginRemoveRows(QModelIndex(), index, index);
events.remove(index);
endRemoveRows();
}
void TimelineModel::toggleActive(int index) {
if (index < 0 || index >= events.size()) return;
events[index].isActive = !events[index].isActive;
QModelIndex modelIndex = createIndex(index, 0);
emit dataChanged(modelIndex, modelIndex, {ActiveRole});
}
QHash<int, QByteArray> TimelineModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[TimestampRole] = "timestamp";
roles[TitleRole] = "title";
roles[DescriptionRole] = "description";
roles[IconRole] = "icon";
roles[ActiveRole] = "isActive";
roles[CategoryRole] = "category";
return roles;
}
```
#### 3. 自定义委托
```cpp
// timelinedelegate.h
#pragma once
#include <QStyledItemDelegate>
class TimelineDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
explicit TimelineDelegate(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;
bool editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) override;
signals:
void eventClicked(const QModelIndex &index, const QPoint &pos);
void eventDoubleClicked(const QModelIndex &index);
void contextMenuRequested(const QModelIndex &index, const QPoint &pos);
private:
QRect getIconRect(const QRect &itemRect) const;
QRect getTextRect(const QRect &itemRect) const;
QRect getTimeRect(const QRect &itemRect) const;
const int TIMELINE_WIDTH = 80;
const int ICON_SIZE = 32;
const int TIMELINE_MARGIN = 10;
};
```
```cpp
// timelinedelegate.cpp
#include "timelinedelegate.h"
#include <QPainter>
#include <QApplication>
#include <QMouseEvent>
#include <QMenu>
TimelineDelegate::TimelineDelegate(QObject *parent)
: QStyledItemDelegate(parent) {}
void TimelineDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const {
painter->save();
// 绘制背景
bool isActive = index.data(TimelineModel::ActiveRole).toBool();
QColor bgColor = isActive ? QColor("#E3F2FD") : option.palette.base().color();
painter->fillRect(option.rect, bgColor);
// 绘制时间轴线
painter->setPen(QPen(Qt::gray, 2));
int lineX = TIMELINE_WIDTH / 2;
painter->drawLine(lineX, option.rect.top(),
lineX, option.rect.bottom());
// 绘制时间点
QDateTime timestamp = index.data(TimelineModel::TimestampRole).toDateTime();
QString timeText = timestamp.toString("hh:mm");
QRect timeRect = getTimeRect(option.rect);
painter->setPen(Qt::darkGray);
painter->drawText(timeRect, Qt::AlignCenter, timeText);
// 绘制事件图标
QIcon icon = qvariant_cast<QIcon>(index.data(TimelineModel::IconRole));
QRect iconRect = getIconRect(option.rect);
icon.paint(painter, iconRect);
// 绘制标题和描述
QString title = index.data(TimelineModel::TitleRole).toString();
QString description = index.data(TimelineModel::DescriptionRole).toString();
QRect textRect = getTextRect(option.rect);
painter->setPen(option.palette.text().color());
// 绘制标题
QFont titleFont = option.font;
titleFont.setBold(true);
painter->setFont(titleFont);
painter->drawText(textRect, Qt::AlignLeft | Qt::AlignTop, title);
// 绘制描述
QFont descFont = option.font;
descFont.setPointSize(descFont.pointSize() - 1);
painter->setFont(descFont);
painter->setPen(Qt::darkGray);
QRect descRect = textRect.adjusted(0, 20, 0, 0);
painter->drawText(descRect, Qt::TextWordWrap, description);
// 绘制选中状态
if (option.state & QStyle::State_Selected) {
painter->setPen(QPen(Qt::blue, 2));
painter->drawRect(option.rect.adjusted(1, 1, -1, -1));
}
painter->restore();
}
QSize TimelineDelegate::sizeHint(const QStyleOptionViewItem &,
const QModelIndex &) const {
return QSize(300, 100); // 固定高度
}
bool TimelineDelegate::editorEvent(QEvent *event,
QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) {
switch (event->type()) {
case QEvent::MouseButtonPress: {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
// 检测图标点击
QRect iconRect = getIconRect(option.rect);
if (iconRect.contains(mouseEvent->pos())) {
emit eventClicked(index, mouseEvent->globalPos());
return true;
}
// 检测右键菜单
if (mouseEvent->button() == Qt::RightButton) {
emit contextMenuRequested(index, mouseEvent->globalPos());
return true;
}
break;
}
case QEvent::MouseButtonDblClick: {
emit eventDoubleClicked(index);
return true;
}
default:
break;
}
return QStyledItemDelegate::editorEvent(event, model, option, index);
}
QRect TimelineDelegate::getIconRect(const QRect &itemRect) const {
return QRect(
TIMELINE_WIDTH / 2 - ICON_SIZE / 2,
itemRect.top() + (itemRect.height() - ICON_SIZE) / 2,
ICON_SIZE,
ICON_SIZE
);
}
QRect TimelineDelegate::getTextRect(const QRect &itemRect) const {
return QRect(
TIMELINE_WIDTH + TIMELINE_MARGIN,
itemRect.top() + TIMELINE_MARGIN,
itemRect.width() - TIMELINE_WIDTH - 2 * TIMELINE_MARGIN,
itemRect.height() - 2 * TIMELINE_MARGIN
);
}
QRect TimelineDelegate::getTimeRect(const QRect &itemRect) const {
return QRect(
0,
itemRect.top(),
TIMELINE_WIDTH,
itemRect.height()
);
}
```
#### 4. 自定义视图
```cpp
// timelinelistwidget.h
#pragma once
#include <QListView>
#include "timelinemodel.h"
#include "timelinedelegate.h"
class TimelineListWidget : public QListView {
Q_OBJECT
public:
explicit TimelineListWidget(QWidget *parent = nullptr);
void addEvent(const TimelineEvent &event);
TimelineModel* model() const;
signals:
void eventClicked(int index, const QPoint &globalPos);
void eventDoubleClicked(int index);
void contextMenuRequested(int index, const QPoint &globalPos);
private slots:
void handleEventClicked(const QModelIndex &index, const QPoint &pos);
void handleEventDoubleClicked(const QModelIndex &index);
void handleContextMenu(const QModelIndex &index, const QPoint &pos);
private:
TimelineModel *m_model;
TimelineDelegate *m_delegate;
};
```
```cpp
// timelinelistwidget.cpp
#include "timelinelistwidget.h"
TimelineListWidget::TimelineListWidget(QWidget *parent)
: QListView(parent) {
// 设置模型和委托
m_model = new TimelineModel(this);
m_delegate = new TimelineDelegate(this);
setModel(m_model);
setItemDelegate(m_delegate);
// 连接信号
connect(m_delegate, &TimelineDelegate::eventClicked,
this, &TimelineListWidget::handleEventClicked);
connect(m_delegate, &TimelineDelegate::eventDoubleClicked,
this, &TimelineListWidget::handleEventDoubleClicked);
connect(m_delegate, &TimelineDelegate::contextMenuRequested,
this, &TimelineListWidget::handleContextMenu);
// 视图设置
setVerticalScrollMode(ScrollPerPixel);
setSelectionMode(SingleSelection);
setSpacing(5);
setUniformItemSizes(true);
setMouseTracking(true);
}
void TimelineListWidget::addEvent(const TimelineEvent &event) {
m_model->addEvent(event);
}
TimelineModel* TimelineListWidget::model() const {
return m_model;
}
void TimelineListWidget::handleEventClicked(const QModelIndex &index, const QPoint &pos) {
emit eventClicked(index.row(), pos);
}
void TimelineListWidget::handleEventDoubleClicked(const QModelIndex &index) {
emit eventDoubleClicked(index.row());
}
void TimelineListWidget::handleContextMenu(const QModelIndex &index, const QPoint &pos) {
emit contextMenuRequested(index.row(), pos);
}
```
### 使用示例
```cpp
// mainwindow.cpp 部分代码
#include "mainwindow.h"
#include "timelinelistwidget.h"
#include <QVBoxLayout>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent) {
TimelineListWidget *timeline = new TimelineListWidget(this);
// 添加示例事件
timeline->addEvent({
QDateTime::currentDateTime(),
"项目启动",
"新项目正式启动,分配初始任务",
QIcon(":/icons/start.png")
});
timeline->addEvent({
QDateTime::currentDateTime().addDays(1),
"设计评审",
"UI/UX设计评审会议",
QIcon(":/icons/design.png")
});
timeline->addEvent({
QDateTime::currentDateTime().addDays(3),
"开发阶段",
"核心功能开发开始",
QIcon(":/icons/code.png"),
true // 设为活动状态
});
// 连接信号
connect(timeline, &TimelineListWidget::eventClicked,
this, &MainWindow::onEventClicked);
connect(timeline, &TimelineListWidget::eventDoubleClicked,
this, &MainWindow::onEventDoubleClicked);
connect(timeline, &TimelineListWidget::contextMenuRequested,
this, &MainWindow::onContextMenuRequested);
setCentralWidget(timeline);
}
void MainWindow::onEventClicked(int index, const QPoint &pos) {
qDebug() << "事件点击:" << index << "位置:" << pos;
// 显示自定义弹出窗口等
}
void MainWindow::onEventDoubleClicked(int index) {
qDebug() << "事件双击:" << index;
// 打开详细编辑窗口
}
void MainWindow::onContextMenuRequested(int index, const QPoint &pos) {
QMenu menu;
menu.addAction("编辑事件");
menu.addAction("删除事件");
menu.addAction("标记完成");
QAction *selected = menu.exec(pos);
if (selected) {
// 处理菜单操作
}
}
```
### 关键实现解释
1. **自定义模型(TimelineModel)**:
- 使用角色(Roles)系统存储不同类型的数据
- 实现数据编辑和状态切换方法
- 支持动态添加/删除事件
2. **自定义委托(TimelineDelegate)**:
- 实现复杂的绘制逻辑:时间轴、图标、文本
- 处理鼠标事件:点击、双击、右键菜单
- 定义不同元素的绘制区域(图标区、时间区、文本区)
3. **交互功能**:
- 图标点击检测
- 双击事件处理
- 右键上下文菜单
- 活动事件高亮显示
4. **视图优化**:
- 固定尺寸项目提高性能
- 平滑滚动
- 单选模式
5. **扩展性**:
- 支持自定义图标和类别
- 可扩展的事件数据结构
- 信号/槽机制实现外部交互
这个实现提供了完整的垂直时间线功能,包括复杂的事件展示和用户交互。您可以根据需要进一步扩展功能,如添加拖拽排序、动画效果或与后端数据同步。