Nominate our 2022 Qt Champions!

How can I use QPainter to paint on QGraphicsView?



  • I am trying to develop an app to draw stuff like lines, rectangles, and arcs and to adjust them. I know that QGraphicsView Class and QGraphicsScene Class provides methods to generate and manage geometries. But I don't know how can I update my canvas instantly while I'm moving my mouse to draw. Since QGraphicsView Class inherits QWidget Class, I thought that QPainter can be used to do so. But here's error information:

    Starting F:\QtProj\build-scene-Desktop_Qt_5_6_3_MinGW_32bit-Debug\debug\scene.exe...
    QWidget::paintEngine: Should no longer be called
    QPainter::begin: Paint device returned engine == 0, type: 1
    QPainter::setPen: Painter not active
    

    And of course nothing was displayed on the screen.
    Here's myview.h:

    #ifndef CANVASWIDGET_H
    #define CANVASWIDGET_H
    
    #include <QWidget>
    #include <QGraphicsView>
    
    class MyView : public QGraphicsView
    {
        Q_OBJECT
    
    public:
        explicit MyView(QWidget *parent = 0);
        int drawType = 0;
        QPointF pStart, pMove;
        int started = 0;
    
    signals:
    
    private slots:
        void setCursor(int);
        void setType(int);
        void resetPos();
        qreal getLength();
    
    protected:
        void mousePressEvent(QMouseEvent *event);
        void mouseMoveEvent(QMouseEvent *event);
        void paintEvent(QPaintEvent *event);
    };
    
    #endif // CANVASWIDGET_H
    
    

    Here's myview.cpp:

    #include "myview.h"
    #include <QDebug>
    #include <QWidget>
    #include <QGraphicsView>
    #include <QMouseEvent>
    #include <QPainter>
    #include <math.h>
    
    MyView::MyView(QWidget *parent)
    {
    
    }
    void MyView::resetPos(){
        pStart.setX(0.0);  pStart.setY(0.0);
        pMove.setX(0.0);  pMove.setY(0.0);
    }
    qreal MyView::getLength(){
        return sqrt((pStart.x()-pMove.x())*(pStart.x()-pMove.x()) + (pStart.y()-pMove.y())*(pStart.y()-pMove.y()));
    }
    void MyView::setType(int type){
    //    qDebug()<<"scene:"<<type;
        drawType = type;
    }
    void MyView::setCursor(int type){
    //    qDebug()<<"view:"<<type;
    
        if(type == 1 || type == 2 || type == 3){
            viewport()->setCursor(Qt::CrossCursor);
        }
        if(type == 4){
            viewport()->setCursor(Qt::SizeAllCursor);
        }
        if(type == 5){
            viewport()->setCursor(Qt::ArrowCursor);
        }
    }
    void MyView::mousePressEvent(QMouseEvent *event){
        //  Click to draw
            if(drawType==1 || drawType==2 || drawType==3){
                if(!started){
                    pStart = event->localPos();
                    started = 1;
                    setMouseTracking(true);
                }
                if(started){
                        setMouseTracking(false);
                        this->update();
                        started = 0;
                }
            }
    }
    void MyView::mouseMoveEvent(QMouseEvent *event){
        if(drawType == 1 || drawType == 2 || drawType == 3){
            pMove = event->localPos();
        }
    }
    void MyView::paintEvent(QPaintEvent *event){
        QPainter painter(this);
        QPen pen;
        pen.setWidth(1);
        pen.setStyle(Qt::SolidLine);
        painter.setPen(pen);
    
        switch (drawType) {
            case 1: painter.drawLine(pStart, pMove);
                    break;
            case 2: painter.drawRect(QRectF(pStart, pMove));
                    break;
            case 3: pen.setStyle(Qt::DashLine);  painter.setPen(pen);
                    painter.drawLine(pStart, pMove);
                    pen.setStyle(Qt::SolidLine);  painter.setPen(pen);
                    painter.drawEllipse(pStart, getLength(), getLength());
                    break;
        }
    }
    

    Here's mainwindow.cpp:

    #include "mainwindow.h"
    #include "myitem.h"
    #include "myscene.h"
    #include "myview.h"
    #include "ui_mainwindow.h"
    #include <QGraphicsScene>
    #include <QGraphicsView>
    
    MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent),
        ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
        //New MyScene
        MyScene *s = new MyScene;
        //New View
        MyView *v = new MyView;
        v->setScene(s);
        setCentralWidget(v);
    
        QObject::connect(this, SIGNAL(drawType(int)), v, SLOT(setCursor(int)));
        QObject::connect(this, SIGNAL(drawType(int)), s, SLOT(setType(int)));
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    void MainWindow::on_actionDrawLine_triggered()
    {
        emit drawType(1);
        ui->statusBar->showMessage(tr("Type: Line"));
    }
    
    void MainWindow::on_actionDrawRect_triggered()
    {
        emit drawType(2);
        ui->statusBar->showMessage(tr("Type: Rect"));
    }
    
    void MainWindow::on_actionDrawArc_triggered()
    {
        emit drawType(3);
        ui->statusBar->showMessage(tr("Type: Circle"));
    }
    void MainWindow::on_actionDrag_triggered()
    {
        emit drawType(4);
        ui->statusBar->showMessage(tr("Type: Drag"));
    }
    void MainWindow::on_actionClear_triggered()
    {
        emit drawType(5);
        ui->statusBar->showMessage(tr("Type: Clear"));
    }
    

    Many thanks if you can help me!


  • Lifetime Qt Champion

    Hi
    Normally you would make a custom widget based on QWidget and
    use http://doc.qt.io/qt-5/qgraphicsproxywidget.html
    to have it in scene.
    Drawing directly on the view would make it impossible to have support for
    all the features of moving / zooming etc.



  • @mrjj I'd try this. I am sure it would work. Thank you so much!


  • Lifetime Qt Champion

    Hi and welcome to devnet,

    Are you looking for something like the Scribble example ?



  • hi @elmLiu and Welcome!

    First of all, if you are going to use a QGraphicsView, always use the QGraphicsScene! I draw and interact with 10000s of elements (lines, arcs, text, points, etc.) and have real-time panning, zooming, and rotation at least 20Hz (I have clocked it at 100Hz) for the moving map! Actual mouse interaction is instantaneous.

    If all you want to do is draw something yourself, derive your own QGraphicsScene override the drawForeground method. This will give you the ability draw anything you desire.

    I use drawForeground to draw things like a site scale, compass rose, vehicle, or a navigation overlay over the linework and surfaces. If you do not want to have widgets as part of your scene, the QGraphicsView accepts and works nicely with layouts (see buttons on the right of the plan screen).

    0_1533326482036_7cb0ec68-9b84-40c5-889a-8faa9fa021c3-image.png

    Don't be afraid!



  • @Buckwheat Hi, before trying to use QGraphicsView & QGraphicsScene, I customized QWidget Class as my canvas, setting it central widget, and used mousePressEvent & mouseMoveEvent & mouseReleaseEvent & paintEvent to instantly update my canvas while using mouse to draw geometries, and also, to detect certain points on geometries such as end point & mid point. So far I've made it happen, but I just don't know how to do the same using QGraphicsView & QGraphicsScene.
    Which part can the drawForeground method do? I know that all items are stored in Scene and displayed via View, but in which class or whose method shall I try to instantly update my canvas?
    I'm new to Qt, may you forgive my doubts :)



  • Hi @elmLiu

    I was new to Qt back in 2012. You will find that you will learn to use it quickly. Being older (and hopefully wizened) I find that a lot of people like to build Qt from scratch (why I do not know). I just want to use it and learn it. It makes developing fun in some respects.

    Please derive MyGraphicsScene from QGraphicsScene and override mousePressEvent, mouseMoveEvent, mouseReleaseEvent and if desired, wheelEvent. These will give you the mouse position and the last mouse position in scene coordinates in QGraphicsSceneMouseEvent. From there you can do your magic! I pan, rotate, zoom, select, etc. using

    Again, if you wish to manage your own data and paint it yourself, you can override drawBackground (always is the bottom layer) and drawForeground (always the top layer) and you will have complete control. I do a mixture of both. As I do not want to manage 10000s of items but only the overlays I need.

    Unfortunately, if you want complete control of zooming, you have to jump through hoops to disable the scrolling features. I had to do this because of the nature of my work. I consider this the only problem with QGraphicsView. I will there were two classes: QGraphicsView (no scrolling at all and acts more like a pure painter) and QGraphicsScrollView (this woudl behave like the current).

    Enjoy!



  • @Buckwheat Thanks a lot! By overriding drawForeground method I reimplemented my drawing demand! I'll dig more of QGraphicsScene! : )



  • In your MyView::paintEvent write QPainter painter(viewport()); instead of QPainter painter(this);.



  • For those who still want to draw on a QGraphicsView instead of a viewport. For example when you set margins around the viewport with serViewportMargins() and want to draw something in that free space around the viewport.

    The quick answer:
    QPainter painter(this); in paintEvent() - leads to an error 'QWidget::paintEngine: Should no longer be called'.

    Instead of overriding painteEvent() you need to go with the overriding event() for events with QEvent::Paint type.

    bool MyGraphicsView::event(QEvent* e)
    {
        const bool result = QGraphicsView::event(e);
    
        if (e->type() == QEvent::Paint)
        {
             QPainter painter(this);
             // No errors 
             // paint what you need
        }
    
        return result;
    }
    

    The longer answer:
    Usually, all events for widgets come to event() function. The type of the event is checked and for example events with QEvent::Paint type are redirected to painterEvent() function, or events with QEvent::Resize type are redirected to resizeEvent() function, and so on.

    But that works in a different way for QAbstractScrollArea which is the base class for QGraphicsView. If you look into sources of QAbstractScrollArea::event() then you can see that there is some drawing code (btw calling QPainter p(this) with no errors) and the code follows to QFrame::paintEvent() instead of paintEvent(). But when paintEvent() of QGraphicsView is called then? It's called when the viewport widget (child widget) receives QEvent::Paint event.

    As you can know QWidget is the sub-class of QPaintDevice. But painting in fact can occur not on the widget itself but on so-called "redirected" painting device (probably the top-most parent, see QWidgetPrivate::setRedirected). The usual flow is next:

    1. set redirected device for the widget
    2. send paint event for the widget
    3. handle event in event() and redirect it to paintEvent()
    4. restore redirected device for the widget (set it to nullptr)

    So when the paint event is handled in QAbstractScrollArea::event() the redirected device for QGraphicsView is valid and there are no errors when creating and using a painter object.
    But when QAbstractScrollArea::paintEvent() is called the redirected device of the QGraphicsView is already restored and now it's enabled for the viewport widget instead. So at this moment using
    QPainter painter(this) causes an errors
    QPainter painter(viewport()) doesn't cause any errors.

    I don't think that qt developers made that intentionally so no one can draw on the QGraphicsView. I hope that will help someone:)