PySide Python

UI に画像を埋め込んだ Python プログラムを exe化する

python で書いたプログラムを exe 化する際、めちゃくちゃ面倒だったので残しておきます。

開発環境:

  • Python 3.9
  • PyCharm
  • Pyinstaller 4.9
  • PySide2 の UI

下記を参考にすれば 画像以外の exe化までは問題なくいけました。

テストコード

下記のテストコードで見ていきます。

# -*- 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_())

-PySide, Python