2

I am working on a small .ui project and I am was trying to understand how to properly make a ruler on a QGraphicsView.

So when the use sees the image it looks like the following:

rulerA

But if the user needs to zoom-in (or zoom-out) the ruler moves accordingly along with the value of the measurements:

rulerB

Thanks for shedding light on this issue and sor providing any potential example or point to the right direction.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Emanuele
  • 2,194
  • 6
  • 32
  • 71
  • You'll need to paint the rulers yourself. They should probably be plain QWidget subclasses, and then you can use QGridLayout to easily position them and the QGraphicsView correctly. A relevant example might be how this paints the line numbers: https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html – hyde Jun 13 '19 at 22:19
  • @hyde, thanks for your comment it is definitely a good starting point! – Emanuele Jun 13 '19 at 23:12

2 Answers2

2

Create a new class by subclassing QWidget to draw your ruler. Then, set a viewport margin based on the size of your ruler.

The main difficulty is to handle the units of your ruler: The painting process in Qt uses only pixels. So, you have to convert all the distance to the right unit.

An example of ruler (in millimeters) for any QAbstractScrollArea (including QGraphicsView):

class Ruler: public QWidget
{
    Q_OBJECT
public:
    Ruler(QAbstractScrollArea* parent=nullptr): QWidget(parent),
        offset(0)
    {
        setFixedSize(40, parent->height());
        move(0, 40);
        connect(parent->verticalScrollBar(), &QScrollBar::valueChanged, this, &Ruler::setOffset);
    }
    virtual void paintEvent(QPaintEvent* event)
    {
        QPainter painter(this);
        painter.translate(0, -offset);
        int const heightMM = height() * toMM();
        painter.setFont(font());
        QFontMetrics fm(font());
        for (int position = 0; position < heightMM; ++position)
        {
            int const positionInPix = int(position / toMM());
            if (position % 10 == 0)
            {
                if (position != 0)
                {
                    QString const txt = QString::number(position);
                    QRect txtRect = fm.boundingRect(txt).translated(0, positionInPix);
                    txtRect.translate(0, txtRect.height()/2);
                    painter.drawText(txtRect, txt);
                }
                painter.drawLine(width() - 15, positionInPix, width(), positionInPix);
            }
            else {
                painter.drawLine(width() - 10, positionInPix, width(), positionInPix);
            }
        }
    }

    virtual void resizeEvent(QResizeEvent* event)
    {

        int const maximumMM = event->size().height() * toMM();
        QFontMetrics fm(font());
        int w = fm.width(QString::number(maximumMM)) + 20;
        if (w != event->size().width())
        {
            QSize const newSize(w, event->size().height());
            sizeChanged(newSize);
            return setFixedSize(newSize);
        }
        return QWidget::resizeEvent(event);
    }

    void setOffset(int value)
    {
        offset = value;
        update();
    }
signals:
    void sizeChanged(QSize const&);
private:
    int offset;

    static qreal toMM()
    {
        return 25.4 / qApp->desktop()->logicalDpiY();
    }
};

The paintEvent() and resizeEvent() functions could be simplified if you want to draw the values at the vertical instead of horizontal (you will not have to resize the ruler to be able to display all digits).

How to use it:

class GraphicsView: public QGraphicsView
{
public:
    GraphicsView(QWidget* parent=nullptr): QGraphicsView(parent),
        ruler(new Ruler(this))
    {
        connect(ruler, &Ruler::sizeChanged, [this](QSize const& size) { setViewportMargins(size.width(), size.width(), 0, 0); });
    }

    void setScene(QGraphicsScene* scene)
    {
        QGraphicsView::setScene(scene);
        if (scene)
            ruler->setFixedHeight(scene->height());
    }
private:
    Ruler* ruler;
};

The conversion pixel -> millimeters is not really accurate and you should find another way to make it.

I didn't handle the resize of the scene, either.

Dimitry Ernot
  • 6,256
  • 2
  • 25
  • 37
  • thank you very much for the detailed example, that was exactly what I was looking for :) – Emanuele Jun 15 '19 at 02:01
  • I used your code exactly the way you've described. The result is that area for ruler has appeared on my QGraphicsView, but no units are drawn on it. Why might that be? – George Jun 27 '19 at 13:31
  • 1
    @George, I also had to change something very minimal to make it work, in particular the `connect` part for the `SIGNAL` and `SLOT`. But it works and you should see something like [this](https://i.imgur.com/MCcxOxy.png). However I prepared a complete example based on Romha Korev code and you can download it from [here](https://bitbucket.org/ERaggi/ruler/src/master/). Let me know if it works for you. – Emanuele Jun 28 '19 at 03:08
  • @Emanuele it works and it is definatley going to help me. Thank you for your time – George Jun 28 '19 at 05:44
1

On creation, scene->height() is zero by default. If ruler->setFixedHeight() unwittingly set the height of the ruler to 0, paintEvent for the ruler will not be called.

void setScene(QGraphicsScene* scene)
    {
        QGraphicsView::setScene(scene);
        if (scene)
            ruler->setFixedHeight(scene->height());
    }