troubleshooting2024-04-01·8·315/348

PyQt5 GUI 렌더링 에러 해결

Troubleshooting PyQt5 GUI rendering issues — paint device errors, widget update loops, threading violations, and display scaling problems.

PyQt5 GUI 렌더링 에러 해결

Introduction

PyQt5 is a powerful GUI framework, but rendering errors can be some of the most frustrating problems to debug. Unlike console errors that give you a traceback, GUI rendering issues often manifest as blank windows, frozen UIs, or cryptic paint device errors that point to low-level Qt internals.

I encountered several PyQt5 rendering issues while building a real-time trading dashboard in Lisbon. The application needed to display live price charts and order book data, and the rendering errors occurred intermittently under high update rates.

Environment

Python 3.12.3
PyQt5 5.15.11
Qt 5.15.2
Linux 6.5.0 (X11)

Problem

Error 1: QPaintDevice: Cannot paint device

from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QPainter
from PyQt5.QtCore import QTimer

class ChartWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.data = []
        self.timer = QTimer()
        self.timer.timeout.connect(self.update_chart)
        self.timer.start(100)  # Update every 100ms
    
    def update_chart(self):
        self.data.append(len(self.data))
        self.update()  # Triggers paintEvent
    
    def paintEvent(self, event):
        painter = QPainter(self)
        # This crashes if widget is not yet shown
        painter.drawRect(0, 0, self.width(), self.height())
        painter.end()

app = QApplication([])
widget = ChartWidget()
widget.show()
# Sometimes crashes with: QPaintDevice: Cannot paint device

Error 2: Maximum update recursion

class BadWidget(QWidget):
    def paintEvent(self, event):
        # This triggers another update, causing infinite recursion
        self.update()
    
app = QApplication([])
widget = BadWidget()
widget.show()
# RuntimeError: maximum recursion depth exceeded

Error 3: GUI freeze from main thread blocking

class MainWindow(QMainWindow):
    def start_processing(self):
        # This blocks the main thread
        result = long_running_operation()
        self.label.setText(result)
    
    def long_running_operation(self):
        import time
        time.sleep(10)  # GUI frozen for 10 seconds!
        return "Done"

Error 4: Display scaling issues on HiDPI

# Text and widgets appear blurry or too small on HiDPI displays
app = QApplication([])
# No DPI scaling configured
app.exec_()

Analysis

PyQt5 rendering errors have several root causes.

Root Cause 1: Painting before widget is shown. QPainter requires the widget to be fully initialized and visible. Painting before show() is called can cause QPaintDevice errors.

Root Cause 2: Update loops. Calling self.update() inside paintEvent() creates an infinite recursion because paintEvent() is called by update().

Root Cause 3: Main thread blocking. PyQt5 requires all GUI operations on the main thread. Blocking the main thread freezes the UI.

Root Cause 4: HiDPI scaling. PyQt5 does not automatically handle high-DPI displays. Without proper configuration, the UI appears blurry or too small.

Solution

Fix 1: Safe painting with guards

from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QPainter, QColor
from PyQt5.QtCore import QTimer

class SafeChartWidget(QWidget):
    def __init__(self):
        super().__init__()
        self.data = []
        self._needs_update = False
        self.setMinimumSize(400, 300)
    
    def add_data_point(self, value):
        self.data.append(value)
        self._needs_update = True
        self.update()  # Safe — does not paint immediately
    
    def paintEvent(self, event):
        if not self.isVisible():
            return  # Guard against painting before shown
        
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        
        # Clear background
        painter.fillRect(self.rect(), QColor(255, 255, 255))
        
        # Draw data
        if self.data:
            pen = painter.pen()
            pen.setWidth(2)
            painter.setPen(pen)
            
            width = self.width()
            height = self.height()
            
            for i, value in enumerate(self.data):
                x = i * width / max(len(self.data), 1)
                y = height - (value / max(max(self.data), 1) * height)
                painter.drawPoint(int(x), int(y))
        
        painter.end()
        self._needs_update = False

Fix 2: Prevent update loops

class NoLoopWidget(QWidget):
    def __init__(self):
        super().__init__()
        self._painting = False
    
    def paintEvent(self, event):
        if self._painting:
            return  # Prevent re-entry
        
        self._painting = True
        try:
            painter = QPainter(self)
            # Safe painting here
            painter.end()
        finally:
            self._painting = False

Fix 3: Use QThread for long operations

from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QMainWindow, QLabel, QPushButton

class WorkerThread(QThread):
    finished = pyqtSignal(str)
    
    def run(self):
        result = long_running_operation()
        self.finished.emit(result)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.label = QLabel("Processing...")
        self.setCentralWidget(self.label)
        
        self.button = QPushButton("Start")
        self.button.clicked.connect(self.start_processing)
    
    def start_processing(self):
        self.button.setEnabled(False)
        self.thread = WorkerThread()
        self.thread.finished.connect(self.on_complete)
        self.thread.start()
    
    def on_complete(self, result):
        self.label.setText(result)
        self.button.setEnabled(True)

Fix 4: HiDPI scaling configuration

import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt

def main():
    # Enable high-DPI scaling
    QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
    
    app = QApplication(sys.argv)
    
    # Set application-level font for readability
    from PyQt5.QtGui import QFont
    font = QFont("Arial", 10)
    app.setFont(font)
    
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

Fix 5: Throttled updates for high-frequency data

from PyQt5.QtCore import QTimer

class ThrottledChart(QWidget):
    def __init__(self):
        super().__init__()
        self.buffer = []
        self.update_timer = QTimer()
        self.update_timer.timeout.connect(self.flush_buffer)
        self.update_timer.start(50)  # Max 20 FPS
    
    def add_data(self, value):
        self.buffer.append(value)  # Buffer incoming data
    
    def flush_buffer(self):
        if self.buffer:
            self.data.extend(self.buffer)
            self.buffer.clear()
            self.update()  # Single update per frame

Lessons Learned

  • Always guard paintEvent() with visibility and initialization checks.
  • Never call self.update() inside paintEvent() — use flags and check them in the event loop.
  • Use QThread or QRunnable for long-running operations to keep the GUI responsive.
  • Enable high-DPI scaling at application startup for modern displays.
  • Throttle high-frequency updates to prevent GUI freezing and excessive repaints.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.