用Qt实现垂直时间轴

119 篇文章 ¥59.90 ¥99.00
本文介绍了如何使用Qt框架创建一个垂直时间轴控件,包括控件的初始化、样式设置、鼠标滚动事件处理、自定义数据类型以及事件的添加和状态更新。通过绘制刻度线和刻度值,提供清晰的时间线条展示。

用Qt实现垂直时间轴

在日常的软件开发中,常常需要展示时间线条。为了更好的用户体验,我们可以设计出不同样式、不同功能的时间轴来满足用户需求。这里我们介绍一种基于Qt实现的垂直时间轴控件。

首先,我们创建一个自定义控件类TimeLineWidget,它继承自QWidget类。在其中建立一个QListWidget用于显示时间轴上的事件。我们设置该控件的最小宽度为50,最大宽度为100,高度为400。

class TimeLineWidget : public QWidget
{
    Q_OBJECT
public:
    TimeLineWidget(QWidget *parent = nullptr);

private:
    QListWidget *timeEventsList;
};

接着,在构造函数中对该控件进行初始化,并设置样式表来美化控件。同时,我们需要设置鼠标滚动事件,以便在时间轴上滚动鼠标时,能够自动调整列表内容并更新其它UI效果。

TimeLineWidget::TimeLineWidget(QWidget *parent)
    : QWidget(parent)
{
    setMinimumWidth(50);
    setMaximumWidth(100);
    setFixedHeight(400);

    timeEventsList = new QListWidget(this);
    timeEventsList->setFrameShape
<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. **扩展性**: - 支持自定义图标和类别 - 可扩展的事件数据结构 - 信号/槽机制实现外部交互 这个实现提供了完整的垂直时间线功能,包括复杂的事件展示和用户交互。您可以根据需要进一步扩展功能,如添加拖拽排序、动画效果或与后端数据同步。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值