QT图形视图系统 - 使用一个项目来学习QT的图形视图框架 - 始篇

本文介绍了QT图形视图系统的基本使用,包括QGraphicsView和QGraphicsScene的关系,以及如何在MainWindow中搭建框架,设置scene属性,添加缩放功能和标尺。作者通过实例展示了如何重写QGraphicsView并逐步实现所需效果。
摘要由CSDN通过智能技术生成

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);
}

好了,本篇先介绍到这里,接下来我会写下一篇,让我们一起去实现后续的效果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

turbolove

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值