《QT实用小工具·六十》Qt 多列时间轴控件

1、概述
源码放在文章末尾

Qt 多列时间轴控件。

可与多段字符串格式自由转换,也可手动添加列表项。

专门用来以时间轴作为事件线发展顺序的故事大纲。

特点
时间背包功能:记录所有物品或属性发生的变化,随时回溯
时间可输入任意内容,不限于时间
每一时间段允许多列,即多个文字节点
全自动调整节点大小(宽高),尽量看起来舒服
行与行、列与列 之间任意拖拽更换顺序
可与文字自由转换,默认一段为一个文字节点
方便的多行操作
按需修改快捷键
所有编辑可撤销
美观的调整动画

项目demo演示如下所示:
在这里插入图片描述
项目部分代码如下所示:

#ifndef TIMELINEWIDGET_H
#define TIMELINEWIDGET_H#include <QObject>
#include <QDebug>
#include <QInputDialog>
#include <QMenu>
#include <QAction>
#include <QMimeData>
#include <QDrag>
#include <QPropertyAnimation>
#include <QScrollArea>
#include <QScrollBar>
#include <QPlainTextEdit>
#include <QTimer>
#include "timelinebucket.h"
#include "labeleditor.h"class TimelineWidget : public QScrollArea
{Q_OBJECT
public:TimelineWidget(QWidget *parent = nullptr);void addItem(QString time, QString text);void addItem(QString time, QStringList texts);TimelineBucket* insertItem(QString time, QStringList texts, int index = -1);void removeItem(int index);void clearAll();int count();int indexOf(TimelineBucket* bucket) const;TimelineBucket* at(int index) const;void moveBucket(int from_index, int to_index);void selectAll();void unselectAll();void selectItem(TimelineBucket* bucket);void selectItems(QList<int> rows, bool clearBefore = true);void unselectItem(TimelineBucket* bucket);void setCurrentItem(int row, bool multi = false);void setCurrentItem(TimelineBucket* bucket, bool multi = false);void scrollTo(int index = -1);QList<int> selectedIndexes(int delta = 0) const;void resetWidth();void adjustBucketsPositions(int start = -1);void adjustBucketsPositionsWithAnimation(int start = 0, int end = -1);void fromString(QString string, QString time_reg = "【(.*)】", QString node_split = "\n", QString nodes_split = "\n");QString toString(QString time_format = "【%1】", QString para_split = "\n", QString line_split = "\n\n");protected:void keyPressEvent(QKeyEvent* event) override;private:TimelineBucket *createItemWidget(QString time, QStringList texts);signals:void manualSelected(); // 鼠标/键盘事件void targetItemsChanged(); // 选中项改变,或者选中的内容改变了public slots:void updateUI();void slotBucketWidgetToSelect(TimelineBucket* bucket);void slotTimeWidgetClicked(TimelineTimeLabel* label);void slotTextWidgetClicked(TimelineTextLabel* label);void slotTimeWidgetDoubleClicked(TimelineTimeLabel* label);void slotTextWidgetDoubleClicked(TimelineTextLabel* label);void slotMenuShowed(const QPoint& pos);void slotDroppedAndMoved(TimelineBucket* from, TimelineBucket* to);void slotEditChanged();void slotEdit(int row, int col);void finishEditing();void actionAddText();void actionAddTextLeft();void actionAddTextRight();void actionEditTime();void actionEditText(int index);void actionAddLine();void actionInsertAbove();void actionInsertUnder();void actionDeleteLine();void actionCopyText();void actionPaste();private:QWidget* center_widget;QList<TimelineBucket*> buckets;QList<TimelineBucket*> selected_buckets;int current_index;LabelEditor* edit;TimelineBucket* editing_bucket;QLabel* editing_label;bool _adusting_buckets_size; // 是否正在调整索引buckets大小(无视此时触发的sizeHintChanged信号)bool _width_need_adjust = false; // 下次动画是否强制调整宽度
};#endif // TIMELINEWIDGET_H
#include "timelinewidget.h"TimelineWidget::TimelineWidget(QWidget *parent) : QScrollArea(parent)
{setAcceptDrops(true);setAttribute(Qt::WA_TransparentForMouseEvents, false);setContextMenuPolicy(Qt::CustomContextMenu);setFocusPolicy(Qt::StrongFocus);connect(this,SIGNAL(customContextMenuRequested (const QPoint&)),this,SLOT(slotMenuShowed(const QPoint&)));_adusting_buckets_size = false;_width_need_adjust = false;current_index = -1;center_widget = new QWidget(this);setWidget(center_widget);editing_bucket = nullptr;editing_label = nullptr;edit = new LabelEditor(center_widget);connect(edit, &LabelEditor::textChanged, this, [=] {if (editing_label == nullptr)return ;editing_label->setText(edit->toPlainText());
//        if (editing_label->objectName() == "TimelineTextLabel") {
//            static_cast<TimelineTextLabel>(editing_label).adjustSize(true, edit->toPlainText());
//        } else {editing_label->adjustSize();
//        }edit->move(editing_label->pos() + editing_label->parentWidget()->pos());edit->resize(editing_label->size());editing_bucket->adjustWidgetsPositions();});connect(edit, &LabelEditor::signalEditCanceled, this, [=](QString origin) {if (editing_label == nullptr)return ;edit->setPlainText(origin); // 设置回初始内容QTimer::singleShot(0, [=]{editing_label = nullptr;editing_bucket = nullptr;});this->setFocus();});connect(edit, &LabelEditor::signalEditFinished, this, [=](QString text) {if (editing_label == nullptr) // 快速按下两次时会触发这个信号槽,而第一次已经使 editing_label = nullptrreturn ;// 编辑结束,保存 undoQString orig = edit->getOriginText();if (text != orig) // 文本有变动{if (editing_bucket->indexOf(static_cast<TimelineTextLabel*>(editing_label)) >= 0){timeline_undos->modifyCommand(editing_bucket, static_cast<TimelineTextLabel*>(editing_label), orig, text);emit targetItemsChanged();}else{timeline_undos->modifyCommand(editing_bucket, orig, text);emit targetItemsChanged();}}edit->hide();editing_label = nullptr;editing_bucket = nullptr;this->setFocus();});edit->hide();updateUI();
}void TimelineWidget::addItem(QString time, QString text)
{addItem(time, QStringList{text});
}void TimelineWidget::addItem(QString time, QStringList texts)
{insertItem(time, texts, -1);
}TimelineBucket *TimelineWidget::insertItem(QString time, QStringList texts, int index)
{TimelineBucket* bucket = createItemWidget(time, texts);bucket->adjustWidgetsPositions();if (index < 0 || index >= count()) // 添加到末尾{if (count() >= 1)bucket->move(buckets.last()->pos());buckets.append(bucket);bucket->setVerticalIndex(count()-1); // 已经添加了,下标索引要-1if (count())bucket->move(buckets.last()->geometry().topLeft());}else // 插入到中间{buckets.insert(index, bucket);for (int i = index; i < count(); i++)buckets.at(i)->setVerticalIndex(i);if (index+1 < count())bucket->move(buckets.at(index+1)->geometry().topLeft());else if (index > 0)bucket->move(buckets.at(index-1)->geometry().topLeft());}bucket->show();// 设置item的尺寸connect(bucket, &TimelineBucket::signalSizeHintChanged, this, [=](QSize size){if (!_adusting_buckets_size)adjustBucketsPositions(buckets.indexOf(bucket));});// 连接事件信号connect(bucket, &TimelineBucket::signalBucketWidgetPressed, this, [=]{ slotBucketWidgetToSelect(bucket); });connect(bucket, SIGNAL(signalTimeWidgetClicked(TimelineTimeLabel*)), this, SLOT(slotTimeWidgetClicked(TimelineTimeLabel*)));connect(bucket, SIGNAL(signalTextWidgetClicked(TimelineTextLabel*)), this, SLOT(slotTextWidgetClicked(TimelineTextLabel*)));connect(bucket, SIGNAL(signalTimeWidgetDoubleClicked(TimelineTimeLabel*)), this, SLOT(slotTimeWidgetDoubleClicked(TimelineTimeLabel*)));connect(bucket, SIGNAL(signalTextWidgetDoubleClicked(TimelineTextLabel*)), this, SLOT(slotTextWidgetDoubleClicked(TimelineTextLabel*)));connect(bucket, &TimelineBucket::signalDroppedAndMoved, this, [=](TimelineBucket* from_bucket) {slotDroppedAndMoved(from_bucket, bucket);});return bucket;
}void TimelineWidget::removeItem(int index)
{if (index < 0 || index >= count())return ;auto bucket = buckets.takeAt(index);selected_buckets.removeOne(bucket);bucket->deleteLater();adjustBucketsPositionsWithAnimation(index);
}void TimelineWidget::clearAll()
{while (buckets.size()){buckets.takeFirst()->deleteLater();}
}int TimelineWidget::count()
{return buckets.size();
}int TimelineWidget::indexOf(TimelineBucket *bucket) const
{return buckets.indexOf(bucket);
}TimelineBucket *TimelineWidget::at(int index) const
{if (index < 0 || index >= buckets.size())return nullptr;return buckets.at(index);
}void TimelineWidget::moveBucket(int from_index, int to_index)
{if (from_index == to_index) // 很可能发生的自己和自己交换return ;if (from_index < 0 || to_index < 0)return ;finishEditing();// 交换 bucketTimelineBucket* bucket = buckets.at(from_index);buckets.removeAt(from_index);if (from_index < to_index) // 下移{buckets.insert(to_index, bucket);for (int i = from_index; i <= to_index; i++)buckets.at(i)->setVerticalIndex(i);}else // 上移{buckets.insert(to_index, bucket);for (int i = from_index; i >= to_index; i--)buckets.at(i)->setVerticalIndex(i);}adjustBucketsPositionsWithAnimation(qMin(from_index, to_index));
}void TimelineWidget::selectAll()
{int left = horizontalScrollBar()->sliderPosition(),right = horizontalScrollBar()->sliderPosition() + horizontalScrollBar()->pageStep();bool odd = true;foreach (TimelineBucket* bucket, buckets) {if (odd)bucket->setPressPos(QPoint(left, bucket->height()/2));elsebucket->setPressPos(QPoint(right, bucket->height()/2));odd = !odd;bucket->setSelected(true);}selected_buckets = buckets;emit targetItemsChanged();
}void TimelineWidget::unselectAll()
{foreach (TimelineBucket* bucket, selected_buckets) {bucket->setSelected(false);}selected_buckets.clear();emit targetItemsChanged();
}void TimelineWidget::selectItem(TimelineBucket *bucket)
{bucket->setSelected(true);if (!selected_buckets.contains(bucket))selected_buckets.append(bucket);emit targetItemsChanged();
}void TimelineWidget::selectItems(QList<int> rows, bool clearBefore)
{if (clearBefore)unselectAll();foreach (auto row, rows){auto bucket = buckets.at(row);bucket->setSelected(true);if (!selected_buckets.contains(bucket))selected_buckets.append(bucket);}emit targetItemsChanged();
}void TimelineWidget::unselectItem(TimelineBucket *bucket)
{bucket->setSelected(false);selected_buckets.removeOne(bucket);emit targetItemsChanged();
}void TimelineWidget::setCurrentItem(int row, bool multi)
{if (!multi)unselectAll();selectItem(buckets.at(row));current_index = row;
}void TimelineWidget::setCurrentItem(TimelineBucket *bucket, bool multi)
{if (!multi)unselectAll();selectItem(bucket);current_index = buckets.indexOf(bucket);
}/*** 确保某个bucket可视*/
void TimelineWidget::scrollTo(int index)
{if (index == -1)index = current_index;if (index == -1)return ;auto bucket = buckets.at(index);int h = bucket->height();int top = bucket->pos().y();int bottom = bucket->geometry().bottom();if (top - h < verticalScrollBar()->sliderPosition()) // 在上面{verticalScrollBar()->setSliderPosition(top - h);}else if (bottom + h > verticalScrollBar()->sliderPosition() + verticalScrollBar()->pageStep()) // 在下面{verticalScrollBar()->setSliderPosition(bottom + h - verticalScrollBar()->pageStep());}
}QList<int> TimelineWidget::selectedIndexes(int delta) const
{int size = buckets.size();QList<int> indexes;for (int i = 0; i < size; i++)if (buckets.at(i)->isSelected())indexes << (i+delta);return indexes;
}void TimelineWidget::resetWidth()
{_width_need_adjust = true;
}/*** 调整某一个位置及后面的所有top*/
void TimelineWidget::adjustBucketsPositions(int start)
{int end = count();int top = (start-1) >= 0 ? buckets.at(start-1)->geometry().bottom() : 0;int max_width = 0;int current_width = center_widget->width();if (start > 0)max_width = center_widget->width();for (int i = start; i < end; i++){TimelineBucket* bucket = buckets.at(i);if (max_width < bucket->width())max_width = bucket->width();bucket->move(bucket->pos().x(), top);top += bucket->height();}_adusting_buckets_size = true;{if (max_width != current_width || _width_need_adjust){foreach (auto bucket, buckets){bucket->resize(max_width, bucket->height());}}int height = 0;if (buckets.size())height = top + buckets.last()->height();elseheight = 50;center_widget->resize(max_width, height);}_adusting_buckets_size = false;
}/*** 调整某一范围内 buckets 的位置* 并且包含位置移动动画*/
void TimelineWidget::adjustBucketsPositionsWithAnimation(int start, int end)
{if (end == -1)end = count();elseend++;int top = (start-1) >= 0 ? buckets.at(start-1)->geometry().bottom() : 0;int current_width = center_widget->width();int max_width = 0;if (start > 0)max_width = center_widget->width();for (int i = start; i < end; i++){TimelineBucket* bucket = buckets.at(i);if (max_width < bucket->width())max_width = bucket->width();if (top != bucket->pos().y()){QPropertyAnimation* ani = new QPropertyAnimation(bucket, "pos");ani->setStartValue(bucket->pos());ani->setEndValue(QPoint(bucket->pos().x(), top));ani->setDuration(300);ani->setEasingCurve(QEasingCurve::OutQuart);connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater()));ani->start();}top += bucket->height();}// 这句会在启动时触发 signalSizeHintChanged,但是必须需要啊// _adusting_buckets_size = true;{if (max_width != current_width || _width_need_adjust){foreach (auto bucket, buckets){bucket->resize(max_width, bucket->height());}}int height = 0;if (buckets.size())height = top + buckets.last()->height();elseheight = 50;center_widget->resize(max_width, height);}// _adusting_buckets_size = false;
}/*** 从字符串中读取* @param string       带格式的字符串* @param time_format  获取时间正则表达式,以第一个括号确定(不要带有 ^ $ 标记!)* @param node_split   时间节点内分段格式* @param nodes_split  时间节点之间分段格式*/
void TimelineWidget::fromString(QString string, QString time_reg, QString node_split, QString nodes_split)
{QList<QString> times;QList<QStringList> textss;if (node_split == nodes_split) // 分段符一致,以每一段的时间标记为准{QString time, time_total;QStringList texts;QRegExp rx(time_reg);rx.setMinimal(true);QStringList lines = string.split(nodes_split, QString::SkipEmptyParts);for (int i = 0; i < lines.length(); i++){QString& line = lines[i];int pos = rx.indexIn(line);if (pos != -1) // 找到时间标记{// 添加上一个时间轴if (time != nullptr || texts.length() > 0){times.append(time);textss.append(texts);time = "";texts.clear();}time_total = rx.cap(0);time = rx.cap(1);// 删除行内标记QRegExp ex(time_total + "[\\s ]*");line.replace(ex, "");if (!line.trimmed().isEmpty()) // 这一段还有其他内容,继续便利i--;}else{texts.append(line.trimmed());}}if (time != nullptr || texts.length() > 0){times.append(time);textss.append(texts);time = "";texts.clear();}}else // 根据分割来{QStringList lines = string.split(nodes_split, QString::SkipEmptyParts);foreach (QString line, lines){QString time_total, time; // 带格式的时间字符串、纯时间字符串QStringList texts;QRegExp rx(time_reg);rx.setMinimal(true);int pos = rx.indexIn(line);if (pos != -1){time_total = rx.cap(0);time = rx.cap(1);// 删除时间标记QRegExp ex(time_total + "[\\s ]*");line.replace(ex, "");}texts = line.split(node_split, QString::SkipEmptyParts);for (int i = 0; i < texts.size(); i++){texts[i] = texts[i].trimmed();}times.append(time);textss.append(texts);}}QList<int> indexs;int c = count();for (int i = 0; i < times.size(); i++)indexs.append(c);timeline_undos->addCommand(indexs, times, textss);
}/*** 将时间轴转换成带分段格式的字符串* @param time_format 时间格式,以 %1 确定* @param para_split  同一时间节点内分段格式* @param line_split  时间节点之间的分段格式* @return 所有字符串*/
QString TimelineWidget::toString(QString time_format, QString para_split, QString line_split)
{QString result;foreach (auto bucket, buckets){if (!result.isEmpty())result += line_split;result += bucket->toString(time_format, para_split);}return result;
}void TimelineWidget::keyPressEvent(QKeyEvent *event)
{auto modifiers = event->modifiers();auto key = event->key();switch (key){case Qt::Key_Up:if (current_index > 0){if (modifiers == Qt::NoModifier) // 上移选中项{auto bucket = buckets.at(current_index-1);bucket->setPressPos(QPoint(qMin(bucket->width(), horizontalScrollBar()->pageStep()), bucket->height()));setCurrentItem(current_index-1);scrollTo();emit manualSelected();return ;}else if (modifiers == Qt::ShiftModifier) // 上移并选中/取消你{auto bucket = buckets.at(current_index);auto bucket_up = buckets.at(current_index-1);if (bucket->isSelected() && bucket_up->isSelected()){unselectItem(bucket);current_index--;}else{bucket_up->setPressPos(QPoint(qMin(bucket_up->width(), horizontalScrollBar()->pageStep()), bucket_up->height()));setCurrentItem(current_index-1, true);}scrollTo();emit manualSelected();return ;}}break;case Qt::Key_Down:if (current_index > -1 && current_index < count()-1){if (modifiers == Qt::NoModifier) // 下移选中项{auto bucket = buckets.at(current_index+1);bucket->setPressPos(QPoint(qMin(bucket->width(), horizontalScrollBar()->pageStep()), 0));setCurrentItem(current_index+1);scrollTo();emit manualSelected();return ;}else if (modifiers == Qt::ShiftModifier) // 下移并选中/取消{auto bucket = buckets.at(current_index);auto bucket_down = buckets.at(current_index+1);if (bucket->isSelected() && bucket_down->isSelected()){unselectItem(bucket);current_index++;}else{bucket_down->setPressPos(QPoint(qMin(bucket_down->width(), horizontalScrollBar()->pageStep()), 0));setCurrentItem(current_index+1, true);}scrollTo();emit manualSelected();return ;}}break;case Qt::Key_Home:if (current_index > 0 && modifiers == Qt::ShiftModifier){int index = current_index;while (index >= 0){setCurrentItem(index, true);index--;}scrollTo();emit manualSelected();return ;}break;case Qt::Key_End:if (current_index > -1 && modifiers == Qt::ShiftModifier){int index = current_index;while (index < count()){setCurrentItem(index, true);index++;}scrollTo();emit manualSelected();return ;}break;case Qt::Key_Delete:{int index = current_index;actionDeleteLine();// 删除键删除的需要继续保持选中状态if (index > -1 && index < count()) // 聚焦原来的同一个索引setCurrentItem(index);else if (index > 0 && index == count()) // 聚焦最后一个setCurrentItem(index-1);return ;}case Qt::Key_Insert:actionInsertAbove();return ;case Qt::Key_Escape:if (current_index > -1){if (selected_buckets.size() > 1)setCurrentItem(current_index);else if (selected_buckets.size())unselectItem(buckets.at(current_index));return ;}else{unselectAll();}break;case Qt::Key_A:if (modifiers == Qt::ControlModifier){selectAll();return ;}break;case Qt::Key_Z:if (modifiers == Qt::ControlModifier){timeline_undos->undoCommand();return ;}break;case Qt::Key_Y:if (modifiers == Qt::ControlModifier){timeline_undos->redoCommand();return ;}break;case Qt::Key_C:actionCopyText();return ;case Qt::Key_Tab:/*** 注意:如果要监听到 Tab 键,要禁止 Tab 切换* QWidget::setFocus(Qt::NoFocus)*/if (modifiers == Qt::NoModifier){actionAddText();}return ;case Qt::Key_Enter:case Qt::Key_Return:if (modifiers == Qt::ShiftModifier)actionInsertAbove();else if (modifiers == Qt::ControlModifier)actionAddLine();else if (modifiers == Qt::NoModifier)actionInsertUnder();return ;case Qt::Key_Space:actionEditText(0);return ;case Qt::Key_1:actionEditText(0);return ;case Qt::Key_2:actionEditText(1);return ;case Qt::Key_3:actionEditText(2);return ;case Qt::Key_4:actionEditText(3);return ;case Qt::Key_5:actionEditText(4);return ;case Qt::Key_6:actionEditText(5);return ;case Qt::Key_7:actionEditText(6);return ;case Qt::Key_8:actionEditText(7);return ;case Qt::Key_9:actionEditText(8);return ;case Qt::Key_0:case Qt::Key_QuoteLeft: // 反撇号actionEditTime();return ;case Qt::Key_Apostrophe: // 这是单引号……break;}QScrollArea::keyPressEvent(event);
}TimelineBucket *TimelineWidget::createItemWidget(QString time, QStringList texts)
{TimelineBucket* bucket = new TimelineBucket(center_widget);bucket->setTime(time);bucket->setText(texts);connect(bucket, SIGNAL(signalBucketContentsChanged()), this, SIGNAL(targetItemsChanged()));return bucket;
}void TimelineWidget::updateUI()
{QString style = "#TimelineTimeLabel { background:white; border: 1px solid orange; border-radius: 5px; padding: 10px; }""#TimelineTextLabel { background:white; border: 1px solid blue; border-radius: 5px; padding: 10px; }""#TimelineEdit { background:white; border: 1px solid transparent; border-radius: 5px; padding: 5px; margin: 1px; margin-left: 4px;}";setStyleSheet(style);
}void TimelineWidget::slotBucketWidgetToSelect(TimelineBucket *bucket)
{finishEditing();if (QApplication::keyboardModifiers() == Qt::NoModifier) // 没有修饰符,单选{setCurrentItem(bucket);}else if (QApplication::keyboardModifiers() == Qt::ControlModifier) // 按下 ctrl{if (!bucket->isSelected())setCurrentItem(bucket, true);elseunselectItem(bucket);}else if (QApplication::keyboardModifiers() == Qt::ShiftModifier) // 按下 shift{int prev = current_index; // 上次按下的int curr = buckets.indexOf(bucket);if (prev != -1){if (prev < curr){// 判断是否已经全选bool has_unselect = false;for (int i = prev; i <= curr; i++){if (!buckets.at(i)->isSelected()){has_unselect = true;break;}}// 再次遍历,如果有没有选择的,则选择;否则取消选择for (int i = prev; i <= curr; i++){TimelineBucket* bucket = buckets[i];if (bucket->isSelected() != has_unselect){selected_buckets.append(bucket);bucket->setSelected(has_unselect);}}}else if (prev > curr){bool has_unselect = false;for (int i = prev; i >= curr; i--){if (!buckets.at(i)->isSelected()){has_unselect = true;break;}}for (int i = prev; i >= curr; i--){TimelineBucket* bucket = buckets[i];if (bucket->isSelected() != has_unselect){selected_buckets.append(bucket);bucket->setSelected(has_unselect);}}}}current_index = curr;}emit manualSelected();
}void TimelineWidget::slotTimeWidgetClicked(TimelineTimeLabel *label)
{}void TimelineWidget::slotTextWidgetClicked(TimelineTextLabel *label)
{}void TimelineWidget::slotTimeWidgetDoubleClicked(TimelineTimeLabel *label)
{QTimer::singleShot(0, [=]{editing_bucket = buckets.at(current_index);editing_label = label;edit->move(label->pos() + label->parentWidget()->pos());edit->setPlainText(label->text());edit->resize(label->size());edit->setOriginText(label->text());edit->show();edit->raise();edit->setFocus();edit->selectAll();});
}void TimelineWidget::slotTextWidgetDoubleClicked(TimelineTextLabel *label)
{QTimer::singleShot(0, [=]{editing_bucket = buckets.at(current_index);editing_label = label;edit->move(label->pos() + label->parentWidget()->pos());edit->setPlainText(label->text());edit->resize(label->size());edit->setOriginText(label->text());edit->show();edit->raise();edit->setFocus();edit->selectAll();});
}void TimelineWidget::slotMenuShowed(const QPoint &pos)
{QMenu* menu = new QMenu("菜单", this);QAction* add_text_action = new QAction("添加文字节点", this);QAction* add_line_action = new QAction("添加新行", this);QAction* insert_above_action = new QAction("上方插入行", this);QAction* insert_under_action = new QAction("下方插入行", this);QAction* delete_line_action = new QAction("删除行", this);QAction* copy_text_action = new QAction("复制文字", this);QAction* paste_action = new QAction("剪贴板导入", this);menu->addAction(add_text_action);menu->addAction(add_line_action);menu->addAction(insert_above_action);menu->addAction(insert_under_action);menu->addAction(delete_line_action);menu->addSeparator();menu->addAction(copy_text_action);menu->addAction(paste_action);if (current_index == -1){add_text_action->setEnabled(false);insert_above_action->setEnabled(false);insert_under_action->setEnabled(false);delete_line_action->setEnabled(false);copy_text_action->setEnabled(false);}// 设置事件connect(add_text_action, SIGNAL(triggered()), this, SLOT(actionAddText()));connect(add_line_action, SIGNAL(triggered()), this, SLOT(actionAddLine()));connect(insert_above_action, SIGNAL(triggered()), this, SLOT(actionInsertAbove()));connect(insert_under_action, SIGNAL(triggered()), this, SLOT(actionInsertUnder()));connect(delete_line_action, SIGNAL(triggered()), this, SLOT(actionDeleteLine()));connect(copy_text_action, SIGNAL(triggered()), this, SLOT(actionCopyText()));connect(paste_action, SIGNAL(triggered()), this, SLOT(actionPaste()));menu->exec(QCursor::pos());
}void TimelineWidget::slotDroppedAndMoved(TimelineBucket *from, TimelineBucket *to)
{int from_index = buckets.indexOf(from);int to_index = buckets.indexOf(to);timeline_undos->moveCommand(from_index, to_index);
}void TimelineWidget::slotEditChanged()
{}/*** 编辑某一个节点* @param row 时间行* @param col 改行第几项。0为时间,1开始为文字*/
void TimelineWidget::slotEdit(int row, int col)
{if (row < 0 || row >= buckets.size())return;auto bucket = buckets.at(row);bucket->edit(col);
}/*** 准备进行其他操作时,如果正在编辑,则结束编辑*/
void TimelineWidget::finishEditing()
{edit->finishIfEditing();
}void TimelineWidget::actionAddText()
{QList<int> bucket_indexes = selectedIndexes();QList<QList<int>> texts_indexes;foreach (auto bucket_index, bucket_indexes){texts_indexes << QList<int>{buckets.at(bucket_index)->count()};}timeline_undos->addCommand(bucket_indexes, texts_indexes);if (bucket_indexes.size() == 1){// 等待动画结束,显示编辑框QTimer::singleShot(300, [=]{slotEdit(bucket_indexes.first(), buckets.at(bucket_indexes.first())->count());});}
}void TimelineWidget::actionAddTextLeft()
{}void TimelineWidget::actionAddTextRight()
{}void TimelineWidget::actionEditTime()
{if (current_index == -1)return ;auto bucket = at(current_index);slotTimeWidgetDoubleClicked(bucket->timeLabel());
}void TimelineWidget::actionEditText(int index)
{if (current_index == -1)return ;auto bucket = at(current_index);if (bucket->count() <= index)return ;slotTextWidgetDoubleClicked(bucket->at(index));
}void TimelineWidget::actionAddLine()
{timeline_undos->addCommand(count());setCurrentItem(count()-1);scrollTo();
}void TimelineWidget::actionInsertAbove()
{QList<int> indexes = selectedIndexes();timeline_undos->addCommand(indexes);unselectAll();int cumu = 0;for (int i = 0; i < indexes.count(); i++){auto bucket = buckets.at(indexes.at(i)+cumu);bucket->setSelected(true);selected_buckets.append(bucket);cumu++;}
}void TimelineWidget::actionInsertUnder()
{QList<int> indexes = selectedIndexes(1);timeline_undos->addCommand(indexes);unselectAll();int cumu = 0;for (int i = 0; i < indexes.count(); i++){auto bucket = buckets.at(indexes.at(i)+cumu);bucket->setSelected(true);selected_buckets.append(bucket);cumu++;}
}void TimelineWidget::actionDeleteLine()
{finishEditing();QList<int> indexes = selectedIndexes();timeline_undos->deleteCommand(indexes);selected_buckets.clear();current_index = -1;
}void TimelineWidget::actionCopyText()
{QString result;foreach (auto bucket, buckets){if (bucket->isSelected()){if (!result.isEmpty())result += "\n";result += bucket->toString();}}QApplication::clipboard()->setText(result);
}void TimelineWidget::actionPaste()
{QString text = QApplication::clipboard()->text();if (text.isEmpty())return ;int c = this->count();fromString(text);int c2 = this->count();if (c == c2) // 没有变化return ;unselectAll();for (int i = c; i < c2; i++)selectItem(at(i));scrollTo(c2-1);scrollTo(c);
}

源码下载

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3016503.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

【区块链】智能合约简介

智能合约起源 智能合约这个术语至少可以追溯到1995年&#xff0c;是由多产的跨领域法律学者尼克萨博&#xff08;NickSzabo&#xff09;提出来的。他在发表在自己的网站的几篇文章中提到了智能合约的理念。他的定义如下&#xff1a;“一个智能合约是一套以数字形式定义的承诺&a…

【C++STL详解(八)】--------stack和queue的模拟实现

目录 前言 一、stack模拟实现 二、queue的模拟实现 前言 前面也介绍了stack和queue的常见接口&#xff0c;我们也知道stack和queue实际上是一种容器适配器&#xff0c;它们只不过是对底层容器的接口进行封装而已&#xff0c;所以模拟实现起来比较简单&#xff01;一起来看看是…

杭州恒生面试,社招,3年经验

你好&#xff0c;我是田哥 一位朋友节前去恒生面试&#xff0c;其实面试问题大部分都是八股文&#xff0c;但由于自己平时工作比较忙&#xff0c;完全没有时间没有精力去看八股文&#xff0c;导致面试结果不太理想&#xff0c;HR说节后通知面试结果&#xff08;估计是凉了&…

北邮22级信通院DSP:实验三(1):FFT变换、IFFT变换(附每步8点变换蝶形图)保姆级讲解+用C++程序实现复数域的FFT变换和IFFT变换

北邮22信通一枚~ 跟随课程进度更新北邮信通院DSP的笔记、代码和文章&#xff0c;欢迎关注~ 获取更多文章&#xff0c;请访问专栏&#xff1a; 北邮22级信通院DSP_青山入墨雨如画的博客-CSDN博客 目录 一、预备知识 1.1 FFT算法 1.2.1由DFT到FFT 1.2.2 基2时域抽选算法 …

华为:三层交换机与路由器连通上网实验

三层交换机是一种网络交换机&#xff0c;可以实现基于IP地址的高效数据转发和路由功能&#xff0c;通常用于大型企业、数据中心和校园网络等场景。此外&#xff0c;三层交换机还支持多种路由协议&#xff08;如OSPF、BGP等&#xff09;&#xff0c;以实现更为复杂的网络拓扑结构…

重装win11系统后找不到WiFi

由于电脑崩溃重装了系统&#xff0c;win11,装完之后WiFi图标不见了且网络适配器根本没有无线网络选项。 右键电脑》管理》网络适配器。 在刚装好系统时候并没有前两项&#xff0c;查了很多资料&#xff0c;比如 关机14s 重启&#xff0c;还有通过服务配置 WLAN AutoConfig 都…

【嵌入式DIY实例】-基于GSM的远程灌溉系统

基于GSM的远程灌溉系统 文章目录 基于GSM的远程灌溉系统1、硬件准备与接线2、软件准备3、代码实现本文将详细介绍如何搭建通过使用手机实现对灌溉系统的远程控制。该系统利用全球移动通信系统(GSM)技术在灌溉系统和移动电话之间建立通信。该系统建立在流行的开源微控制器平台…

IP规划案例

整个OSPF环境IP基于172.16.0.0/16划分 172.16.0.0/16 先分成2个网段&#xff08;OSPF RIP&#xff09;&#xff0c;借1位172.16.0.0/17 ---OSPF 再按区域划分&#xff08;5个区域&#xff09;&#xff0c;借3位 172.16.0.0/20 ---Area 0 三个环回 MGRE 4个网…

Vulnhub项目:NAPPING: 1.0.1

1、靶机介绍 靶机地址&#xff1a;Napping: 1.0.1 ~ VulnHub 2、渗透过程 老规矩&#xff0c;先探测&#xff0c;靶机ip&#xff1a;192.168.56.152 本机ip&#xff1a;192.168.56.146 来看一看靶机开放哪些端口&#xff0c;nmap一下 nmap -sS -sV -A -T5 192.168.56.152 开…

软件系统测试方案书(测试计划-Word原件)

2 引言 2.1 编写目的 2.3 测试人员 2.4 项目背景 2.5 测试目标 2.6 简写和缩略词 2.7 参考资料 2.8 测试提交文档 2.9 测试进度 3 测试环境 3.1 软硬件环境 4 测试工具 5 测试策略 5.1 测试阶段划分及内容 5.1.1 集成测试 5.1.2 系统测试 5.1.2.1 功能测试 5.…

PXE批量安装

系统装机的三种引导方式 u盘光盘网络装机 光盘&#xff1a; 1.类似于usb模式 2.刻录模式 系统安装过程 加载boot loader Boot Loader 是在操作系统内核运行之前运行的一段小程序。通过这段小程序&#xff0c;我们可以初始化硬件设备、建立内存空间的映射图&#xff0c;从…

「C++ 内存管理篇 00」指针

目录 一、变量&#xff0c;变量名和指针 1. 什么是变量&#xff1f; 2. 变量名和指针 3. 使用指针获取数据 二、指针变量和数组变量 三、编译器对指针的等级有着严格的检查 四、指针的加减 1. 存放指针的变量的加减 2. 存放指针的变量的自增自减 3. 两个指针相减 一、变量&…

网络基础——路由

网络基础——路由 要想网络畅通&#xff0c;应让网络中的路由器知道如何转发数据包到各个网段。路由器根据路由表来转发数据包&#xff0c;而路由表是通过直连网络、静态路由以及动态路由来构建的。 route命令&#xff0c;底层是使用ioctl实现&#xff1b;ip命令&#xff0c;…

OFD(Open Fixed-layout Document)

OFD(Open Fixed-layout Document) &#xff0c;是由工业和信息化部软件司牵头中国电子技术标准化研究院成立的版式编写组制定的版式文档国家标准&#xff0c;属于中国的一种自主格式&#xff0c;要打破政府部门和党委机关电子公文格式不统一&#xff0c;以方便地进行电子文档的…

TP3602 同步升压型直流/直流稳压器芯片IC

一般描述 该TP3602是高效率的同步&#xff0c;PWM升压DC/DC转换器优化&#xff0c;提供一个高效的解决方案&#xff0c;以中等功率系统。该器件工作在0.9V至4.4V的输入电压下&#xff0c;具有1.4MHz的固定频率开关。这些特性允许使用微小、低轮廓电感器和陶瓷电容器&…

windows系统远程执行脚本部署项目操作手册

windows系统远程执行脚本部署项目操作手册 windows系统远程执行脚本部署项目 如果频繁的需要部署项目到远程的服务器上,每次要手动上传项目,然后停止项目,启动项目,很麻烦,像Linux天生支持远程执行脚本 Windows借助工具也可以做到. 安装WinSCP软件 自行下载软件或关注我的公…

PXE批量网络装机和Kickstart无人值守安装

一、PXE定义 PXE&#xff08;preboot execute environment&#xff09;:用于通过网络来引导系统的标准&#xff0c;工作在Client/Server模式&#xff08;也称为CS模式&#xff09;&#xff0c;允许客户机通过网络从远程服务器上下载引导镜像&#xff0c;并加载安装文件或整个操…

[C++][数据结构]哈希1:哈希函数的介绍与线性探测的实现

前言 学完了二叉树&#xff0c;我们要学当前阶段数据结构的最后一个内容了&#xff1a;哈希&#xff01;&#xff01; 引入 先来介绍两个用哈希封装的两个容器&#xff1a;unordered_map unordered_set 与map和set的不同&#xff1a; map/set是双向迭代器&#xff0c;而另…

OSPF链路状态数据库

原理概述 OSPF是一种基于链路状态的动态路由协议&#xff0c;每台OSPF路由器都会生成相关的LSA&#xff0c;并将这些LSA通告出去。路由器收到LSA后&#xff0c;会将它们存放在链路状态数据库LSDB中。 LSA有多种不同的类型&#xff0c;不同类型的LSA的功能和作用是不同的&…

书生·浦语大模型实战营之XTuner多模态训练与测试

书生浦语大模型实战营之XTuner多模态训练与测试 目录 XTuner多模态训练与测试给LLM装上电子眼&#xff1a;多模态LLM原理简介文本单模态文本图像多模态 电子眼&#xff1a;LLaVA方案简介LLaVA训练阶段示意图LLaVA测试阶段示意图 项目实践环境准备XTuner安装概述Pretrain阶段Fi…