NiceGUIを使ったPython GUI アプリの作成と配布


Pythonで作成したツールの公開・配布を考えた場合にベストな開発構成は何かを考えた次第です。
言語はやはりライブラリが豊富なPythonがよい。GUIはやはり必要なのでGUIフレームワークを利用します。
`Streamlit` `Flet` `NiceGUI` などを検討しましたが、ネイティブアプリ化がしやすく、UIのカスタマイズも容易な `NiceGUI` がベストでした。

## 準備

ryeを導入しておきます。

https://rye.astral.sh/

以降のステップは Windows環境 を想定しています。最終的にアプリのexeファイルを作るからですが、どのような環境でもほぼ同じ手順だと思います。


## 1.プロジェクトを作る

rye で project を作ります。名前は適当。

```powershell
❯ mkdir niceapp-hoge
❯ cd .\niceapp-hoge\
❯ rye init --script
```

必要な依存関係を追加してsyncする。

```powershell
❯ rye add nicegui pywebview
❯ rye sync
```

ツリーはこんな感じ

```
.
│  .gitignore
│  .python-version
│  pyproject.toml
│  README.md
│  requirements-dev.lock
│  requirements.lock
├─.venv
└─src
    └─niceapp_hoge
            __init__.py
            __main__.py
```

Python仮想環境にはプロジェクト名と同じ実行可能スクリプトが定義されていて、実行してみると

```
❯ rye run niceapp-hoge
Hello from niceapp-hoge!
```

となり、これは　`src/niceapp_hoge/__init__.py` の　`main` を実行している。


```python:__init__.py
def main() -> int:
    print("Hello from niceapp-hoge!")
    return 0
```

## 2.NiceGUIアプリを作る

`__init__.py` の `main` から起動できる NiceGUIアプリを作る。以下は例。

```python:__init__.py
from nicegui import ui, app


def exitApp():
    app.shutdown()


class ViewMain:
    def __init__(self):
        pass

    def view(self):
        with ui.header().classes("p-2"):
            with ui.row().classes("items-center fit"):
                ui.label("Hello from niceapp-hoge!")
                ui.space()
                ui.icon("cancel", size="sm").on("click", handler=lambda: exitApp())

        ui.label("This is a niceapp-hoge example.")

        with ui.footer().classes("p-2"):
            ui.space()
            ui.label("Footer")


def main() -> int:
    viewMain = ViewMain()
    viewMain.view()
    ui.run(
        reload=False,
        native=True,
        frameless=True,
        show_welcome_message=False,
    )
    return 0

```

`native=True` と `frameless=True` を指定しておくと、ブラウザ上ではなくネイティブアプリのようにアプリを起動することができる。

モジュール実行（ryeで定義されたスクリプトから実行すると）では `ui.run` で `reload=False` にしておかないとではうまく動作しない。これはホットリロードを無効にする指定なので、開発時は別の起動用スクリプトを用意しておいて、直接実行するのがよい。例えば、以下のようなコードを用意しておいて、開発時は `python app.py` として実行する。

```python:app.py
from niceapp_hoge import ViewMain
from nicegui import ui

viewMain = ViewMain()
viewMain.view()
ui.run(
    reload=True,
    native=True
    frameless=True,
    show_welcome_message=False
)
```

スクリプトを実行してみると

```
❯ rye run niceapp-hoge
```

![スクリーンショット 2024-07-15 113416.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123579/fec6f821-a693-a87f-44d6-61d1792c758a.png)

いい感じ。以降はNiceGUIアプリを作りこんでいく。


## 3.pypiへ公開

NiceGUIアプリが完成したらpypiで公開する。pypiのAPIトークンを用意しておく。以下は参考。

https://qiita.com/que9/items/de1d54c3c37e67dbe798


必要に応じて、`pyproject.toml`を修正(versionとかDescriptionとか)して、`build`する。 `-o`で出力先ディレクトリを`dist`から変更しておく。あとで使う実行可能パッケージの作成で名前が重複してしまうため。

```powershell
❯ rye build -o ./dist_build
building niceapp-hoge
* Creating isolated environment: venv+uv...
* Using external uv from C:\Users\xxxxx\.rye\uv\0.2.22\uv.EXE
* Installing packages in isolated environment:
  - hatchling
* Getting build dependencies for sdist...
* Building sdist...
* Building wheel from sdist
* Creating isolated environment: venv+uv...
* Using external uv from C:\Users\xxxxxx\.rye\uv\0.2.22\uv.EXE
* Installing packages in isolated environment:
  - hatchling
* Getting build dependencies for wheel...
* Building wheel...
Successfully built niceapp_hoge-0.1.0.tar.gz and niceapp_hoge-0.1.0-py3-none-any.whl
```

次に`publish`

```
❯ rye publish      
No access token found, generate one at: https://pypi.org/manage/account/token/
Access token: [APIトークンを貼り付けする]
Uploading distributions to https://upload.pypi.org/legacy/
Uploading niceapp_hoge-0.1.0-py3-none-any.whl
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.8/4.8 kB • 00:00 • ?
Uploading niceapp_hoge-0.1.0.tar.gz
100% ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5.0/5.0 kB • 00:00 • ?

View at:
https://pypi.org/project/niceapp-hoge/0.1.0/
```

これだけでOK

試しに、別の仮想環境を作ってインストールしてみる。

```
❯ mkdir testenv
❯ cd .\testenv\
❯ python -m venv .venv
❯ .\.venv\Scripts\Activate.ps1
❯ pip install niceapp-hoge
```

アプリの起動はスクリプトを実行するだけ

```
❯ niceapp-hoge
```

これでNiceGUIアプリが起動する。ユーザー側でPython環境が整っている状況が想定できるのであれば、アプリの配布はこれで十分かもしれない。


# 4. Exeファイルの作成

ユーザ側にPython環境があることを想定できるのであればpypiで公開するのでもよいが、そうでないのであれば単独で実行可能なファイル（ここでは exeファイル）を配布したいということになる。NiceGUIは実行可能ファイルへのパッケージ用コマンドが用意されているのでそれを利用する。

PyInstallerを利用するので、依存関係を追加しておく。

```
❯ rye add pyinstaller
```

パッケージ用の起動スクリプトを書いておく。`ui.run`の引数が異なるだけなので、先ほどの `app.py` を書き換えるのでもよい。

```python:pack.py
from niceapp_hoge import ViewMain
from nicegui import ui, native

viewMain = ViewMain()
viewMain.view()
ui.run(
    reload=False,
    native=True,
    frameless=True,
    port=native.find_open_port(),
    show_welcome_message=False
)
```

パッケージ用のコマンドを実行する

```powershell
❯ rye run nicegui-pack --onefile --windowed --name "NiceAPP" .\pack.py
```

`dist`ディレクトリに `NiceAPP.exe` ができている。起動時間が多少かかるが単独で実行可能なアプリになる。

![スクリーンショット 2024-07-15 150246.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123579/10e125c5-9b9c-c5f2-75a0-5774b48cd0f1.png)

アイコンとかがダサいので、アイコンなどを変更したりするのであれば pyinstaller を自分で叩く必要があるかも。こだわらなければこれで十分でしょうか。