Understanding paintEvent()



  • Hi,

    I'm very new to Qt and I'm trying to make a very simple program that does the following:
    -The program draws a square at the center of a fixed size window.
    -At a press of a QPushButton, the square moves up on the screen by the same amount as its height (e.g., if the square is 10 pixels in height, then move it 10 pixels upwards).

    The thing is, I don't understand how to update my window. The names for my classes are placeholders, I will change them later. Here are my files:

    -main.cpp:
    
    #include <QApplication>
    #include "mymain.h"
    
    int main(int argc, char **argv)
    {
     QApplication app (argc, argv);
    
     Mymain window;
     window.show();
    
     return app.exec();
    }
    
    -header:
    
    #ifndef MYMAIN_H
    #define MYMAIN_H
    
    #include <QWidget>
    
    class QPushButton;
    class Mymain : public QWidget
    {
        Q_OBJECT
    public:
        explicit Mymain(QWidget *parent = 0);
    protected:
        void paintEvent(QPaintEvent *event);
        void drawBody(QPainter *qp);
        QPushButton *button;
    signals:
    //    void upKeyPressed();     //placing this here for future reference, see below.
    private slots:
        void moveBody();
    };
    
    class Body
    {
    private:
        int xpos, ypos, width, height;
    public:
        Body (int x=245, int y=245, int w=10, int h=10):
        xpos(x), ypos(y), width(w), height(h) {}
    
        void moveSquare(char where)
        {
            if (where == 'u') {
                ypos += 10;
            }
        }
    };
    
    #endif // MYMAIN_H
    
    -mymain.cpp
    
    #include "mymain.h"
    #include <QPainter>
    #include <QPushButton>
    #include <iostream>
    
    Mymain::Mymain(QWidget *parent) : QWidget(parent)
    {
        setFixedSize(500, 500);
    
        button = new QPushButton("Move", this);
        button->setGeometry(5, 5, 40, 40);
    
        connect(button, SIGNAL(clicked()),
                this, SLOT(moveSnake()));
    }
    
    void Mymain::paintEvent(QPaintEvent *e) {
    
        Q_UNUSED(e);     // I don't really know what that does and what is the point of the drawBody() function.
    
        QPainter qp(this);
        drawBody(&qp);
    }
    
    void Mymain::drawBody(QPainter *qp) {
        qp->setBrush(QBrush("#c56c00"));
        qp->drawRect(245, 245, 10, 10);
    }
    
    void Mymain::moveBody() {
        std::cout << "Button Pressed!" << std::endl;
        Body body1;
        body1.moveSquare('u');
        //feed the new values to the drawBody() function and update the window?
    }
    

    I'd like at one point to have multiple squares, hence the Body class to generate multiple instances. I generated the first instance inside the function moveBody() for the sake of testing, I have yet to decide how I would proceed in dealing with these objects.

    My first instinct was, after creating the instance body1 and calling the method moveSquare(), to call paintEvent,

    paintEvent(/something?/, body1.xpos, body1.ypos, body1.width, body1.height);

    and add 4 parameters to my functions paintEvent() and drawBody() to put into the qp->drawRect();.
    I simply don't know what I could feed into the first parameter of paintEvent. Anyways, I'm pretty sure that's not the right approach since I read there is something like QtWidget::update() that triggers it.

    Finally, I added a button as a proof of concept, but I'd also like if I could somehow connect the press of the up arrow key to the moveBody() function (the void upKeyPressed(); signal).

    Thanks!



  • Also, some tips to type some code into the forums without it looking choppy or 2x the normal size would be appreciated :-P


  • Moderators

    Hi @Dekeon,

    Sounds like you want to implement a Snake game?

    Instead of doing low-level painting in a QWidget, I recommend using the Graphics View Framework.

    • Each square can be an instance of a QGraphicsRectItem.
    • Put the squares in a QGraphicsScene.
    • Display the scene with a QGraphicsView.

    Whenever you want to move a square, simply update the coordinates of the QGraphicsRectItem by calling QGraphicsItem::setPos(). The framework will take care of the painting; you don't need to write any painting code.

    Alternatively, you can use Qt Quick with the QML language. Each square can be a Rect instance.

    @Dekeon said in Understanding paintEvent():

    Also, some tips to type some code into the forums without it looking choppy or 2x the normal size would be appreciated :-P

    Put triple backticks ( ` ) before and after your code. I've done it for you in this case :)



  • Thank you for the suggestion and for the tip to insert proper code into the forum thread! I'll try using the Graphics View Framework!



  • I've tried multiple things but I don't understand the syntax of QGraphicsRectItem enough to make something work. I have 3 questions:

    1 - How do I create (and customize: color, size, location) and add a QGraphicsRectItem to my scene?

    2 - Is there a way to identify each QGraphicsRectItem? I made a vector to store the rectangles since I haven't found such an option.

    3 - At first I tried using scene->addRect(), but my rectangles were set relative to the center of the scene and not in an "absolute" coordinate system relative to the scene. If, for example, I set the following rectangles:

    addRect(0, 0, 9, 9, pen, brush);
    addRect(400, 0, 9, 9, pen, brush);

    they would be set 400 pixels apart, the first rectangle drawn at -200 and the second at +200. How can I set my QGraphicsRectItem so that the first one would be at the center of the screen and the other one at +400?


  • Moderators

    @Dekeon

    1. You can call QGraphicsScene::addRect() (as you've already found), or manually create a new QGraphicsRectItem and call QGraphicsScene::addItem(). To change the rectangle after creation, call:

      • QAbstractGraphicsShapeItem::setPen() and QAbstractGraphicsShapeItem::setBrush() (for colour)
      • QGraphicsRectItem::setRect() (for size)
      • QGraphicsItem::setPos() (for location).
    2. You can identify them by their pointers. There is no built-in way to name a QGraphicsItem, but you can store the pointers in a vector or a map. Also, have a look at https://doc.qt.io/qt-5/qgraphicsitem.html#setData

    3. There's a difference between the Scene's coordinates and the View's coordinates. Your code correctly puts the rectangles at (0, 0) and (400, 0) in Scene's coordinate system. However, the View tries to center all your scene's items by default.



  • Thank you very much for your help, I'll try that out!



  • I need concrete examples, I'm turning crazy! I try stuff but nothing seems to work. Sorry for the amount of questions asked, but I really want to understand the syntax of QGraphics.

    • What is the difference between the scene and the view?
    • Why will this work:
      -> as a private pointer of my class:
    QGraphicsScene *scene;
    

    -> and then in the constructor:

    scene = new QGraphicsScene(this);
    ui->graphicsView->setBackgroundBrush(QBrush(Qt::black, Qt::SolidPattern));
    ui->graphicsView->setScene(scene);
    

    but this won't work (or at least the background isn't black):

    QGraphicsScene scene(this);
    ui->graphicsView->setBackgroundBrush(QBrush(Qt::black, Qt::SolidPattern));
    ui->graphicsView->setScene(&scene);
    
    • For your 3rd point, you mentionned calling:
    view.setSceneRect(-405, -5, 810, 10);
    

    or

    view.setFixedSize(900, 600)
    

    which implies the instanciation of a view object?

    QGraphicsView view(this);
    

    I tried setting both the backgrounds of the scene and the view black, but the dialog screen opens white unless I use the first way of creating the scene above (with new).
    Why is the 4th parameter of

    view.setSceneRect(-405, -5, 810, 10);
    

    the number 10? Doesn't it represent the height of the view? Why would you want a height of 10?

    • I tried this:
    rectangle = new QGraphicsRectItem;
    QGraphicsScene::addItem(rectangle);
    

    which of course doesn't work and I don't know why.


  • Moderators

    @Dekeon said in Understanding paintEvent():

    I need concrete examples, I'm turning crazy! I try stuff but nothing seems to work. Sorry for the amount of questions asked, but I really want to understand the syntax of QGraphics.

    Hi @Dekeon, no worries about the amount of questions. This is what the forum is for! We're happy to help people who take the effort to learn.

    I'll answer your questions from the most basic to the most advanced, instead of the order you asked them. Make sure you understand my 1st answer before you move on to the 2nd answer, and so on.

    • Why.... this won't work (or at least the background isn't black):
    QGraphicsScene scene(this);
    ui->graphicsView->setBackgroundBrush(QBrush(Qt::black, Qt::SolidPattern));
    ui->graphicsView->setScene(&scene);
    

    You created the QGraphicsScene as a local variable in your class' constructor function. When the constructor finishes running, all of the local variables go out of scope and get destroyed. (This is how all C++ functions behaves. A constructor is a function.)

    This code is won't do what you want because your QGraphicsScene gets destroyed before your view is even shown on screen.

    • I tried this:
      ...
      which of course doesn't work and I don't know why.

    I know this isn't a question in itself, but I'd like to point out that it is important for you to spend time to read and understand your compiler's error messages. This is because they contain valuable information about what's wrong with the code.

    Also, when you ask for help in a forum about an error, people will often find it difficult to help you unless you provide the error message.

    rectangle = new QGraphicsRectItem;
    QGraphicsScene::addItem(rectangle);
    

    When I wrote "Call QGraphicsScene::addItem()", I wanted to show you which function to call (i.e. a function named addItem() that is a member of the QGraphicsScene class -- this follows the C++ convention of writing "Class::memberFunction()"). I wasn't showing you the call syntax. Sorry for making it unclear.

    The correct syntax is:

    QGraphicsScene *scene = new QGraphicsScene;
    QGraphicsRectItem *rectangle = new QGraphicsRectItem(0, 0, 9, 9); // Or any coordinates/size you like
    scene->addItem(rectangle);
    

    Note: The above code produces exactly the same results as

    QGraphicsScene *scene = new QGraphicsScene;
    QGraphicsRectItem *rectangle = scene->addRect(0, 0, 9, 9); // Or any coordinates/size you like
    

    It's just 2 different ways of doing the same thing. QGraphicsScene::addRect() requires less code, but a complex application might find good reasons to use QGraphicsScene::addItem() instead.

    • For your 3rd point, you mentionned calling:
    view.setSceneRect(-405, -5, 810, 10);
    

    or

    view.setFixedSize(900, 600)
    

    which implies the instanciation of a view object?

    QGraphicsView view(this);
    

    Kind of... but not quite.

    You definitely need an instance of a QGraphicsView in order to display your graphics items.

    However, since you are using a Qt Designer form, you already have one such instance. You can access this instance via ui->graphicsView.

    The example I gave didn't make any assumptions on how the QGraphicsView was instantiated. In my previous post's code, you can replace "view." with "ui->graphicsView->"

    Note: By writing QGraphicsView view(this); you are creating a local variable which will get destroyed as soon as the function ends. This is the same as the QGraphicsScene issue above.

    • What is the difference between the scene and the view?

    Think of the Scene as the "world" which contains all your QGraphicsItems. This world stretches infinitely in all directions.

    You need a View to render/display this world on your screen. Furthermore, since the world (scene) is infinitely large but your screen has a finite size, you need to tell the View which part of the world you want to see. By default, the View tries to fit all of your Items.

    The View lets you pan/scroll around the Scene. The View also lets you zoom in, zoom out, and rotate the Scene -- all without changing the Scene or the Graphics Items themselves.

    Even cooler (although you probably won't need this feature for now), you can have multiple Views rendering the same scene. For example, you can have:

    • One single Scene
    • One View showing all the Items in that Scene
    • One View zoomed into a few Items in the top-left corner of that Scene
    • One View showing all the Items in that Scene, but rotated 45 degrees.

    When you move an Item in a Scene, all of the attached Views update their display accordingly. If you want to see all this in action, load the 40000 Chips example in Qt Creator.

    Why is the 4th parameter of

    view.setSceneRect(-405, -5, 810, 10);
    

    the number 10? Doesn't it represent the height of the view? Why would you want a height of 10?

    The 10 represents the height of the sceneRect, which is the part of the Scene that you're interested in. Since both your squares have the same y-coordinate and the same height, then the sceneRect's height is your square's height. (The square itself is 9 pixels wide, but its border adds 1 pixel)

    • The sceneRect is set by QGraphicsView::setSceneRect(), and it tells the View which part of the Scene you are interested in.
    • The height of the View is set when you call QGraphicsView::resize() or QGraphicsView::setFixedSize().

    The View size can be different from the sceneRect.

    • If the View is bigger than the sceneRect, then it will display more than what you asked for while keepeing the sceneRect at the View's center.
    • If the View is smaller than the sceneRect, then it will give you scrollbars so that you can scroll around the sceneRect.

    Now, the question is: Which parts of the Scene are you interested in? Well,

    • Your 1st square covers (0, 0) to (9, 9) -- its center is (4.5, 4.5)
    • Your 2nd square covers (400, 0) to (409, 9).
    • The square's border adds an extra 0.5 pixels' width to each side of the square.
    • So, if you want show both squares while centering on (4.5, 4.5), then your scene needs to cover (-400.5, -0.5) to (+409.5, +9.5) -- this is 810x10.

    By the way, I made a mistake in my previous post. The code was supposed to be view.setSceneRect(-400.5, -0.5, 810, 10);



  • Wow, your answer clarifies a lot of things! Thank you so much for your patience.

    I'd like to verify if I understand the SceneRect right. Lets say I have these 2 squares:

    1. (-5, -5, 10, 10)
    2. (-16, -5, 10, 10)

    To center square 1. at (0, 0) on the screen, I would have to do:

    ui->graphicsView->setSceneRect(-16.5, -5.5, 33, 11);
    

    For the general case, I would find the furthest point of my furthest square from the center square (in my example, it would be -16.5 in the x axis, counting the 0.5 pixel border width, and -5.5 in the y axis) and set the width and height to twice the absolute value of these maximum coordinates. Again, in my example, 2*|-16.5| = 33 and 2*|-5.5| = 11. The same width and height would be found if the square the furthest from the center was in the positive x and y axis, only we have to set these coordinates negative. E.g., for the square (50, 15, 10, 10), its furthest point from the center would be (60, 25). Thus, we would do:

    ui->graphicsView->setSceneRect(-60.5, -25.5, 121, 51);
    

    Is that right?

    By the way, is there a simpler way of doing it?


  • Moderators

    You're welcome!

    @Dekeon said in Understanding paintEvent():

    I'd like to verify if I understand the SceneRect right. Lets say I have these 2 squares:

    1. (-5, -5, 10, 10)
    2. (-16, -5, 10, 10)

    The most important thing to understand is the relationship between the Scene, the sceneRect, and the View.

    The calculations are just general maths (coordinate geometry), nothing specific to the Graphics View Framework.

    To center square 1. at (0, 0) on the screen, I would have to do:

    ui->graphicsView->setSceneRect(-16.5, -5.5, 33, 11);
    

    Yep, you got it.

    Note: Since your main goal is to put Square 1 in the center, you just need to make sure that your sceneRect is symmetrical around the center square. All of the following will put Square 1 in the center, but #4 will give you the longest View scrollbars, while #1 will give you the shortest scrollbars.

    1. ui->graphicsView->setSceneRect(-16.5, -5.5, 33, 11);
    2. ui->graphicsView->setSceneRect(-33, -11, 66, 22);
    3. ui->graphicsView->setSceneRect(-330, -110, 660, 220);
    4. ui->graphicsView->setSceneRect(-660, -220, 1320, 440);

    For the general case, I would find the furthest point of my furthest square from the center square (in my example, it would be -16.5 in the x axis, counting the 0.5 pixel border width, and -5.5 in the y axis) and set the width and height to twice the absolute value of these maximum coordinates. Again, in my example, 2*|-16.5| = 33 and 2*|-5.5| = 11. The same width and height would be found if the square the furthest from the center was in the positive x and y axis, only we have to set these coordinates negative. E.g., for the square (50, 15, 10, 10), its furthest point from the center would be (60, 25). Thus, we would do:

    ui->graphicsView->setSceneRect(-60.5, -25.5, 121, 51);
    

    Is that right?

    Yep

    By the way, is there a simpler way of doing it?

    ​I think the simpler way is to:

    • Follow the conventions of 2D computer graphics
      • Think of (0, 0) as the top-left, not the center.
      • X increases to the right, Y increases downwards
    • Use positive coordinates only
    • Set the size of your sceneRect first, making sure that it is big enough. Then, put your square at the center of this area.
    // ASSUMPTION: The code below is called in the widget's constructor
    
    // Set sizes of the Scene and squares
    const qreal sceneWidth = 800;
    const qreal sceneHeight = 600;
    const qreal squareWidth = 10;
    
    // Calculate derived parameters
    qreal sceneXCenter = sceneWidth/2;
    qreal sceneYCenter = sceneHeight/2;
    qreal square0X = sceneXCenter - (squareWidth/2);
    qreal square0Y = sceneYCenter - (squareWidth/2);
    
    // Initialize the Scene and View
    auto scene = new QGraphicsScene(this);
    scene->setSceneRect(0, 0, sceneWidth, sceneHeight); // Note: You can set the sceneRect on the Scene instead of the View
    ui->graphicsView->setScene(scene);
    
    // Add squares
    //     * square0 is at the center of sceneRect
    //     * square1 is 300 pixels to the right of square0
    auto square0 = scene->addRect(square0X,     square0Y, squareWidth, squareWidth);
    auto square1 = scene->addRect(square0X+300, square0Y, squareWidth, squareWidth);
    

    This way, you don't need to think about the border width, and all your numbers are nicer.



  • I'm in principle one step away from coding the logic part of my project! One QGraphics obstacle remains:

    • How do I connect arrow keys events to a method of my main class in which I created the scene?

    When I tried adding the method keyPressEvent(), it was the scene (or view?) which was controlled by the arrow keys.
    I also tried creating a new class inheriting from QGraphicsRectItem and have one of my squares be an object of that new class that would handle those key events (I gave it focus). It seemed to work, but I have no way of calling a method of my main class without creating an instance of it and I couldn't use a static method because I'm using private attributes in that method I need to call.

    Solution online involved overriding the function keyPressEvent (since it seems you can't simply use connect), but I don't know how to implement that.


  • Moderators

    @Dekeon said in Understanding paintEvent():

    I'm in principle one step away from coding the logic part of my project!

    Great!

    One QGraphics obstacle remains:

    • How do I connect arrow keys events to a method of my main class in which I created the scene?

    When I tried adding the method keyPressEvent(), it was the scene (or view?) which was controlled by the arrow keys.
    I also tried creating a new class inheriting from QGraphicsRectItem and have one of my squares be an object of that new class that would handle those key events (I gave it focus). It seemed to work, but I have no way of calling a method of my main class without creating an instance of it and I couldn't use a static method because I'm using private attributes in that method I need to call.

    Solution online involved overriding the function keyPressEvent (since it seems you can't simply use connect), but I don't know how to implement that.

    Yes, you want to override keyPressEvent(). The easiest way is to override it in your main widget (the one which created the scene), not in the individual QGraphicsItems. Here is an example of how to override mousePressEvent() (it's very similar to how you handle a key press event): http://doc.qt.io/qt-5/eventsandfilters.html

    When your widget receives a key press event, check the key and update the position of the QGraphicsRectItem(s).



  • I tried it with the AWSD keys and it worked fine. The arrow keys don't seem to work, they simply nudge the scene by a little bit. I have this method in my Dialog class:

    void keyPressEvent(QKeyEvent *event) {
        switch (event->key()) {
            case Qt::Key_W : moveBody('u'); break;
            case Qt::Key_S : moveBody('d'); break;
            case Qt::Key_A : moveBody('l'); break;
            case Qt::Key_D : moveBody('r'); break;
        }
    }
    

    where Dialog::moveBody() handles the positions of my squares.

    If I use Qt::Key_Up, Qt::Key_Down, Qt::Key_Left and Qt::Key_Right instead, they go undetected, as if the scene handled all the arrow keys events and prevented them from reaching my keyPressEvent method.

    ////////////////////////////////////////////////////////////////////////////////////////////////////

    Edit 3: From my testing, scenePos() gives relative coordinates from the square's current location, hence why I always get (0, 0).

    Edit 2: Concerning the edit below, I used boundingRect() coordinates as an alternate solution.

    Edit 1: I'm using rect_pointer->scenePos().x() and rect_pointer->scenePos.y() to get the screen coordinates of my QGraphicsRectItems. For some reason, the debugger tells me both coordinates always get the value 0, regardless of the actual position of the squares. Why is that?

    Here is my for loop which moves each square:

    for (int i=1; i<=size_snake-1; i++) {
        int xtemp = snake[i]->scenePos().x();
        int ytemp = snake[i]->scenePos().y();
    
        if (size_turning_points != 0) {
            for (int z=0; z<=size_turning_points-1; z++) {
                if (xtemp == turning_points[z][0] && ytemp == turning_points[z][1]) {
                     directions[i] = directions[i-1];
                     break;
                }
            }
        }
        moveSquare(directions[i], snake[i]);
    }
    

    where:

    • snake is a vector <QGraphicsRectItem*> which contains all of my squares.
    • size_snake is an integer, the size of the vector snake.
    • turning_points is a vector <array<int,2>>, each array containing the coordinates where the snake needs to turn.
    • size_turning_points is an integer, the size of the vector turning_points .
    • directions is a vector <char> containing the direction in which each square is heading.
    • moveSquare is a method that handles the actual moving of the squares.

Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.