QGraphicsScene/View的缩放理解

3
我不理解QGraphicsScene/View的比例尺值。这是我在场景中放置目标的方式。
QPointF Mainwindow::pointLocation(double bearing, double range){
    int offset = 90; //used to offset Cartesian system
    double centerX = baseSceneSize/2;//push my center location out to halfway point
    double centerY = baseSceneSize/2;
    double newX = centerX + qCos(qDegreesToRadians(bearing - offset)) * range;
    double newY = centerY + qSin(qDegreesToRadians(bearing - offset)) * range;
    QPointF newPoint = QPointF(newX, newY);
    return newPoint;

}

每个目标都有方位角和距离。只要我不缩放或缩小场景,这些值就足够了。我的问题是我需要实现缩放。
问题出在这里:
我有一个目标,方位角为270度,距离为10海里。
当应用程序运行时,我的垂直滑块的值为零,我可以在视图中看到这个目标。但实际上我不希望它出现在视图中,我需要这个目标仅在滑块达到10的值时才能看到。请想象滑块上的每个位置值相当于1海里。因此,如果目标在10海里处,它应该只在滑块>=10时可见。
以下是我如何进行缩放的方法:
void MainWindow:: on_PlotSlider_sliderMoved(int position){
    const qreal factor = 1.01;
    viewScaleValue = qPow(factor, -position);//-position to invert the scale
    QMatrix matrix;
    matrix.scale(viewScaleValue, viewScaleValue);
    view->setMatrix(matrix);
}

我已经尝试过将“View”放大、将“Scene”放大,但没有达到预期的效果。 这是我的场景设置:
view = ui->GraphicsView;
scene = new QGraphicsScene(this);
int baseSize = 355;
scene->setSceneRect(0,0,baseSize,baseSize);
baseSceneSize = scene->sceneRect().width();
view->setScene(scene);

如何将我的目标范围推出到场景中,使其与滑块值对齐?

