How do I add an icon via Stylesheet using PyInstaller --onefile option with resource_path in PyQt5?

Viewed 641

So basically what I want to do is add a down arrow to a combobox via Stylesheet(QSS) so I can bundle the py.file with --onefile option using PyInstaller: The code is this but it doesn't seem to work :

QComboBox::drop-down{
    image: url(resource_path("icon_example.png"))
}

and the resource_path method is :

def resource_path(self,relative_path):
    try:
       base_path = sys._MEIPASS
     except Exception:
       base_path = os.path.abspath(".")
     return os.path.join(base_path, relative_path)
3 Answers

The following code is what I used and it worked perfectly.

import sys
from os.path import join, abspath
def resource_path(relative_path):
    if hasattr(sys, '_MEIPASS'):
        return join(sys._MEIPASS, relative_path)
    return join(abspath("."), relative_path)

Add the following code in spec file:

a.datas += [ ('back.png', 'icon/back.png', 'DATA')]

where 'back.png' is my image path will be saved in pyinstaller exe bundle, and 'icon/back.png' is I put my image file in icon folder currently before packaging.

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['main.py'],
             pathex=['C:\\Users\\User\\Documents\\GitHub\\Mypath'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=['mkl','whl'],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)

a.datas += [ ('back.png', 'icon/back.png', 'DATA')]
a.datas += [ ('main.ico', 'icon/main.ico', 'DATA')]
a.datas += [ ('folder.png', 'icon/folder.png', 'DATA')]
a.datas += [ ('download.png', 'icon/download.png', 'DATA')]

pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='main',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True , icon='icon\\main.ico')

Then you can call you image like resource_path('download.png')

It's an old topic, but my answer could be useful for others (widely inspired by https://forum.qt.io/topic/17318/solved-using-variables-in-style-sheets-qss/11)

As mentioned, the *.spec file must contain a link to the files to include in the *.exe:

added_files = [
    ( './images', 'images' ),
    ( './fonts', 'fonts' )
]

a = Analysis(['MyScript.py'],
             [...]
             datas=added_files
             [...]

The function to get the relative path:

def relative_path(relative_path):
    try:
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

Then replace the text "iconExample" inside the styleSheet by the variable (relative path) iconExample:

iconExample = relative_path("./images/icon_example.png")).replace("\\", "/")

styleSheet = '''
QComboBox::drop-down{
    image: url(iconExample);
}
'''
styleSheet = styleSheet.replace("iconExample", iconExample)
myWidget.setStyleSheet(styleSheet)

I don't know if it's the best solution, but it works.

you should use absolute path in qss url reference. you can use a function to repalce that path content and then config your icon path into the *.exe in .spec file.

for example in qss file:

QComboBox::down-arrow
{
    image: url(QSS/QSS_IMG/arrow-down.png);
    width: 8px;
    height: 8px;
}


def replace_qss_path(content: str)->str:
    base_path = getattr(sys,"_MEIPASS",os.path.abspath(os.path.join(os.path.dirname(
    os.path.abspath(__file__)))))
    p = re.compile("url *[(](.*?)[)]", re.S)
    c_l = re.findall(p, content)
    for item in c_l:
        if not item.startswith(":"):
            content = content.replace(item, os.path.join(base_path, item).replace("\\","/"))
    return content

def get_qss():
    with open(resource_path("QSS/Integrid.qss"), "r", encoding="utf-8") as f:
        content = f.read()
        return replace_qss_path(content)


class MainWindow(QtWidgets.QMainWindow, Ui_MainWin):
    def __init__(self) -> None:
        super().__init__()
        self.setupUi(self)
        self.setStyleSheet(get_qss())

and in main.spec:

datas=[("E:/QSS/Integrid.qss", "QSS"),
 ("E:/QSS/QSS_IMG/*","QSS/QSS_IMG")
 ]
Related