tkinterにアイコンをつけてexeにする

【目的】
tkinterで作ったアプリをexeにして配布したい
→どうせならデフォルトのアイコンではなく、好きなアイコンを設定してみたい

【環境】
Windows11
Python 3.12.8


### 目次
[1. 動くものを作る](#1-動くものを作る)
[2. Pyinstallerでexeにする](#2-pyinstallerでexeにする)
[3. exeにアイコンをつける](#3-exeにアイコンをつける)
[4. tkinter GUIにアイコンをつける](#4-tkinter-guiにアイコンをつける)
[5. GUIにアイコンを付けたままexeにする](#5-guiにアイコンを付けたままexeにする)
[6. 簡単に解説](#6-簡単に解説)
[7. ソース](#7-ソース)

# 1. 動くものを作る
好きなものを作る。
簡単にカウンターアプリでも。

```python:main.py
import tkinter as tk

def CountDown():
    count["text"] = count["text"] - 1

def CountUp():
    count["text"] = count["text"] + 1

root = tk.Tk()
root.title("Counter")
root.geometry("220x100")

count = tk.Label(root, text=0, font=("normal", 20))
count.pack()
tk.Button(root, text=("＋"), font=("normal", 20), command=CountUp).pack(
    side=tk.LEFT, padx=20, pady=5
)
tk.Button(root, text=("ー"), font=("normal", 20), command=CountDown).pack(
    side=tk.RIGHT, padx=20, pady=5
)

root.mainloop()
```

この段階で`python main.py`とすると、tkinterのGUIアイコンはデフォルトの羽マークになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/f9774bbd-eb08-6958-5b2e-f51215081dcc.png)




# 2. Pyinstallerでexeにする
以下のコマンドでexeファイルを作成できる。
```terminal:terminal
pyinstaller main.py --onefile --noconsole
```
exeファイル一つだけで動くようにしたいので
`--onefile`：ファイルを一つにまとめる
`--noconsole`：コンソールを出さない
のオプションを指定する

上記コマンドにて作成したexeはデフォルトアイコンになる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/c72f1fe7-e78f-4eeb-ba3d-b51945a230f1.png)
作成したexeを実行するとタスクバーは羽マークが表示される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/6e0d58ec-6074-4570-8c1e-9cbb2b43b107.png)
tkinterのGUIアイコンはデフォルトの羽マークのまま変わらない


現時点でのspecファイルはこんな感じ
```text:main.spec
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='main',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)
```
specファイルが既にある場合、
```terminal:terminal
pyinstaller main.spec
```
で前回に叩いたコマンドと同じ内容でexeを再生成することができる。


specファイルのあれこれについては別途記事として後日記載予定

# 3. exeにアイコンをつける
exeのアイコンを任意の画像にするために、コマンドの場合`--icon=[ファイルパス]`オプションを使用してアイコンを指定する
```terminal:terminal
pyinstaller main.py --onefile --noconsole --icon=icon.ico
```

上記コマンドを実行時、specファイルは以下になる。
```text:main.spec
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='main',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=['icon.ico'],
)
```
`exe = EXE()` の末尾にiconが指定される。
specファイルを変更して`pyinstaller main.spec`を実行してもいい。

作成されたexeアイコンは指定した画像になる
←before　after→
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/c72f1fe7-e78f-4eeb-ba3d-b51945a230f1.png)　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/065eb0e5-376f-4fd2-930a-2d7096d89dde.png)
※実行時のタスクバーのアイコンは変わらない


# 4. tkinter GUIにアイコンをつける
`root.iconbitmap(default="icon.ico")`でtkinterのGUIにアイコンをつける
アイコンファイルは拡張子を`.ico`にしただけだと表示されないのでしっかりとコンバートする必要がある

```python:main.py
import tkinter as tk

def CountDown():
    count["text"] = count["text"] - 1

def CountUp():
    count["text"] = count["text"] + 1

root = tk.Tk()
root.title("Counter")
root.geometry("220x100")
root.iconbitmap(default="icon.ico")

count = tk.Label(root, text=0, font=("normal", 20))
count.pack()
tk.Button(root, text=("＋"), font=("normal", 20), command=CountUp).pack(
    side=tk.LEFT, padx=20, pady=5
)
tk.Button(root, text=("ー"), font=("normal", 20), command=CountDown).pack(
    side=tk.RIGHT, padx=20, pady=5
)

root.mainloop()
```

これでtkinterのGUIアイコンは指定したicoになる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/c3f85be8-7bf5-ae9c-4d27-7159cb936dc5.png)


# 5. GUIにアイコンを付けたままexeにする
この状態で`pyinstaller main.spec`を叩いて生成したexeを実行すると、エラーになる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/c0d62334-a563-4767-80b6-c6a2fff578ca.png)

今のspecファイルではアイコンファイルを取り込んでおらず、また取り込んだとしてもexe実行時はファイルパスが変わるため、このままでは表示できない。

そのためにまずexeからも参照できるようにコードを変更する

```python:main.py
import os
import sys
import tkinter as tk


def CountDown():
    count["text"] = count["text"] - 1


def CountUp():
    count["text"] = count["text"] + 1


root = tk.Tk()
root.title("Counter")
root.geometry("220x100")

iconfile = "icon.ico"
if getattr(sys, "frozen", False):
    iconfile = os.path.join(sys._MEIPASS, iconfile)
root.iconbitmap(default=iconfile)

count = tk.Label(root, text=0, font=("normal", 20))
count.pack()
tk.Button(root, text=("＋"), font=("normal", 20), command=CountUp).pack(
    side=tk.LEFT, padx=20, pady=5
)
tk.Button(root, text=("ー"), font=("normal", 20), command=CountDown).pack(
    side=tk.RIGHT, padx=20, pady=5
)

root.mainloop()
```

main.pyの変更後、
```terminal:
pyinstaller main.py --onefile --noconsole --icon=icon.ico --add-data="icon.ico;."
```
もしくはspecファイルを以下に変更して`pyinstaller main.spec`を実行する


```text:main.spec
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
    ['main.py'],
    pathex=[],
    binaries=[],
    datas=[('icon.ico', '.')],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='main',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=['icon.ico'],
)
```

この時、
・exeのアイコン
・exe実行時のタスクバーのアイコン
・exe実行時のGUIアイコン
が全て任意指定したアイコン画像になる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/065eb0e5-376f-4fd2-930a-2d7096d89dde.png)　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/c33345fb-c94c-42ff-92db-31e68aa1d990.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3819870/c3f85be8-7bf5-ae9c-4d27-7159cb936dc5.png)


完成！



# 6. 簡単に解説
1. pyinstaller  
    `--add-data`、` datas=[('icon.ico', '.')]`にて、`icon.ico`というファイルをパス`.`に取り込むという処理をしている
1. main.py
    以下の部分にて、ファイル呼び出し時にexeからだと以下のIF文がTrueになるため、exe内に取り込んだ画像を呼びだすようにしている
    ```python:main.py
    iconfile = "icon.ico"
    if getattr(sys, "frozen", False):
        iconfile = os.path.join(sys._MEIPASS, iconfile)
    root.iconbitmap(default=iconfile)
    ```

これら2つの処理によって、exe実行時もmain.py実行時もアイコンがしっかりと表示されるようになる

# 7. ソース
最終状態のものをアップしておきます。
参考になれば幸いです。

https://github.com/yunatainaka/tkinter_exe