我认为“基础”场景大小的想法是不必要的。使用任何有意义的单位,例如海里,作为您的场景单位,并假定您的船位于坐标系的原点。然后将目标放置在QPoint(range * cos(bearing),range * sin(bearing)的坐标处。缩放选择场景和视图之间的映射,以便给定数量的英里适合视图中。场景会照顾其余的事情。 - Kuba hasn't forgotten Monica
场景基础大小是为了确保我将物体放置在视图中心,然后再进行范围和方位转换的偏移量。此外,这也是一种测试元素,用于尝试并查看会改变场景单位测量的内容。如何使用“任何单位作为我的场景单位”? - bauervision
只是为了澄清,355的baseSize来自于我的UI中QGraphicsView的实际尺寸。如果我不写 centerX = baseSceneSize/2;那么物品将基于视图的左上角创建,而不是中心。 - bauervision
1
场景和视图是完全不同的东西。添加项目时,场景应该按你方便的方式进行设计。对于船舶雷达式图像,我认为在0,0的船舶最容易,单位是海里。要在视图上显示10NM的范围,你需要将场景置于视图中心,并将视图/场景缩放至min(viewHeight/20.0, viewWidth/20.0),其中20.0是左/右/上/下的10NM。所有这些都非常简单 - 否则你会使事情复杂化。 - Kuba hasn't forgotten Monica
我明白我可能把事情搞得太复杂了,这就是为什么我开始说“我迷失了”。我知道“(viewHeight/20.0, viewWidth/20.0)”很有道理,但我不知道如何在我现有的代码中实现它。 - bauervision
显示剩余4条评论
2个回答

4

QGraphicsView::fitInView 是选择显示范围并将视图居中的必需函数。

以下是如何使用它的完整示例。

示例截图

// https://github.com/KubaO/stackoverflown/tree/master/questions/scene-radar-40680065
#include <QtWidgets>
#include <random>

首先,让我们获取随机目标位置。场景按照例如海里的比例缩放:因此,场景中的任何坐标都应该是这些单位。这只是一种约定:场景本身并不关心,视图也不关心。参考点位于0,0:所有范围/方位角度数都相对于原点。

QPointF randomPosition() {
    static std::random_device dev;
    static std::default_random_engine eng(dev());
    static std::uniform_real_distribution<double> posDis(-100., 100.); // NM
    return {posDis(eng), posDis(eng)};
}

然后,为了帮助打开和关闭场景组件(例如格网),需要为它们创建一个空的父组件:

class EmptyItem : public QGraphicsItem {
public:
    QRectF boundingRect() const override { return QRectF(); }
    void paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override {}
};

一个场景管理器设置显示。空项目充当项目集合,可以轻松地隐藏/显示而无需修改子项目。它们还强制执行其子项的相对 Z 顺序。
class SceneManager : public QObject {
    Q_OBJECT
    Q_PROPERTY(bool microGraticuleVisible READ microGraticuleVisible WRITE setMicroGraticuleVisible)
    QGraphicsScene m_scene;
    QPen m_targetPen{Qt::green, 1};
    EmptyItem m_target, m_center, m_macroGraticule, m_microGraticule;

可以在视图上安装事件过滤器以在视图调整大小时发出信号。这可用于使视图保持居中,尽管调整大小:

    bool eventFilter(QObject *watched, QEvent *event) override {
        if (event->type() == QEvent::Resize 
                && qobject_cast<QGraphicsView*>(watched))
            emit viewResized();
        return QObject::eventFilter(watched, event);
    }

场景的Z顺序如下:中心十字、宏观和微观网格,然后是位于顶部的目标。

public:
    SceneManager() {
        m_scene.addItem(&m_center);
        m_scene.addItem(&m_macroGraticule);
        m_scene.addItem(&m_microGraticule);
        m_scene.addItem(&m_target);
        m_targetPen.setCosmetic(true);
        addGraticules();
    }

我们可以监视图形视图的大小调整;我们还公开微网格的可见性。

    void monitor(QGraphicsView *view) { view->installEventFilter(this); }
    QGraphicsScene * scene() { return &m_scene; }
    Q_SLOT void setMicroGraticuleVisible(bool vis) { m_microGraticule.setVisible(vis); }
    bool microGraticuleVisible() const { return m_microGraticule.isVisible(); }
    Q_SIGNAL void viewResized();

目标可以随机生成,目标在视图坐标中具有固定大小。但它的位置可能会受到任何从场景到视图的转换的影响。

目标和栅格线的笔是装饰性笔:它们的宽度以视图设备单位(像素)而不是场景单位来表示。

    void newTargets(int count = 200) {
        qDeleteAll(m_target.childItems());
        for (int i = 0; i < count; ++i) {
            auto target = new QGraphicsEllipseItem(-1.5, -1.5, 3., 3., &m_target);
            target->setPos(randomPosition());
            target->setPen(m_targetPen);
            target->setBrush(m_targetPen.color());
            target->setFlags(QGraphicsItem::ItemIgnoresTransformations);
        }
    }

格网线是以原点(范围参考点)为圆心的同心圆和一个十字架。原点的十字架在视图单位中具有固定的大小 - 这由ItemIgnoresTransformations标志指示。
   void addGraticules() {
        QPen pen{Qt::white, 1};
        pen.setCosmetic(true);
        auto center = {QLineF{-5.,0.,5.,0.}, QLineF{0.,-5.,0.,5.}};
        for (auto l : center) {
            auto c = new QGraphicsLineItem{l, &m_center};
            c->setFlags(QGraphicsItem::ItemIgnoresTransformations);
            c->setPen(pen);
        }
        for (auto range = 10.; range < 101.; range += 10.) {
            auto circle = new QGraphicsEllipseItem(0.-range, 0.-range, 2.*range, 2.*range, &m_macroGraticule);
            circle->setPen(pen);
        }
        pen = QPen{Qt::white, 1, Qt::DashLine};
        pen.setCosmetic(true);
        for (auto range = 2.5; range < 9.9; range += 2.5) {
            auto circle = new QGraphicsEllipseItem(0.-range, 0.-range, 2.*range, 2.*range, &m_microGraticule);
            circle->setPen(pen);
        }
    }
};

场景单元与视图之间的映射关系如下所示:
  1. Each time the view range is changed (from e.g. the combo box), the QGraphicsView::fitInView method is called with a rectangle in scene units (of nautical miles). This takes care of all of the scaling, centering, etc.. E.g. to select a range of 10NM, we'd call view.fitInView(QRect{-10.,-10.,20.,20.), Qt::KeepAspectRatio)

  2. The graticule(s) can be disabled/enabled as appropriate for a given range to unclutter the view.

    int main(int argc, char ** argv) {
        QApplication app{argc, argv};
        SceneManager mgr;
        mgr.newTargets();
    
        QWidget w;
        QGridLayout layout{&w};
        QGraphicsView view;
        QComboBox combo;
        QPushButton newTargets{"New Targets"};
        layout.addWidget(&view, 0, 0, 1, 2);
        layout.addWidget(&combo, 1, 0);
        layout.addWidget(&newTargets, 1, 1);
    
        view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
        view.setBackgroundBrush(Qt::black);
        view.setScene(mgr.scene());
        view.setRenderHint(QPainter::Antialiasing);
        mgr.monitor(&view);
    
        combo.addItems({"10", "25", "50", "100"});
        auto const recenterView = [&]{
            auto range = combo.currentText().toDouble();
            view.fitInView(-range, -range, 2.*range, 2.*range, Qt::KeepAspectRatio);
            mgr.setMicroGraticuleVisible(range <= 20.);
        };
        QObject::connect(&combo, &QComboBox::currentTextChanged, recenterView);
        QObject::connect(&mgr, &SceneManager::viewResized, recenterView);
        QObject::connect(&newTargets, &QPushButton::clicked, [&]{ mgr.newTargets(); });
        w.show();
        return app.exec();
    }
    
    #include "main.moc"
    

1

正如Kuba所建议的那样,我有点过于复杂化了。在他的帮助下,这就是让我得到所需结果的方法。对其中一些内容不是100%确定,但现在它正在按照我需要的方式工作。

view = ui->GraphicsView;
scene = new QGraphicsScene(this);
int baseSize = 1000; // MAGIC value that works, anything other than this, not so much
view->setSceneRect(0,0,baseSize,baseSize);
baseViewSize = view->sceneRect().width();
view->setScene(scene);

我的drawPoint方法运行良好,无需更改。

最后,这是我的滑块。

void MainWindow:: on_PlotSlider_sliderMoved(int position){
    const qreal factor = 1.01;
    viewScaleValue = qPow(factor, -position);//-position to invert the scale
    QMatrix matrix;
     // below is the update, again 6 is a MAGIC number, no clue why 6 works...
    matrix.scale((baseViewSize/6 / position, baseViewSize/6 / position);
    view->setMatrix(matrix);

}

虽然我的问题已经解决了,但我希望能够解释一下我的两个“神奇数字”。
只有当baseSize为1000时,所有内容才有效,为什么?
只有将BaseViewSize除以6,才能正确地进行缩放,为什么?

1
很难说它的工作原理,因为我们不知道位置在哪里,场景中使用的是什么单位等等。您的代码缺少其他部分。设置场景矩形没有意义-场景已经执行了这一操作,并且已经正确地执行了此操作。假设您想要适应场景坐标系中原点周围的10NM范围-它是一个20NM宽/高的区域,从-10NM,-10NM开始。您需要做的就是调用view->fitInView(-10.,-10.,20.,20.)。就是这样。视图会负责计算要设置的缩放比例。当然,每次视图改变大小时都需要调用它。 - Kuba hasn't forgotten Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接