Skip to content

Instantly share code, notes, and snippets.

@akaszynski
Created July 11, 2023 21:37
Show Gist options
  • Save akaszynski/951b85620292e7eb55ab478705da120b to your computer and use it in GitHub Desktop.
Save akaszynski/951b85620292e7eb55ab478705da120b to your computer and use it in GitHub Desktop.
General PyQt tips and tricks

Implement Scaling

Scaling can only be set on initialization (or restart)

# Enable High DPI display with PyQt
os.environ.setdefault("QT_SCALE_FACTOR", str(rcParams["scale_factor"]))
if hasattr(QApplication, "setAttribute"):
    QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)

Better StyleSheet

Use qdarkstyle and implement it with:

import qdarkstyle

app = QApplication(sys.argv)
app.setStyleSheet(qdarkstyle.load_stylesheet(palette=qdarkstyle.LightPalette))

# simple method to enable/disable darkmode for the MainWindow

    def enable_dark_mode(self, state=True):
        """Enable or disable dark mode."""
        if state:
            self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())
        else:
            self.setStyleSheet(self.default_style_sheet)

Show loading activity within a status bar

Recommend using a status bar to show progress using a QProgressBar. Also, change the cursor to busy when running.

    def cursor_busy(self, busy):
        """Show a busy cursor or revert it to the default cursor.

        Parameters
        ----------
        busy : bool
            When ``True``, show a busy cursor. When ``False``, show the normal cursor.

        """
        if busy:
            QApplication.setOverrideCursor(Qt.WaitCursor)
        else:
            QApplication.restoreOverrideCursor()

Here's the status bar class:

class SystemUsageStatusBar(QStatusBar):
    """
    A custom QStatusBar to show real-time system usage.

    This status bar shows real-time CPU and memory usage at the bottom
    of a QMainWindow. The CPU usage and memory usage are shown in
    percentage and GB, respectively. The values are updated every second.

    Parameters
    ----------
    parent : Optional[QWidget]
        Parent widget.

    """

    _disable_progress_bar_signal = Signal()
    _increment_progress_signal = Signal(int)
    _initialize_progress_bar_signal = Signal(int, str)
    _set_progress_signal = Signal(int)
    _show_progress_bar_executing_signal = Signal()

    def __init__(self, parent=None):
        """Initialize this statusbar."""
        super().__init__(parent)

        # Initialize Progress bar
        self._progress_bar = QProgressBar()
        self._progress_bar.setRange(0, 100)  # default range 0-100%
        self._progress_bar.setValue(0)
        self._progress_bar.setFormat(f"Not Executing")
        self._progress_bar.setTextVisible(True)
        self._progress_bar.setEnabled(False)
        self._progress_bar.setMinimumWidth(200)
        self.addPermanentWidget(self._progress_bar, 1)

        # Add an empty widget with stretch factor to push CPU and Memory labels to the right
        self.addPermanentWidget(QLabel(), 1)

        # Initialize CPU usage label
        self._cpu_label = QLabel()
        self._cpu_label.setFrameStyle(QLabel.Panel | QLabel.Sunken)  # Add border
        self._cpu_label.setMinimumWidth(120)  # Set fixed width
        self.addPermanentWidget(self._cpu_label)

        # Initialize Memory usage label
        self._memory_label = QLabel()
        self._memory_label.setFrameStyle(QLabel.Panel | QLabel.Sunken)  # Add border
        self._memory_label.setMinimumWidth(180)  # Set fixed width
        self.addPermanentWidget(self._memory_label)

        # Initialize timer for updating the status bar
        self._status_timer = QTimer()
        self._status_timer.timeout.connect(self._update_status)
        self._status_timer.start(1000)  # update every second

        # Update the status bar for the first time
        self._update_status()

        # Connect signals
        self._disable_progress_bar_signal.connect(self._disable_progress_bar)
        self._increment_progress_signal.connect(self._increment_progress)
        self._initialize_progress_bar_signal.connect(self._initialize_progress_bar)
        self._set_progress_signal.connect(self._set_progress)
        self._show_progress_bar_executing_signal.connect(
            self._show_progress_bar_executing
        )

    def show_progress_bar_executing(self):
        """Show the status bar executing using a signal."""
        self._show_progress_bar_executing_signal.emit()

    def _show_progress_bar_executing(self):
        """Show the status bar executing."""
        self._progress_bar.setRange(0, 0)

    def initialize_progress_bar(self, value, label=""):
        """Set the maximum value and label of the progress bar using a signal."""
        self._initialize_progress_bar_signal.emit(value, label)

    def _initialize_progress_bar(self, max_value, label):
        """Set the maximum value of the progress bar."""
        self._progress_bar.setValue(0)

        if len(label) > 23:
            label = f"{label[:20]}..."

        # %p is replaced by the percentage completed.
        self._progress_bar.setFormat(f"{label} %p%")
        self._progress_bar.setMaximum(max_value)
        self._progress_bar.setTextVisible(True)
        self._progress_bar.setEnabled(True)

    def increment_progress(self, value):
        """Increments the progress bar by a specified value using a signal."""
        self._increment_progress_signal.emit(value)

    def _increment_progress(self, value):
        """Increments the progress bar by one tick."""
        if self._progress_bar.value() + value > self._progress_bar.maximum():
            self._progress_bar.setValue(self._progress_bar.maximum())
        else:
            self._progress_bar.setValue(self._progress_bar.value() + value)

    def set_progress(self, value):
        """Set the value of the progress bar to a specified value using a signal."""
        self._set_progress_signal.emit(value)

    def _set_progress(self, value):
        """Set the value of the progress bar to a specified value."""
        self._progress_bar.setValue(value)

    def disable_progress_bar(self):
        """Reset and disable the progress bar using a signal."""
        self._disable_progress_bar_signal.emit()

    def _disable_progress_bar(self):
        """Disable the progress bar."""
        self._progress_bar.setValue(0)
        self._progress_bar.setMaximum(1)
        self._progress_bar.setFormat(f"Not Executing")
        self._progress_bar.setEnabled(False)

    def disable_progress_bar_executing(self):
        """Disable the progress bar executing."""
        if self._progress_bar.maximum() == 0:
            self.disable_progress_bar()

    def _update_status(self):
        """
        Update the status bar with the current system usage.

        This method fetches the current system usage (CPU and memory) and
        updates the status bar with these values. It is connected to
        _status_timer's timeout signal and is called every second.

        """
        cpu_usage, used_memory_gb, total_memory_gb = get_system_usage()
        memory_usage_percent = (used_memory_gb / total_memory_gb) * 100

        self._cpu_label.setText(f"CPU Usage: {cpu_usage:5.1f}%")
        self._memory_label.setText(
            f"Memory Usage: {used_memory_gb:.1f}GB/{total_memory_gb:.1f}GB ({memory_usage_percent:.1f}%)"
        )

    def reset(self):
        """
        Reset this status bar.

        For now, this just disables the progress bar.
        """
        self.disable_progress_bar()

One way to avoid adding modifying each method that is long running is to use a protected_thread decorator and then setting the progress bar progress and cursor to "busy."

def protected_thread(fn):
    """Call a function using a thread.

    Reports error under the assumption the first argument is an object
    containing a ``main_window``.

    """

    @wraps(fn)
    def wrapper(*args, **kwargs):
        self = args[0]

        def protected_fn():
            try:
                if hasattr(self, "main_window"):
                    self.main_window.cursor_busy(True)
                    self.main_window.pbar_show_executing()
                ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment