Manage QIcon instances on a application global level

Maurizio Berti maurizio.berti at gmail.com
Fri Jul 7 04:04:56 BST 2023


[Addendum]

I just realized that my idea can be furtherly improved in a very
interesting way, so, thank you Christian and everybody else.

I suspect that almost anybody reading this has mixed feelings about the
verbosity, readability and length of Qt namings (especially after the Enum
introduction).

Saving some characters could make our "readabilitiness" simpler, so here's
my proposal: a basic camelCase regex that creates a syntax valid for the
standard fromTheme() syntax.

_camelCaseSplit =
re.compile(r'.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)')
class MetaIcon(type):
    _icons = {}
    def __getattr__(self, attr):
        if attr in self._icons:
            return self._icons[attr]
        match = _camelCaseSplit.finditer(attr)
        if match:
            self._icons[attr] = icon = QIcon.fromTheme(
                '-'.join(m.group(0).lower() for m in match))
            return icon
        return QIcon()

class Icon(metaclass=MetaIcon): pass

Note that I'm not a regex expert, I suppose that there could be better
syntaxes for this case (especially considering single letters, such as
those used in MIME types), but this is mainly a proof of concept.

The above allows something like the following:

undoButton = QPushButton(Icon.EditUndo, 'Undo') # as in:
QIcon.fromTheme('edit-undo')
quitAction = QAction(Icon.ApplicationExit, 'Quit') # or:
QIcon.fromTheme('application-exit')

If anybody has further insights or suggestions, I'll be glad to read them.

Best regards,
MaurizioB

Il giorno ven 7 lug 2023 alle ore 02:46 Maurizio Berti <
maurizio.berti at gmail.com> ha scritto:

> The main issue with commonly used resources like QIcon is that they
> require an existing QGuiApplication instance.
>
> When using a single script with the canonical if __name__ == '__main__':
> that's not a real problem, since the module can be loaded within that block
> and it can be accessed from outside it due to the global scope.
>
> Unfortunately, this is obviously not possible for modules that are not the
> main script but still use those QIcons and are imported before the
> QApplication construction.
>
> A possible solution could be to create a custom metaclass that actually
> returns a QIcon and a class based on it that implements its "enum", then
> import that class instead.
>
> The concept is similar to that of enums, with the difference that the
> returned value is actually a QIcon.
> Using an object reference is required since the "attribute" is not static,
> but it's returned at runtime.
>
> Since we can assume that an icon will only be required from a QWidget
> instance, using this approach should be safe enough.
>
> Let's suppose that this is the content of a MyIcons.py script:
>
> class MetaIcon(type):
>     _names = {
>         PAUSE: 'media-playback-pause',
>         ...
>     }
>     def __getattr__(self, attr):
>         if attr in self._names:
>             return QIcon.fromTheme(self._names[attr])
>         return QIcon()
>
> class Icon(metaclass=MetaIcon): pass
>
> The above is necessary because an unknown class attribute has to be that
> of a "class of the class" (aka, a type): you cannot just create a basic
> Icon class and override __getattr__, since it only works on the instance.
> Doing that in the metaclass is fine, because it works on the "instance" of
> the type.
>
> Then, you can just import Icon from that module, and `Icon.PAUSE` will
> return a proper QIcon.
>
> from MyIcons import Icon
> ...
> button = QPushButton(icon=Icon.PAUSE)
>
> Note that the constructor requirement of QIcon (as it goes for QPixmap)
> remains: it shouldn't be created without an existing QApplication instance;
> this means that you should not try to create class attributes using the
> "icon enum" above (for example, to create class defaults).
>
> If you want to achieve some level of optimization (to avoid unnecessary
> duplicates of QIcon instances - hence, further QIconEngine instances) or
> code tidiness, use the @cached_property decorator or create a null class
> attribute, then overwrite it at the class level within the instance init:
>
> class MyPlayer(QWidget):
>     icons = None
>     def __init__(self, *args, **kwargs):
>         super().__init__(*args, **kwargs)
>         if not self.icons:
>             self.icons.__class__.icons = [Icons.PLAY, Icons.PAUSE]
>     ...
>     def playerStateChanged(self, state):
>         self.playButton.setIcon(self.icons[state])
>
> The above will also work for deeper inheritance levels as long as the
> super __init__ is called first, because the __init__ of the super class
> will automatically make the attribute valid.
>
> Best regards,
> MaurizioB
>
> Il giorno gio 6 lug 2023 alle ore 12:15 <c.buhtz at posteo.jp> ha scritto:
>
>> Hello,
>>
>> I do use PyQt5.
>>
>> I have a module "icon.py" in my application containing lines with
>> constants like this:
>>
>>      PAUSE = QIcon.fromTheme('media-playback-pause')
>>
>> Somewhere else in my application I do use "icon.PAUSE". But before doing
>> this I have to "import icon" of course.
>>
>> The problem is that I'm not able to put the import statement in the
>> beginning of the py files like you usually do it with imports. I have to
>> put it e.g. in the __init__() method of my QMainWindow derived classed
>> and after QApplication() was instanciated. I understand why it is that
>> way.
>>
>> But maybe you can explain how you do manage icons in your application?
>>
>> Kind
>> Christian
>>
>
>
> --
> È difficile avere una convinzione precisa quando si parla delle ragioni
> del cuore. - "Sostiene Pereira", Antonio Tabucchi
> http://www.jidesk.net
>


-- 
È difficile avere una convinzione precisa quando si parla delle ragioni del
cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20230707/d0181823/attachment.htm>


More information about the PyQt mailing list