paintEvent called incorrectly when monitor changed
-
Hi there.
My program suddenly crash. I implemented a custom dialog widget and it has overriden paintEvent member. This widget has no parent and has next flags:- Qt::FramelessWindowHint
- Qt::WindowStaysOnTopHint
- Qt::Tool
These flags allows me to draw this widget in a new window without frame, it doesn't appears in a task panel and in system task monitor.
My program crashed after next actions:
- my_widget.show(), all okay, widget shows normally
- my_widget.hide(), all okay, program alive
- drag program's window to another monitor, all okay, program still alive
- my_widget.show()
After that program put these strings in output:
QPainter::begin: A paint device can only be painted by one painter at a time. QPainter::setCompositionMode: Painter not active QWidget::repaint: Recursive repaint detected QBackingStore::endPaint() called with active painter; did you forget to destroy it or call QPainter::end() on it?
I tried to find out what happens and place many
qDebug() << "0";
in paint event handler's code and find out that before program crash this handler called twice. Further more, when paintEvent called second time, the first call isn't complete.Guys, what do you think about it? Looks like I forgot something necessary to handle second monitor.
-
@Artem-Shapovalov said in paintEvent called incorrectly when monitor changed:
Guys, what do you think about it?
Without code and the Qt version you're using?
Minimize your program until it no longer crashes and post it here then.
-
@Christian-Ehrlicher
I use Qt 6.5 on Windows 11. I found what caused problem. Minimum code to reproduce it:CrashWidget.h:
#ifndef CRASHWIDGET_H #define CRASHWIDGET_H #include <QWidget> #include <QPainter> /** \brief example widget that covers selected area with new empty window */ class CrashWidget : public QWidget { Q_OBJECT public slots: /** \brief makes window visible * \param global coordinates of area that should be covered */ void invoke(QRect globalButtonCoordinates) { // setGeometry(globalButtonCoordinates); // <- no fault, no crash global = globalButtonCoordinates; show(); } public: /** \brief constructor * \details set up the widget window properties and hides it * \param parent pointer to the parent widget, may be null */ explicit CrashWidget(QWidget *parent = nullptr) : QWidget(parent) { setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool); hide(); } /** \brief paint event handler * \details called by Qt on it's own, set up geometry and fill itself by * green color * \param ev pointer to the event data */ virtual void paintEvent(QPaintEvent *ev) override { // setGeometry(global); // <- fault, but no crash QPainter paint(this); setGeometry(global); // <- fault and crash paint.setPen(Qt::NoPen); paint.setBrush(Qt::green); paint.drawRect(rect()); } /** \brief handler of the mouse leaving widget's area event * \details called by Qt on it's own * \param ev pointer to the event data */ virtual void leaveEvent(QEvent *ev) override { hide(); } private: /** \brief global coordinates of the widget * \details updates by invoke() and used in paintEvent() to change * area of widget */ QRect global; }; #endif // CRASHWIDGET_H
Somewhere in mainwindow.cpp:
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ... CrashWidget *crash = new CrashWidget(ui->pushButton); QObject::connect( ui->pushButton, &QPushButton::clicked, this, [this, crash]() { QPoint topLeft(ui->pushButton->x(), ui->pushButton->y()); QPoint bottomRight( ui->pushButton->x() + ui->pushButton->width(), ui->pushButton->y() + ui->pushButton->height()); QRect global = QRect(QWidget::mapToGlobal(topLeft), QWidget::mapToGlobal(bottomRight)); crash->invoke(global); }); ... }
Explaination:
CrashWidget invokes a new window without frame and fill it with green color. Seems to be that setGeometry() should not call in event handlers, at least at the paintEvent(), because it produce new events. Online documentation warns against call it inside resizeEvent() and moveEvent() only, but as you see this list should be extended.Thanks for pay attention to this problem. I hope it helps to somebody, I googled it for a day and a half with absolutely no result before start to write minimum example and try to crash it.
-
@Artem-Shapovalov said in paintEvent called incorrectly when monitor changed:
new CrashWidget(ui->pushButton);
Why are you passing ui->pushButton as parent? If you want to show CrashWidget as a window it should not have any parent.
-
@Artem-Shapovalov said in paintEvent called incorrectly when monitor changed:
Online documentation warns against call it inside resizeEvent() and moveEvent() only, but as you see this list should be extended.
I guess there are lots of functions which should not be called inside
paintEvent()
, basically anything which causes a re-paint.... -
@jsulm but why? There are many cases when window should have parent. For example, popup-window or hint should hide when the main window hides, it should change it's position when the main window resizes. I thought that is a common practice. What's the correct way to notify popup-window about actions of parent?
-
@Artem-Shapovalov You wrote "CrashWidget invokes a new window without frame" that's why I asked why you're passing a parent.
But there is another strange thing: why do you use a button as parent?! A widget with a prent widget will be put inside its parent - do you really want CrashWidget to be inside a button? -
@JonB online docs says:
Note: Generally, you should refrain from calling update() or repaint() inside a paintEvent(). For example, calling update() or repaint() on children inside a paintEvent() results in undefined behavior; the child may or may not get a paint event.
I didn't knew that setGeometry() call repaint() or update(), I thought it change rectangle only and produce resize or move events. But you're right, produce any event inside event handler is bad idea.
-
@jsulm yes, I need a new popup-window. I checked it. Invoke method:
/** \brief makes window visible * \param global coordinates of area that should be covered */ void invoke(QRect global) { setGeometry(global.x(), global.y(), global.width(), global.height() + 100); show(); }
The green rectangle is really taller than button. Did I found some undocumented and undefined behavior? :)
-
@Artem-Shapovalov said in paintEvent called incorrectly when monitor changed:
I need a new popup-window
A window can't be inside another widget. Are you sure you're using correct wording?
-
@jsulm yeah, maybe I used wrong word. I'm sorry, I'm not a native English speaker and also I'm a newbie in Qt. Actually, in GUI applications too.
I tried to implement a custom popup dialog and use the frameless window for it. In this example green rectangle is a frameless window. And it works as I expected, although parent is passed to constructor and applied.
/** \brief constructor * \details set up the widget window properties and hides it * \param parent pointer to the parent widget, may be null */ explicit CrashWidget(QWidget *parent = nullptr) : QWidget(parent) { setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool); hide(); }
I think it works as needed because setWindowFlags(Qt::Tool). This flag can be represented as bitwise OR: Qt::Dialog | Qt::Popup. Both of these flags contains in their mask Qt::Window flag that Qt handles and draws a window. Maybe I wrong, but drawing widget in a new window if parent is not specified is a side effect, and seems to be that Qt just produces new window and place widget inside it in this case.
-
@Artem-Shapovalov said in paintEvent called incorrectly when monitor changed:
setGeometry(global); // <- fault and crash
You must not call setGeometry inside a paint event as this may trigger a repaint and therefore an infinite loop.
-