python で書いたプログラムを exe 化する際、めちゃくちゃ面倒だったので残しておきます。
開発環境:
- Python 3.9
- PyCharm
- Pyinstaller 4.9
- PySide2 の UI
下記を参考にすれば 画像以外の exe化までは問題なくいけました。
- 【Pyinstaller】Python exe化する方法 エラー対策あり!
- PyInstaller備忘録 2行でPython[.py]の[.exe]化
- [Python exe化]実行ファイル化したPythonプログラムではfileを使わない方が良い。[path バグる原因]
テストコード
下記のテストコードで見ていきます。
# -*- coding: utf-8 -*- import sys from PySide2 import QtCore, QtGui, QtWidgets from PySide2.QtCore import Qt class UISample(QtWidgets.QDialog): def __init__(self, parent=None): super(UISample, self).__init__(parent) layout = QtWidgets.QVBoxLayout() self.setLayout(layout) lbl = QtWidgets.QLabel() lbl.setFixedSize(300,300) # アイコンパス設定 pixmap = QtGui.QPixmap('D:\\testIcon\\gogle.png') scaled_pixmap = pixmap.scaled(300, 300, Qt.KeepAspectRatio) lbl.setPixmap(scaled_pixmap) layout.addWidget(lbl) def launch(): app = QtWidgets.QApplication(sys.argv) a = UISample() a.show() sys.exit(app.exec_())

このアイコンは絶対パスで設定しています。
これをそのまま exe 化した場合、自分のPC上では問題なく表示されます。
ただ、これは
'D:\\testIcon\\gogle.png'
この画像が PC 上に存在しているからです。
exe ファイルを誰かに渡した場合、その PC の同じ場所にこのアイコンがないと表示されません。
exe化したところで、絶対パスは絶対パスのまま です。
ここを参考に、この UI を exe化していきます。
ファイルパスの記述
パスをリソースのパスに変換する必要があります。
リソースパスを取得する関数
内容は後述しますが、下記の関数が必要です。
def resourcePath(filename): if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, filename) return os.path.join(filename)
パスをリソースパスへ変換
pixmap = QtGui.QPixmap(resourcePath('icon_path/gogle.png'))
パスの内容も変更します。 「 icon_path 」 については後で出てきます。ここの名前は何でもいいです。
exe 化
ここまでのコードが下記です。
# -*- coding: utf-8 -*- import sys from PySide2 import QtCore, QtGui, QtWidgets from PySide2.QtCore import Qt # リソースパスへ変換 def resourcePath(filename): if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, filename) return os.path.join(filename) class UISample(QtWidgets.QDialog): def __init__(self, parent=None): super(UISample, self).__init__(parent) layout = QtWidgets.QVBoxLayout() self.setLayout(layout) lbl = QtWidgets.QLabel() lbl.setFixedSize(300,300) # アイコンパス設定 # 元のコード : pixmap = QtGui.QPixmap('D:\\testIcon\\gogle.png') pixmap = QtGui.QPixmap(resourcePath('icon_path/gogle.png')) scaled_pixmap = pixmap.scaled(300, 300, Qt.KeepAspectRatio) lbl.setPixmap(scaled_pixmap) layout.addWidget(lbl) def launch(): app = QtWidgets.QApplication(sys.argv) a = UISample() a.show() sys.exit(app.exec_())
9行目の resourcePath 関数の設置と、28行目のパスの内容を変更しています。
これを一旦 exe化します。ターミナルで下記のコードを実行します。アイコンを設定したい場合はここで設定しておきます。
pyinstaller D:\hoge\hoge.py --onefile
そのままです。シンプルに onefileで exe化しています。
生成された exe ファイルを実行してもこの段階では画像が表示されません。

