QT图形视图系统
介绍
详细的介绍可以看QT的官方助手,那里面介绍的详细且明白,需要一定的英语基础,我这里直接使用一个开源项目来介绍QGraphicsView、QGraphicsScene的使用。
先提供一个项目的图片
先来一个简单的例子,这个例子是介绍了一下QGraphicsView 和 QGraphicsScene的关系,并且如何在View中展示Scene
#include <QApplication> #include <QGraphicsScene> #include <QGraphicsView> int main(int argc, char **argv) { QApplication app(argc, argv); QGraphicsScene scene; scene.addText("Hello, QGraphicsView"); QGraphicsView view(&scene); view.show(); return app.exec(); }
上面的是最基本的QGraphicsView 中显示QGraphicsScene, 并且打印Hello, QGraphicsView在界面上的例子。由此我们可以看到,scene对象需要被view对象管理之后再显示出来。
接下来,我们将重写QGraphicsView 来实现我们自己要的效果。
开始搭建MainWindow框架
使用mainwindowz作为整个项目的外部界面框架,并且将自己的view放在mainwindow中
mainwindow 之后的代码我会将头文件代码和cpp代码放在一个代码块中,请注意区分
// mainwindow.h #ifndef GRAPHICSVIEWQ_MAINWINDOW_H #define GRAPHICSVIEWQ_MAINWINDOW_H #include <QMainWindow> class GraphicsView; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow() override; protected: private: GraphicsView *graphics_view_; }; #endif //GRAPHICSVIEWQ_MAINWINDOW_H // mainwindow.cpp #include <QHBoxLayout> #include "mainwindow.h" #include "graphicsview.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setMouseTracking(true); resize(1600, 1000); graphics_view_ = new GraphicsView(this); graphics_view_->setObjectName(QString::fromUtf8("graphicsView")); graphics_view_->setTransformationAnchor(QGraphicsView::AnchorUnderMouse); graphics_view_->setResizeAnchor(QGraphicsView::AnchorUnderMouse); QWidget *centralWidget = new QWidget(this); centralWidget->setObjectName(QString::fromUtf8("centralwidget")); QHBoxLayout *horizontalLayout= new QHBoxLayout(centralWidget); horizontalLayout->setSpacing(0); horizontalLayout->setObjectName(QString::fromUtf8("horizontalLayout")); horizontalLayout->setContentsMargins(3, 3, 3, 3); horizontalLayout->addWidget(graphics_view_); setCentralWidget(centralWidget); QGraphicsScene *scene = new QGraphicsScene(); scene->addText("Hello, MainWindow"); graphics_view_->setScene(scene); } MainWindow::~MainWindow() { }
graphicsview
// graphicsview.h #ifndef GRAPHICSVIEWQ_GRAPHICSVIEW_H #define GRAPHICSVIEWQ_GRAPHICSVIEW_H #include <QGraphicsView> #include <QWidget> class GraphicsView : public QGraphicsView { Q_OBJECT public: explicit GraphicsView(QWidget *parent = nullptr); explicit GraphicsView(QGraphicsScene *scene, QWidget *parent = nullptr); ~GraphicsView() override; protected: private: }; #endif //GRAPHICSVIEWQ_GRAPHICSVIEW_H // graphicsview.cpp #include "graphicsview.h" GraphicsView::GraphicsView(QWidget *parent) : QGraphicsView(parent) { } GraphicsView::GraphicsView(QGraphicsScene *scene, QWidget *parent) : QGraphicsView(scene, parent) { } GraphicsView::~GraphicsView() { }
这个时候我们展示mainwindow的时候是能正常看到 hello mainwindow的时候,我们离我们的目标又进一步了。
设置scene的属性
接下来给我们的view在构造的时候加一些属性,并且删除掉mainwindow中的scene
void GraphicsView::setBaseAttribute() { // 设置场景 QGraphicsScene *scene = new QGraphicsScene(this); scene->addText("Hello, MainWindow"); setScene(scene); // 设置接收场景交互 setInteractive(true); // 接收Drop事件 setAcceptDrops(true); // 接收鼠标移动事件 setMouseTracking(true); // CacheNone 所有的绘画都是直接在视窗上完成的. // 背景被缓存,这影响自定义背景和基于backgroundBrush属性的背景.当这个标志被启用,QGraphicsView将分配一个像素图与viewport的完整尺寸. setCacheMode(CacheBackground); // 渲染时,QGraphicsView在渲染背景或前景以及渲染每个项目时保护画家状态(参见QPainter::save())。这允许你让画工处于一个改变的状态(例如,你可以调用QPainter::setPen()或QPainter::setBrush(),而不需要在绘画后恢复状态)。但是,如果项目始终恢复状态,则应该启用此标志以防止QGraphicsView做同样的事情。 setOptimizationFlag(DontSavePainterState); // 禁用QGraphicsView对曝光区域的抗锯齿自动调整。 setOptimizationFlag(DontAdjustForAntialiasing); // QGraphicsView将通过分析需要重绘的区域来尝试找到最佳的更新模式。 setViewportUpdateMode(SmartViewportUpdate); // 一个橡皮筋会出现。鼠标拖动将设置橡皮筋的几何形状,并选择橡皮筋覆盖的所有项目。非交互式视图禁用此模式。 setDragMode(RubberBandDrag); // 设置支持鼠标右键弹出菜单 setContextMenuPolicy(Qt::DefaultContextMenu); // 设置横向和纵向滚动条常开 setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); // 设置黑色背景 setStyleSheet("QGraphicsView { background: #000000 }"); scene->setSceneRect(-1000, -1000, +2000, +2000); // 流出添加标尺的空间 setViewportMargins(24, 0, 0, 24); }
这个时候我们再运行的时候,可以看到整个背景就编程黑色的了。并且出现了滚动条
缩放功能的添加
接下来我们给界面添加缩放功能
首先我们需要注释掉黑色背景,方便我们查看文字的变化, 并且添加以下代码,以便放大缩小的时候更好的跟随鼠标
// 设置抗锯齿 setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); // 设置放大缩小的时候跟随鼠标 setTransformationAnchor(QGraphicsView::AnchorUnderMouse); setResizeAnchor(QGraphicsView::AnchorUnderMouse);
接下来我们添加缩放函数,同时我们重写鼠标事件
void GraphicsView::zoomIn() { if(transform().m11() > 1000.0) return; scale(zoomFactor, zoomFactor); } void GraphicsView::zoomOut() { if(transform().m11() < 1.0) return; scale(1.0 / zoomFactor, 1.0 / zoomFactor); } void GraphicsView::wheelEvent(QWheelEvent *event) { const auto delta = event->angleDelta().y(); const auto pos = event->position().toPoint(); static auto sbUpdate = [&delta, this, scale = 3](QScrollBar* sb) { // @TODO 如果是多个view的话 会不会出问题 sb->setValue(sb->value() - delta); }; if (event->buttons() & Qt::RightButton) { if (abs(delta) == 120) { setInteractive(false); if (delta > 0) zoomIn(); else zoomOut(); setInteractive(true); } } else { switch (event->modifiers()) { case Qt::ControlModifier: if (abs(delta) == 120) { setInteractive(false); if (delta > 0) zoomIn(); else zoomOut(); setInteractive(true); } break; case Qt::ShiftModifier: if (!event->angleDelta().x()) sbUpdate(QAbstractScrollArea::horizontalScrollBar()); break; case Qt::NoModifier: if (!event->angleDelta().x()) sbUpdate(QAbstractScrollArea::verticalScrollBar()); break; default: break; } } emit sig_mouseMove(mapToScene(pos)); // QGraphicsView::wheelEvent(event); }
通过鼠标,我们可以看到对应的变化,我这里添加了混合按钮操作,ctrl是缩放,shift是移动横轴,我这里就不贴效果图了,你们按照此步骤加函数即可,自己去尝试效果去吧。
我们还需要回到最初始的大小,这个时候我们需要添加回到100%比例的函数。并且添加一个键盘事件,按下空格的时候则回到100%的状态。这里可以在初始化的时候直接给设置成百分百
QSizeF GraphicsView::getRealSize() { static QSizeF size; if (!size.isEmpty()) return size; if (size.isEmpty()) FIXME 当前界面的物理尺寸 size = QGuiApplication::screens()[0]->physicalSize(); return size; } void GraphicsView::zoomTo100() { 根据物理尺寸设置大小, 因为后面我们会引入尺子,因此这里设置为根据物理尺寸设置 double x = 1.0, y = 1.0; const double m11 = QGraphicsView::transform().m11(), m22 = QGraphicsView::transform().m22(); const double dx = QGraphicsView::transform().dx(), dy = QGraphicsView::transform().dy(); const QSizeF size(getRealSize()); // size in mm const QRect scrGeometry(QApplication::primaryScreen()->geometry()); // size in pix x = qAbs(1.0 / m11 / (size.height() / scrGeometry.height())); y = qAbs(1.0 / m22 / (size.width() / scrGeometry.width())); std::cout << dx << " " << dy << std::endl; scale(x, y); 恢复到初始状态(位移状态未记录) // QMatrix q; // q.setMatrix(1,this->matrix().m12(),this->matrix().m21(),1,this->matrix().dx(),this->matrix().dy()); // this->setMatrix(q,false); } void GraphicsView::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Space: zoomTo100(); break; case Qt::Key_F: zoomFit(); break; default: break; } QGraphicsView::keyPressEvent(event); } void GraphicsView::zoomFit() { fitInView(scene()->itemsBoundingRect(), false); } void GraphicsView::fitInView(QRectF dstRect, bool withBorders) { if (dstRect.isNull()) return; if (withBorders) dstRect += QMarginsF(dstRect.width() / 5, dstRect.height() / 5, dstRect.width() / 5, dstRect.height() / 5); // 5 mm QGraphicsView::fitInView(dstRect, Qt::KeepAspectRatio); }
加上标尺
接下来我们来给我们的视图加上左边和下面的标尺
先上一张图片
ruler
#ifndef GRAPHICSVIEWLEARN_RULER_H #define GRAPHICSVIEWLEARN_RULER_H #include <QWidget> #include <QPen> class Ruler final : public QWidget { Q_OBJECT public: enum { Width = 24}; explicit Ruler(Qt::Orientation rulerType, QWidget* parent); void drawAScaleMeter(QPainter* painter, QRectF rulerRect, double scaleMeter, double startPosition); // 绘制刻度线 void drawFromOriginTo(QPainter* painter, QRectF rect, double startMark, double endMark, int startTickNo, double step, double startPosition); protected: void paintEvent(QPaintEvent* event) override; void drawMousePosTick(QPainter* painter); private: Qt::Orientation orientation_; double grid_step_ {1.0}; double origin_ {}; double ruler_unit_ {1.0}; double ruler_zoom_ {1.0}; double tick_koef_ {1.0}; QPoint cursor_pos_; QPen meter_pen_; bool draw_text_ {}; }; #endif //GRAPHICSVIEWLEARN_RULER_H #include "ruler.h" #include <QPainter> Ruler::Ruler(Qt::Orientation rulerType, QWidget *parent) : QWidget(parent) , orientation_ { rulerType } { setMouseTracking(true); setStyleSheet("QWidget{ background:black; }"); } void Ruler::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QPainter painter(this); painter.setRenderHints(QPainter::TextAntialiasing); painter.setPen(QPen(Qt::darkGray, 0.0)); // 零宽度笔是装饰笔 QRectF rulerRect(rect()); // 需要QRectF // 首先填充矩形 painter.fillRect(rulerRect, QColor().rgb()); if (qFuzzyIsNull(ruler_zoom_)) return; // fixme 这个地方需要修改成带单位转换的 grid_step_ = pow(10.0, ceil(log10(8.0 / ruler_zoom_))); // ViewSettings::instance().gridStep(rulerZoom_); // 绘制小刻度 if ((grid_step_ * ruler_zoom_) > 35) { tick_koef_ = 0.1; draw_text_ = true; } meter_pen_ = QPen(Qt::darkGray, 0.0); drawAScaleMeter(&painter, rulerRect, grid_step_ * 1, static_cast<double>(Ruler::Width) * 0.6); draw_text_ = false; // 绘制中间刻度 if ((grid_step_ * ruler_zoom_) <= 35) { tick_koef_ = 0.5; draw_text_ = true; } meter_pen_ = QPen(Qt::green, 0.0); drawAScaleMeter(&painter, rulerRect, grid_step_ * 5, static_cast<double>(Ruler::Width) * 0.3); draw_text_ = false; // 绘制整刻度线 meter_pen_ = QPen(Qt::red, 0.0); drawAScaleMeter(&painter, rulerRect, grid_step_ * 10, static_cast<double>(Ruler::Width) * 0); // 绘制当前鼠标位置十字线 drawMousePosTick(&painter); // 在视图和标尺之间分割线 红色的线(看是否需要) if ((1)) { QPointF starPt((Qt::Horizontal == orientation_) ? rulerRect.topLeft() : rulerRect.topRight()); QPointF endPt((Qt::Horizontal == orientation_) ? rulerRect.topRight() : rulerRect.bottomRight()); // FIXME same branches!!!!!! painter.setPen(QPen(Qt::red, 2)); painter.drawLine(starPt, endPt); } QWidget::paintEvent(event); } void Ruler::drawAScaleMeter(QPainter* painter, QRectF rulerRect, double scaleMeter, double startPosition) { bool isHorzRuler = Qt::Horizontal == orientation_; scaleMeter = scaleMeter * ruler_unit_ * ruler_zoom_; double rulerStartMark = isHorzRuler ? rulerRect.left() : rulerRect.top(); // Ruler rectangle ending mark double rulerEndMark = isHorzRuler ? rulerRect.right() : rulerRect.bottom(); if (origin_ >= rulerStartMark && origin_ <= rulerEndMark) { drawFromOriginTo(painter, rulerRect, origin_, rulerEndMark, 0, scaleMeter, startPosition); drawFromOriginTo(painter, rulerRect, origin_, rulerStartMark, 0, -scaleMeter, startPosition); } else if (origin_ < rulerStartMark) { int tickNo = int((rulerStartMark - origin_) / scaleMeter); drawFromOriginTo(painter, rulerRect, origin_ + scaleMeter * tickNo, rulerEndMark, tickNo, scaleMeter, startPosition); } else if (origin_ > rulerEndMark) { int tickNo = int((origin_ - rulerEndMark) / scaleMeter); drawFromOriginTo(painter, rulerRect, origin_ - scaleMeter * tickNo, rulerStartMark, tickNo, -scaleMeter, startPosition); } } void Ruler::drawFromOriginTo(QPainter* painter, QRectF rect, double startMark, double endMark, int startTickNo, double step, double startPosition) { const auto isHorzRuler = (Qt::Horizontal == orientation_); // fixme 这个地方要修改成单位转换的 const auto K = grid_step_ * tick_koef_ * 1.0; QColor color(0xFFFFFFFF - QColor(Qt::black).rgb()); painter->setPen(QPen(color, 0.0)); painter->setFont(font()); QVector<QLineF> lines; lines.reserve(abs(ceil((endMark - startMark) / step))); constexpr double padding = 3; for (double current = startMark; (step < 0 ? current >= endMark : current <= endMark); current += step) { double x1, y1; lines.push_back( QLineF(x1 = isHorzRuler ? current : rect.left() + startPosition, y1 = isHorzRuler ? rect.top() : current, /*x2*/ isHorzRuler ? current : rect.right(), /*y2*/ isHorzRuler ? rect.bottom() - startPosition : current) ); if (draw_text_) { painter->save(); auto number { QString::number(startTickNo * K) }; if (startTickNo) number = ((isHorzRuler ^ (step > 0.0)) ? "-" : "+") + number; QRectF textRect(QFontMetricsF(font()).boundingRect(number)); textRect.setWidth(textRect.width() + 1); if (isHorzRuler) { painter->translate(x1 + padding, textRect.height()); painter->drawText(textRect, Qt::AlignCenter, number); } else { painter->translate(textRect.height() - padding, y1 - padding); painter->rotate(-90); painter->drawText(textRect, number); } painter->restore(); } ++startTickNo; } painter->setPen(meter_pen_); painter->drawLines(lines.data(), lines.size()); } void Ruler::drawMousePosTick(QPainter* painter) { QPoint starPt = cursor_pos_; QPoint endPt; if (Qt::Horizontal == orientation_) { starPt.setY(this->rect().top()); endPt.setX(starPt.x()); endPt.setY(this->rect().bottom()); } else { starPt.setX(this->rect().left()); endPt.setX(this->rect().right()); endPt.setY(starPt.y()); } painter->drawLine(starPt, endPt); }
好了,本篇先介绍到这里,接下来我会写下一篇,让我们一起去实现后续的效果。