pythonsvgpyqtpyside6qtstylesheets

Pyside6 - Change svg icon color dynamically


I am trying to build a pyside6 application with a light mode and a dark mode. That means that my icons also need to be light and dark mode.

Now I was hoping to set the color of the QIcon SVGs inside my qss stylesheet with e.g.

#menu_button {
  color: white;
}

I had the hope that fill="currentColor" inside the svg would make it possible for the icon to inherit the color property from the QSS, but it seems that this is not the case.

Is there a good way to dynamically switch the color of svg icons? I would like to have a setting with "Light Mode" and "Dark Mode" or something like this that each loads a custom color set from their stylesheets.


Solution

  • Simply put, this is not possible, at least not just by setting the color of the widget.

    The reason resides on the fact that QIcon doesn't represent an image or a file: it actually is an abstraction layer that is queried by widgets when they need to display a pixmap at a desired size.

    Every icon has its own icon engine, responsible of actually drawing the pixmap when required), and when loading icons from a file the related icon engine is used.

    For instance, when loading an SVG file, QIcon internally creates an instance of a QIconEngine subclass that is able to read SVG files and render their contents when required.

    When icons are drawn (for instance in a button), the Qt style normally calls the pixmap() function of the icon, which internally calls the pixmap(), which has the following argument signature:

    QIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state)

    As you can see, there is absolutely no reference about the context in which the icon is drawn (such as the text color), so there is no way to change the icon rendering just by trying to change the palette, as that context is never passed on.

    QIcon and QIconEngine have a paint() function, which passes a QPainter reference, so, in theory it would be possible to at least try to get the current pen/brush colors from the painter, but there is absolutely no guarantee that those would be set with the appropriate foreground color; also, that function is only called from some classes and under specific circumstances: even the internal QStyleSheetStyle still uses pixmap() for button icons.

    Considering the above, what you ask is not directly possible: if you want to use different color schemes, you need to have different icon sets for each one of them.

    There is a way to simplify the way those different icon sets are chosen, though, which is to use a little known feature of QDir, the setSearchPaths() function.

    The procedure is the following:

    1. create the two icon sets, while keeping the same file names, and put them in two different directories (this also works for Qt resources);
    2. call setSearchPaths() with an appropriate prefix and the target folder for the current color scheme, for instance:
    hints = QApplication.styleHints()
    if hints.colorScheme() == Qt.ColorScheme.Dark:
        path = '<path-to-light-icons>'
    else:
        path = '<path-to-dark-icons>'
    QDir.setSearchPaths('icons', [path])
    
    1. then, whenever you need to set an icon, you can use the above prefix:
    someButton.setIcon(QIcon('icons:someIcon.svg'))
    

    Note that calling setSearchPaths() doesn't work dynamically: it won't automatically change the icon once called, which means that: