PythonとVue.jsで簡単にGUIアプリケーションを作成する方法

PythonとVue.jsで簡単にGUIアプリケーションを作成する方法

概要

Python と Vue.js を利用して、 GUI アプリケーションを作成する方法について紹介します。
Python に、GUI を作成するのに適している Web 技術を組み合わせることで、モダンな GUI アプリケーションを簡単に作成することができます。作成したアプリケーションは、スタンドアローンで動作するように、PyInstaller を使用してパッケージ化します。

GUI に Vue.js を使う理由

Vue.js は、ユーザーインターフェイスを構築するための JavaScript フレームワークで、主に Web アプリケーションのフロントエンド側を作成する際に使用されます。 Python で GUI を作成する際の他の候補として、標準ライブラリに含まれる Tkinter がありますが、Vue.js を含む Web アプリケーション開発の技術を採用するメリットとして以下が挙げられます。

  • Web アプリケーションの開発で広く利用されており、実績があり、情報量も多い。
  • グラフを表示するライブラリや CSS フレームワークなど、 Web アプリケーション開発の資産を利用できる。
  • ユーザーインタフェースを提供するフロントエンド側のコードとデータを処理するバックエンド側のコードを分離できる。

アプリケーション構成

Python の Web アプリケーションフレームワークである Flask でサーバーを立て、Vue.js で作成した Web 画面をホスティングします。処理は以下の順序をで行われます。

  1. フロントエンド側では、ユーザーのアクションにより、バックエンド側の API 宛にリクエストを送信します。
  2. バックエンド側でリクエストを受信したら、Python のコードで処理し、レスポンスを返します。
  3. フロントエンド側でレスポンスを受信したら、その内容を表示します。

フロントエンド、バックエンド間の通信は Ajax で行います。

アプリケーションの構成例
アプリケーションの構成例

フロントエンド側を作成する

作成するサンプルプログラム

例として、身長及び体重から Body Mass Index (BMI) を計算する GUI アプリケーションを作成します。
Vue.js の他、CSS フレームワークである Vuetify と、Ajax 通信を行うため HTTP クライアントである Axios も使用します。今回、Vue.js や各種ライブラリの詳しい解説は省略します。使い方を知りたい方は公式ドキュメントを参照ください。

作成するサンプルプログラムの画面

作成するサンプルプログラムの画面

1. 開発環境を構築する

Vue.js の開発環境を構築するため、以下のソフトウェアをインストールします。

  • Node.js: Javascript の実行環境
  • Vue CLI: Vue.js を使用したプロジェクトを作成するツール

まず、Javascript の実行環境である Node.js をこちらからダウンロードし、インストールします。Windows の場合、.msi ファイルのリンクが該当するインストーラーです。インストールが完了したら、コマンドプロンプトより node コマンドが使用できることを確認します。

> node -v
v17.7.0

次に Vue.js を使用したプロジェクトを作成するためのツール Vue CLI をインストールします。

npm install -g @vue/cli

2. Vue.js のプロジェクトを作成する

vue create <プロジェクト名> コマンドを実行して、プロジェクトを作成します。

vue create frontend

Vue.js のバージョンを聞かれるので、Default ([Vue 2] babel, eslint) を選択します。
すると、frontend 以下に必要なファイルが生成されます。
さらに、Vue.js で利用する以下のライブラリもインストールします。

  • Axios: Javascript の HTTP クライアント
  • vue-axios: Vue.js で Axios を利用するためのプラグイン
  • Vuetify: CSS フレームワーク
cd frontend
npm i axios vue-axios
vue add vuetify

Vuetify のバージョンを聞かれるので、Vuetify 2 – Vue CLI (recommended) を選択します。ここまで完了したら、以下のコマンドでデバッグ用のサーバーを起動します。

npm run serve

ブラウザで http://localhost:8080/ にアクセスし、以下の画面が表示されたらセットアップ完了です。

セットアップ完了時点の画面

セットアップ完了時点の画面

3. GUI を作成する

これまでの作業で、ディレクトリ内は以下のような構成になっています。

frontend
 ┣ public
 ┃ ┣ favicon.ico
 ┃ ┗ index.html
 ┣ src
 ┃ ┣ assets
 ┃ ┣ components
 ┃ ┣ plugins
 ┃ ┃ ┗ vuetify.js
 ┃ ┣ App.vue
 ┃ ┗ main.js
 ┣ .gitignore
 ┣ babel.config.js
 ┣ jsconfig.json
 ┣ package-lock.json
 ┣ package.json
 ┣ README.md
 ┗ vue.config.js

Axios を Vue.js 内で利用するため、main.js に以下を追記します。

main.js

