PyInstaller+PyQtでアイコン対応

# はじめに

Pythonを使ってちょっとしたGUIアプリを作るときに，よくPyQtで書いてそれをPyInstallerでexeにパッケージングしています．
その時にお気に入りのアプリはアイコンもつけておきたいので，その方法についてまとめます．

対応したいアイコン表示は以下の二通りです．

* エクスプローラに表示されるexeファイルのアイコン
* タイトルバーやタスクバーに表示されるアイコン

## 開発環境

今回確認した環境は以下の通りです．

* Windows10 Pro
* Python 3.6.4
* PyInstaller 3.3.1
* PyQt5 5.10.1

## インストール

PyInstallerとPyQtは`pip`からインストールができます．

```console
> pip install pyinstaller pyqt5
```

# exeファイルのアイコン

PyInstallerでパッケージングするときにアイコンをつけることができます．

```console
> pyinstaller foo.py --onefile --noconsole --icon=bar.ico
```

`--icon`オプションにicoファイルを指定します．
またアイコン表示とは直接関係ありませんが，`--onefile`オプションでパッケージングするときに1ファイルにまとめるのと，`--noconsole`オプションで起動時にコンソール画面を表示しないように指定しています．

<img width="130" alt="foo.png" src="https://qiita-image-store.s3.amazonaws.com/0/45310/441bdba6-e35d-c6d6-f610-2f955ab67167.png">

# タイトルバーやタスクバーに表示されるアイコン

こちらはPyQtのコードで対応します．QApplicationの`setWindowIcon()`に画像ファイルを渡すとタスクバーにアイコンが表示されます．

```py
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QWidget

app = QApplication(sys.argv)
app.setWindowIcon(QIcon('bar.ico')) # pngファイルなどでもOK
w = QWidget()
w.show()
sys.exit(app.exec())
```

これでPyInstallerで生成したexeファイルと同じパスにbar.icoを置いておくと，タイトルバーやタスクバーにアイコンが表示されます．
ただしこのままでは，exeファイルのほかにicoファイルも同時に配布する必要があるため，もう一工夫します．

## 画像ファイルの組み込み

PyInstallerでパッケージングするときに，外部ファイルを組み込むことができます．組み込むにはspecファイルに記述します．specファイルは一から記述してもいいですが，上記のように`foo.py`ファイルを指定してPyInstallerを動かしていると`foo.spec`ファイルが生成されています．これに以下の一行を追記すると簡単です．

```diff
a = Analysis(['foo.py'],
             pathex=['C:\\work\\Python\\foo'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
+a.datas += [('bar.ico', '.\\bar.ico', 'Data')]
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          name='foo',
          debug=False,
          strip=False,
          upx=True,
          runtime_tmpdir=None,
          console=False , icon='bar.ico')
```

このspecファイルを使用してPyInstallerを実行します．

```console
> pyinstaller foo.spec
```

これで`bar.ico`ファイルが組み込まれてパッケージングされます．

## 組み込んだファイルの読み込み

PyInstallerでパッケージングしたexeを実行するとTempフォルダに一時ファイルが展開されます．この中に上記で組み込んだ画像ファイルも展開されているので，これを読み込みます．

展開される先のパスは`sys._MEIPASS`から取得ができます．以下のようなコードを書いておけば，PyInstallerを使用したときと普通にPythonで実行したときとで両対応できます．

```py
import os
import sys

def resource_path(relative):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative)
    return path.join(path.abspath('.'), relative)
```

これを利用して以下のようなコードを書けば，パッケージングされたexeファイルだけでタイトルバーやタスクバーにアイコンを表示することができます．

```py
import os
import sys

from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QWidget

def resource_path(relative):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative)
    return path.join(path.abspath('.'), relative)

def main():
    app = QApplication(sys.argv)
    app.setWindowIcon(QIcon(resource_path('bar.ico')))
    w = QWidget()
    w.show()
    sys.exit(app.exec())

if __name__ == '__main__':
    main()
```

<img width="460" alt="foo.png" src="https://qiita-image-store.s3.amazonaws.com/0/45310/8282c82f-8b47-5e79-187b-0bf319b46efe.png">