Live Reloading Qt Stylesheets

posted in: Uncategorized | 0

The more time I spend in PySide, the more I demand of it. Lately, I’ve put a lot of emphasis on the look and feel of my applications and have ventured deep into custom widget styles. Stylesheets do an awesome job at separating concerns of the functionality from the visuals. To date, they are my favorite system to change the appearance of QWidgets.

During a design phase, I found myself iterating rapidly on stylesheets to tweak the pixels between margins or dial in colors, all while having to relaunch my Python process between changes. This feedback loop is not immediate enough and gets tiresome pretty quickly. Today I’m sharing my recipe for live reloading stylesheets in the PySide framework! (This code should be easily reusable across PyQt as well.) The core is quite simple. Hold onto references of stylesheets when they’re applied to widgets and add QFileSystemWatchers to watch when they change. On modification, reload the stylesheet from disk and reapply the sheet to the widgets.

Here’s a quck demo. On the right side, I’m modifying the .css stylesheet. On the left side, the app window updates every time I save the file.

The StylesheetWatcher Class

from PySide import QtCore
import collections

class StylesheetWatcher(object):
    """
    A utility class for live-reloading Qt stylesheets.

    When watched, all changes made to a .css file automatically
    propagate to any running apps with live stylesheets.
    """

    def __init__(self):
        self._widget_sheet_map = collections.defaultdict(list)  # One-to-many map of stylesheets to their widgets
        self._watcher = None

    def watch(self, widget, stylesheet_path):
        """
        Establishes a live link between a widget and a stylesheet.
        When modified, the stylesheet will be reapplied to the widget.

        Args:
            widget: (QtGui.QWidget) A widget to apply style to.
            stylesheet_path: (str) Absolute file path on disk to a .css stylesheet.

        Returns: (None)
        """
        self._widget_sheet_map[stylesheet_path].append(widget)

        self._watcher = QtCore.QFileSystemWatcher()
        self._watcher.addPath(stylesheet_path)

        self._watcher.fileChanged.connect(self.update)

        self.update()

    def update(self):
        """
        Callback to reload and reapply a stylesheet when changed by the file system watcher.

        Returns: (None)
        """
        for stylesheet_path, widgets in self._widget_sheet_map.iteritems():
            with open(stylesheet_path, "r") as fid:
                raw_stylesheet = fid.read()

            for widget in widgets:
                widget.setStyleSheet(raw_stylesheet)

I’ll upload a complete demo app in the coming days with my “Steam” clone stylesheet. Happy designing!