.spec ファイルの編集
exe化した際に生成される .specファイルを編集していきます。
# -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis(['D:\\hoge\\hoge.py'], pathex=[], binaries=[], datas=[], hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='launcher', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=True, disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None )
生成される .spec ファイルがこんな感じです。これを編集します。
icon_path の設定
Tree('アイコンのフォルダまでの絶対パス',prefix='icon_path'),
これを 23行目の exe = EXE(pyz,
の後に入れます。
exe = EXE(pyz, Tree('D:/testIcon',prefix='icon_path'), a.scripts, a.binaries,
ここで上で設定した「 icon_path 」がでてきます。
ここの文字列とコード内で設定したパスの先頭の文字列を同じにします。
console の設定
コンソールウィンドウを出したくない場合は 36行目の console
のステータスを False に変更します。
console=False,
spec ファイルを使用して exe化
ここまでの spec ファイルの内容が下記です。
# -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis(['D:\\hoge\\hoge.py'], pathex=[], binaries=[], datas=[], hiddenimports=[], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE(pyz, Tree('D:/testIcon',prefix='icon_path'), a.scripts, a.binaries, a.zipfiles, a.datas, [], name='launcher', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, disable_windowed_traceback=False, target_arch=None, codesign_identity=None, entitlements_file=None )
spec ファイルを使った exe化は pyinstaller
の後に絶対パスを指定するだけです。
pyinstaller D:\hoge\hoge.spec

画像が正しく表示されていれば成功です。他の PC 環境でも同じ見た目で起動できます。
QSS の画像指定
最後までハマったのがここでした。
例えば PySide の QCheckBox のインジケーターを QSS で変更している場合は上記の方法では表示されません。
理由としては resourcePath 関数で返ってくるパスの区切りが \
(バックスラッシュ)で返ってくるためです。
css では\
(バックスラッシュ) ではなく /
(スラッシュ)を使用する必要があります。
そのため、後から バックスラッシュが スラッシュ に変換されるように記述します。
self.checked_image = resourcePath('icon_path/hoge.png') self.setStyleSheet( 'QCheckBox::indicator:checked {' 'image: url('+ self.checked_image.replace('\\', '/') + ');' '}' )
これで QSS 内で使用する画像も exe化することができます。
QSS を exe化する際のコード
ちゃんとコード全文をみると下記のような感じです。
# -*- coding: utf-8 -*- import os import sys from PySide2 import QtCore, QtGui, QtWidgets from PySide2.QtCore import Qt # リソースパスへ変換 def resourcePath(filename): if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, filename) return os.path.join(filename) class UISample(QtWidgets.QDialog): def __init__(self, parent=None): super(UISample, self).__init__(parent) layout = QtWidgets.QVBoxLayout() self.setLayout(layout) # ラベル #----------------------------------------------------------------------- lbl = QtWidgets.QLabel() lbl.setFixedSize(300,300) # アイコンパス設定 # 元のコード : pixmap = QtGui.QPixmap('D:\\testIcon\\gogle.png') pixmap = QtGui.QPixmap(resourcePath('icon_path/gogle.png')) scaled_pixmap = pixmap.scaled(300, 300, Qt.KeepAspectRatio) lbl.setPixmap(scaled_pixmap) layout.addWidget(lbl) # チェックボックス #----------------------------------------------------------------------- check = QtWidgets.QCheckBox('テスト') check.setStyleSheet( 'QCheckBox::indicator:checked {' 'image: url(' + resourcePath('icon_path/gogle.png').replace('\\', '/') + ');' '}' 'QCheckBox::indicator:unchecked {' 'image: url('+ resourcePath('icon_path/gogle.png').replace('\\', '/') + ');' '}' ) layout.addWidget(check) def launch(): app = QtWidgets.QApplication(sys.argv) a = UISample() a.show() sys.exit(app.exec_())
exe化すると表示されます。
通常時でも UI を見れる状態にする
上記のコードは逆に exe化しないと画像が表示されません。
これはこれで exe化しないかぎり画像が確認できないため面倒です。
そこで、resourcePath の if文で通常時のパスを返すコードを書きます。
# -*- coding: utf-8 -*- import os import sys from PySide2 import QtCore, QtGui, QtWidgets from PySide2.QtCore import Qt exe_status = True # リソースパスへ変換 def resourcePath(filename): # exe化した際にリソースのパスを返す if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, filename) # 通常時は testIcon へのパスに繋げる else: return filename.replace('icon_path/', 'D:/testIcon/') class UISample(QtWidgets.QDialog): def __init__(self, parent=None): super(UISample, self).__init__(parent) layout = QtWidgets.QVBoxLayout() self.setLayout(layout) # ラベル #----------------------------------------------------------------------- lbl = QtWidgets.QLabel() lbl.setFixedSize(300,300) # アイコンパス設定 # 元のコード : pixmap = QtGui.QPixmap('D:\\testIcon\\gogle.png') pixmap = QtGui.QPixmap(resourcePath('icon_path/gogle.png')) scaled_pixmap = pixmap.scaled(300, 300, Qt.KeepAspectRatio) lbl.setPixmap(scaled_pixmap) layout.addWidget(lbl) # チェックボックス #----------------------------------------------------------------------- check = QtWidgets.QCheckBox('テスト') check.setStyleSheet( 'QCheckBox::indicator:checked {' 'image: url(' + resourcePath('icon_path/check.png').replace('\\', '/') + ');' '}' 'QCheckBox::indicator:unchecked {' 'image: url('+ resourcePath('icon_path/check.png').replace('\\', '/') + ');' '}' ) layout.addWidget(check) def launch(): app = QtWidgets.QApplication(sys.argv) a = UISample() a.show() sys.exit(app.exec_())