import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(VueAxios, axios)

入力画面を App.vue に記述します。
 template ブロックに Vuetify のコンポーネントを利用して、身長入力欄、体重入力欄、結果表示欄、計算ボタンの4つを作成します。

App.vue

<template>
  <v-app>
    <v-app-bar app color="primary" dark>
      <span class="text-h5">BMI 計算機</span>
    </v-app-bar>

    <v-main>
      <v-container class="my-5">
        <!-- 身長入力欄 -->
        <v-row no-gutters>
          <v-col cols="auto">
            <v-subheader>身長</v-subheader>
          </v-col>
          <v-col cols="auto">
            <v-text-field
              v-model="height"
              dense
              hide-details
              outlined
              style="width: 250px"
              suffix="cm"
              type="number"
            ></v-text-field>
          </v-col>
        </v-row>

        <!-- 体重入力欄 -->
        <v-row no-gutters>
          <v-col cols="auto">
            <v-subheader>体重</v-subheader>
          </v-col>
          <v-col cols="auto">
            <v-text-field
              v-model="weight"
              dense
              hide-details
              outlined
              style="width: 250px"
              suffix="kg"
              type="number"
            ></v-text-field>
          </v-col>
        </v-row>

        <!-- 結果表示欄 -->
        <v-row no-gutters>
          <v-col cols="auto">
            <v-alert
              v-if="success"
              dense
              style="width: 500px"
              text
              type="success"
            >
              {{ this.message }}
            </v-alert>
            <v-alert
              v-else-if="success === false"
              dense
              style="width: 500px"
              text
              type="error"
            >
              {{ this.message }}
            </v-alert>
          </v-col>
        </v-row>

        <!-- 計算ボタン -->
        <v-row no-gutters>
          <v-col>
            <v-btn
              color="primary"
              @click="calculate"
              :disabled="!height || !weight"
              >計算する</v-btn
            >
          </v-col>
        </v-row>
      </v-container>
    </v-main>
  </v-app>
</template>

script ブロックでは、計算ボタンをクリックされた際に呼ばれるコールバック関数を実装します。
サーバーとやり取りする際のリクエスト、レスポンスは以下の内容の JSON データを想定しています。

リクエスト

{"height": <身長 (cm)>,
"weight": <体重 (kg)>}

レスポンス

{"success": <正常終了か否か>,
"message": <エラーが発生した場合の内容>,
"bmi": <BMI>,
"normal_weight": <入力された身長の標準体重 (kg)>}

Axios を使用して、リクエストを http://127.0.0.1:5000/api/get_bmi に対して post して、結果を受け取ります。結果である BMI 及び標準体重を文字列に整形し、message プロパティに設定します。

App.vue

<script>
export default {
  name: "App",

  data: () => ({
    height: null,
    weight: null,
    success: null,
    message: null,
  }),

  methods: {
    calculate() {
      if (!this.height || !this.weight) {
        return; // 身長または体重が未入力の場合
      }

      this.success = null;
      this.message = null;

      const req = JSON.stringify({
        height: this.height,
        weight: this.weight,
      });

      this.axios
        .post(`http://127.0.0.1:5000/api/get_bmi`, req)
        .then((res) => {
          this.success = res.data.success;

          if (res.data.success) {
            // 正常終了した場合
            const bmi = res.data.bmi.toFixed(1);
             const normalWeight = res.data.normal_weight.toFixed(1);
            this.message = `BMI は${bmi}、標準体重は${normalWeight}kgです。`;
          } else {
            // 異常終了した場合
            this.message = res.data.message;
          }
        })
        .catch(() => {
           // 通信に失敗した場合
          this.success = false;
          this.message = "通信中にエラーが発生しました。";
        });
    },
  },
};
</script>

以上でフロント側の実装は完了です。

バックエンド側を作成する

次にバックエンド側を作成します。
手動で bmi-calculator というディレクトリを作成し、中に main.py というファイルを作成します。また、先程作成したフロントエンド側の frontend ディレクトリをその中に配置します。

bmi-calculator
 ┣ main.py
 ┗ frontend

main.py に API 及び BMI の計算を行う処理を実装します。

1. Flask をインストールする

pip install flask flask-cors

2. フロントエンド側をビルドする

後に先程作成したフロントエンド側のページを Flask でホスティングするために、vue.config.js を編集し、Vue.js アプリケーションをビルドした際の出力ディレクトリを、bmi-calculator/dist になるように変更します。

vue.config.js

const path = require("path");
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
    transpileDependencies: [
        'vuetify'
    ],
    assetsDir: "static",
    outputDir: path.resolve(__dirname, "../data/dist"),
})

