bauervision bauervision - 15 days ago 8
C++ Question

QGraphicsScene/View Scale Understanding

I'm lost with understanding the scale value of QGraphicsScene/View.

Here is how I'm placing my targets in the scene.

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;

}


So each target has a bearing and range. As long as I don't scale, or zoom, the scene, these values work sufficiently. My problem is that I need to implement the zooming.

Here's where things go wrong:

I have a target at Bearing 270, Range 10.

When the app runs, and my vertical slider is at a value of zero, I can see this target in my view. I should not. I need for this target to only come into view when the slider has gotten to a value of 10. Just think each position value on the slider equates to 1 nautical mile. So if a target is at 10 NMs it should only be visible once the slider is >= 10.

here is how I'm doing the zooming:

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


I've tried making the View bigger, the Scene bigger, but nothing is having the proper effect.

Here is my Scene setup:

view = ui->GraphicsView;
scene = new QGraphicsScene(this);
int baseSize = 355;
scene->setSceneRect(0,0,baseSize,baseSize);
baseSceneSize = scene->sceneRect().width();
view->setScene(scene);


How do I take the range of my target and push it out into the scene so that it lines up with the slider value?

Answer

QGraphicsView::fitInView is everything you need to select the displayed range and center the view.

Here's how you might do it. It's a complete example.

screenshot of the example

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

First, let's obtain random target positions. The scene is scaled in e.g. Nautical Miles: thus any coordinate in the scene is meant to be in these units. This is only a convention: the scene otherwise doesn't care, nor does the view. The reference point is at 0,0: all ranges/bearings are relative to the origin.

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

Then, to aid in turning groups of scene items on and off (e.g. graticules), it helps to have an empty parent item for them:

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

A scene manager sets up the display. The empty items act as item collections and they can be easily made hidden/visible without having to modify child items. They also enforce the relative Z-order of their children.

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;

An event filter can be installed on the view to signal when the view has been resized. This can be used to keep the view centered in spite of resizing:

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

Scene has the following Z-order: center cross, macro- and micro-graticule, then the targets are on top.

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

We can monitor a graphics view for resizing; we also expose the visibility of the micro graticule.

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

Targets can be randomly generated. A target has a fixed size in view coordinates. Its position, though, is subject to any scene-to-view transformations.

The pens for targets and graticules are cosmetic pens: their width is given in the view device units (pixels), not scene units.

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

The graticules are concentric circles centered at the origin (range reference point) and a cross at the origin. The origin cross has fixed size in view units - this is indicated by the ItemIgnoresTransformations flag.

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

The mapping between the scene units and the view is maintained as follows:

  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"