《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://www.mzph.cn/news/833127.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[redis] 说一说 redis 的底层数据结构

Redis有动态字符串(sds)、链表(list)、字典(ht)、跳跃表(skiplist)、整数集合(intset)、压缩列表(ziplist) 等底层数据结构。 Redis并没有使用这些数据结构来直接实现键值对数据库&#xff0c;而是基于这些数据结构创建了一个对象系统&#xff0c;来表示所有的key-value。 文章…

书生·浦语大模型实战营之手把手带你评测 Llama 3 能力(OpenCompass 版)

书生浦语大模型实战营之手把手带你评测 Llama 3 能力&#xff08;OpenCompass 版&#xff09; 环境配置 conda create -n llama3 python3.10 pytorch torchvision pytorch-cuda -c nvidia -c pytorch -y conda activate llama3conda install git git-lfs install✨下载 Llama3…

C# OpenCvSharp 图片找茬

C# OpenCvSharp 图片找茬 目录 效果 项目 代码 下载 效果 项目 代码 using OpenCvSharp; using System; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; namespace OpenCvSharp_Demo { public partial class Form1 : Form { …

Meltdown 以及Linux KPTI技术简介

文章目录 前言一、Introduction二、 Background2.1 Out-of-order execution2.2 Address Spaces2.3 Cache Attacks 三、A Toy Example四、Building Blocks of the Attack4.1 Executing Transient Instructions4.2 Building a Covert Channel 五、Meltdown5.1 Attack Description…

关于ssti(简介)

ssti&#xff08;Server-Side Template Injection&#xff09;即服务器端模版注入。 首先&#xff0c;在介绍ssti漏洞之前&#xff0c;需要了解web前端框架&#xff08;MVC&#xff09;中对模版文件的调用和渲染&#xff0c;Web框架比如Flask&#xff08;以使用 Python 编写的轻…

【Linux】Docker 安装部署 Nacos

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 【Linux】Docker 安装部署 Nacos docker搜索na…

Leetcode—289. 生命游戏【中等】

2024每日刷题&#xff08;126&#xff09; Leetcode—289. 生命游戏 算法思想 实现代码 class Solution { public:void gameOfLife(vector<vector<int>>& board) {int rows board.size();int cols board[0].size();int neighbors[3] {0, 1, -1};vector<…

信息系统安全与对抗-网络侦查技术与网络扫描技术(期末复习)

1、网络拓扑结构在网络攻击中的作用 查明目标网络的拓扑结构&#xff0c;有利于找到目标网络的关键节点&#xff0c;从而提高攻击效率&#xff0c;达到最大攻击效果。 2、网络侦查在网络攻击中的作用 识别潜在目标系统&#xff0c;确认目标系统适合哪种类型的攻击。 3、百度…

【嵌入式必读】一文彻底理解PID自整定及PID自整定代码设计

文章目录 1. 前言2. PID简介3. 常用的PID自整定方法3.1 临界度比例法3.2 衰减曲线法 4. 继电反馈整定法原理4.1 继电反馈自整定的基本思想4.2 继电反馈自整定原理 5. 算法设计5.1 振荡的生成5.2 提取出临界周期 T c T_c Tc​和振荡波形幅值 A A A5.3 计算出PID参数 6 原代码6.1…

回归预测 | Matlab实现基于CNN-SE-Attention-ITCN多特征输入回归组合预测算法

回归预测 | Matlab实现基于CNN-SE-Attention-ITCN多特征输入回归组合预测算法 目录 回归预测 | Matlab实现基于CNN-SE-Attention-ITCN多特征输入回归组合预测算法预测效果基本介绍程序设计参考资料 预测效果 基本介绍 【模型简介】CNN-SE_Attention结合了卷积神经网络&#xff…

navicat premium16.3.9重置

软件下载 官网地址&#xff1a;https://navicat.com.cn/products/ # 准备脚本 1、建一个txt 2、复制以下代码 3、修改文件格式为bat 4、运行bat文件 5、重新打开navicat&#xff0c;试用期重置为14 经测试16.2.3以上版本均可用 echo off set dnInfo set dn2ShellFolder set r…

【计组OS】访存过程以及存储层次化结构

苏泽 本专栏纯个人笔记作用 用于记录408 学习的笔记记录&#xff08;敲了两年码实在不习惯手写笔记了&#xff09; 如果能帮助到大家当然最好 但由于是工作后退下来备考 很多说法和想法都会结合实际开发的思想 可能不是那么的纯粹应试哈 希望大家挑选自己喜欢的口味食用…

SpringBootWeb入门

SpringBoot可以帮助我们快速的构建应用程序、简化开发、提高效率 创建SpringBoot工程&#xff0c;并勾选web开发相关依赖 定义HelloController类&#xff0c;添加方法&#xff0c;并添加注解 运行测试 创建SpringBoot工程(联网下载) 在File里面点击new Module 点击next 修…

信创 | 信创产业数字化转型与升级:路径规划与实践!

信创产业的数字化转型与升级路径&#xff0c;主要围绕着构建国产化信息技术软硬件底层架构体系和全周期生态体系&#xff0c;解决核心技术关键环节“卡脖子”的问题&#xff0c;以推动中国经济数字化转型的平稳健康发展。 一、信创产业的发展趋势包括&#xff1a; 加强国产信息…

避雷!这本7.7分毕业神刊,影响因子狂涨6.179,最新分区上升,却沦为风险期刊!

近日&#xff0c;科睿唯安又连续对多本期刊进行重新评估&#xff0c;多本「JCR Q1」沦为风险期刊。 值得注意的是&#xff0c;又一本中科院顶刊COMPUTERS IN BIOLOGY AND MEDICINE被打上“On Hold”标签&#xff0c;这是目前“黑名单”收入的第三本中科院TOP刊。 此前&#xff…

基于Springboot的校园新闻管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的校园新闻管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构…

硬件设计细节1-缓冲驱动器使用注意事项

目录 一、缓冲驱动器二、实例分析1.硬件结构2.问题描述3.原因分析4.原因定位 三、结论 一、缓冲驱动器 缓冲驱动器通常用于隔离、电平转换等应用场景。在使用时&#xff0c;需要关注的点较多&#xff0c;如电平范围、频率范围、延时、控制方式、方向以及输入输出状态。通常&am…

Redis(持久化)

文章目录 1.RDB1.介绍2.RDB执行流程3.持久化配置1.Redis持久化的文件是dbfilename指定的文件2.配置基本介绍1.进入redis配置文件2.搜索dbfilename&#xff0c;此时的dump.rdb就是redis持久化的文件3.搜索dir&#xff0c;每次持久化文件&#xff0c;都会在启动redis的当前目录下…

初识C++ · 类和对象(下)

目录 1 再谈构造函数 2 类中的隐式类型转换 3 Static成员 4 友元和内部类 5 匿名对象 6 编译器的一些优化 1 再谈构造函数 先看一段代码&#xff1a; class Date { public :Date(int year, int month, int day){_year year;_month month;_day day;} private:int _ye…

【强训笔记】day13

NO.1 代码实现&#xff1a; #include <iostream>#include<string>using namespace std;int n,k,t; string s;int func() {int ret0;for(int i0;i<n;i){char chs[i];if(chL) ret-1;else{if(i-1>0&&i-2>0&&s[i-1]W&&s[i-2]W) retk…