変更したら、フロントエンド側をビルドします。

cd bmi-calculator
npm run build --prefix frontend

ビルドに成功すると、以下のようなディレクトリ構成になります。生成された bmi-calculator/dist/index.html を Flask でホスティングします。

bmi-calculator
 ┣ dist
 ┃ ┣ index.html
 ┃ ┣ favicon.ico
 ┃ ┗ static
 ┃    ┣ css
 ┃    ┗ js
 ┣ main.py
 ┗ frontend

3. Flask オブジェクトを作成する

Flask オブジェクトを作成します。
template_folder 引数には index.html があるディレクトリ、static_folder 引数には css や js といった index.html 内で参照しているアセットがあるディレクトリのパスを指定します。これらのデータは Pyinstaller でビルドした際に同梱するので、base_dir() でパスが切り替わるようにしておきます。

import sys
import webbrowser
from pathlib import Path

from flask import Flask, render_template, request
from flask_cors import CORS


def base_dir():
    if hasattr(sys, "_MEIPASS"):
        # 実行ファイルで起動した場合、展開先ディレクトリを基点とする。
        return Path(sys._MEIPASS)
    else:
        # python コマンドで起動した場合、プロジェクトディレクトリを基点とする。
        return Path("..")


app = Flask(
    __name__,
    template_folder=base_dir() / "dist",
    static_folder=base_dir() / "dist/static",
)
CORS(app)

4. BMI、標準体重を計算する API を作成する

<hostname>/api/get_bmi にリクエストがポストされたとき、BMI、標準体重を計算して返す API を作成します。
 post されたデータは request.get_json(force=True) で JSON 形式で取得できます。

身長を h m、体重を w kgとした際に、BMI の計算式は以下になります。また、BMI 値が 22 となる体重をその身長の適正体重とします。

  • BMI: BMI = w / h^2
  • 標準体重: BMI = h^2 * 22
@app.route("/api/get_bmi", methods=["POST"])
def get_bmi():
    """BMI、標準体重を計算して返す API

    Returns:
        dict: レスポンス
    """
    data = request.get_json(force=True)

    # 入力を数値に変換する。
    try:
        height_cm = float(data["height"])
        weight_kg = float(data["weight"])
    except:
        return {"success": False, "message": "入力データが不正です。"}

    if height_cm <= 0 or weight_kg <= 0:
        return {"success": False, "message": "入力データが不正です。"}
    print(F"リクエストを受け取りました。({weight_kg:.1f}cm, {height_cm:.1f}kg)")

    # BMI 及び標準体重を計算する。
    height_m = height_cm / 100
    bmi = weight_kg / height_m**2
    normal_weight = height_m ** 2 * 22

    return {"success": True, "bmi": bmi, "normal_weight": normal_weight}

5. index.html をホストする

/ にアクセスしたとき、フロントエンド側の index.html の内容を返すルーティング設定を作成します。

@app.route("/")
def index():
    """フロントエンド側のページを表示する。

    Returns:
        str: HTML
    """
    return render_template("index.html")

6. サーバーを起動する

ポート番号 5000 で待ち受ける Flask サーバーを起動します。
webbrowser.open() で起動時に http://localhost:5000/ をブラウザで開くようにしておきます。

def main():
    webbrowser.open("http://localhost:5000/", new=2, autoraise=True)
    app.run(debug=True, host="0.0.0.0", port=5000)


if __name__ == "__main__":
    main()

7. 実行する

main.py を実行しすると、ブラウザが起動し、http://127.0.0.1:5000/ が開かれます。

python main.py

身長、体重を入力して計算ボタンをクリックしたあと、メッセージが表示されていれば成功です。

実行結果

実行結果

Pyinstaller でビルドする

Pyinstaller でビルドして、スタンドアローンで動作するアプリケーションにします。 Pyinstaller を pip でインストールします。

pip install pyinstaller

今回は、dist ディレクトリを <展開先ディレクトリ>/dist で参照できるように同梱する必要があるため、ビルド時にコマンドライン引数 add-data “.\dist;.\dist” で指定します。単一の実行ファイルにパッケージしたいため、–onefile オプションも合わせて指定します。

pyinstaller .\main.py --add-data ".\dist;.\dist" --onefile --distpath .\bmi-calculator --name bmi-calculator

ビルドが完了すると、bmi-calculator/bmi-calculator.exe が生成されます。起動して、先程と同様のページが表示されたら成功です。

まとめ

今回は GUI を Vue.js で作成し、Flask を使用して処理を Python 側で行う方法について解説しました。
このように Web アプリケーション開発に使用される Javascript、CSS を使用することで、複雑な GUI でも簡単に作ることができます。