PythonでGUI pywebviewで作るTODOアプリ

プログラミング

概要

pywebviewというGUIライブラリを使って、簡単なアプリのコードの例を紹介します。
いわゆるTODOアプリでタスクを登録し、必要がなくなれば削除できる機能と
jsonを使ってファイルにタスクを保存し、読み込む機能を備えます。

画面操作等ブラウザ操作は、javascriptでほかの複雑な計算やローカルのファイル操作等は、pythonが担当と言った構成になるかと思います。

インストール

pywebviewは、

pip install pywebview

で、インストールできます。

TODOアプリ コード

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import webview
import json
import os

# 初期データ
tasks = []

# JSONファイルのパス
json_file = "tasks.json"

# タスクをJSONファイルから読み込む
def load_tasks_from_file():
    global tasks
    if os.path.exists(json_file):
        with open(json_file, "r", encoding="utf-8") as file:
            tasks = json.load(file)
    else:
        tasks = []

# タスクをJSONファイルに保存する
def save_tasks_to_file():
    with open(json_file, "w", encoding="utf-8") as file:
        json.dump(tasks, file, indent=4, ensure_ascii=False)

# JavaScriptと連携するクラス
class Api:
    def get_tasks(self):
        """タスク一覧を取得"""
        return tasks

    def add_task(self, task):
        """タスクを追加"""
        if task:
            tasks.append(task)
            return {"status": "success", "message": "Task added"}
        return {"status": "error", "message": "Task cannot be empty"}

    def remove_task(self, task_index):
        """タスクを削除"""
        try:
            del tasks[task_index]
            return {"status": "success", "message": "Task removed"}
        except IndexError:
            return {"status": "error", "message": "Invalid index"}

    def save_tasks(self):
        """タスクを保存"""
        save_tasks_to_file()

    def load_tasks(self):
        """タスクを読み込み"""
        load_tasks_from_file()

# HTMLテンプレート
html = """
<!DOCTYPE html>
<html>
<head>
    <title>ToDo App</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        .task {
            display: flex;
            justify-content: space-between;
            margin: 5px 0;
        }
        button {
            margin-left: 10px;
        }
        .menu {
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
    <h1>ToDo App</h1>
    <div class="menu">
        <button onclick="saveTasks()">Save JSON</button>
        <button onclick="loadTasks()">Load JSON</button>
    </div>
    <input id="taskInput" type="text" placeholder="Enter a task" />
    <button onclick="addTask()">Add Task</button>
    <ul id="taskList"></ul>

    <script>
        async function fetchTasks() {
            const tasks = await pywebview.api.get_tasks();
            const taskList = document.getElementById('taskList');
            taskList.innerHTML = '';
            tasks.forEach((task, index) => {
                const li = document.createElement('li');
                li.className = 'task';
                li.innerHTML = `
                    ${task}
                    <button onclick="removeTask(${index})">Delete</button>
                `;
                taskList.appendChild(li);
            });
        }

        async function addTask() {
            const taskInput = document.getElementById('taskInput');
            const task = taskInput.value.trim();
            if (task) {
                const response = await pywebview.api.add_task(task);
                if (response.status === 'success') {
                    taskInput.value = '';
                    fetchTasks();
                } else {
                    alert(response.message);
                }
            } else {
                alert('Task cannot be empty');
            }
        }

        async function removeTask(index) {
            const response = await pywebview.api.remove_task(index);
            if (response.status === 'success') {
                fetchTasks();
            } else {
                alert(response.message);
            }
        }

        async function saveTasks() {
            await pywebview.api.save_tasks();
            alert('Tasks saved to JSON file.');
        }

        async function loadTasks() {
            await pywebview.api.load_tasks();
            fetchTasks();
        }

        // ページ読み込み後に0.5秒待ってからloadTasks()を実行
        window.onload = function() {
            setTimeout(function() {
                loadTasks();
            }, 500);
        };
    </script>
</body>
</html>
"""

# PyWebviewの設定
def start_app():
    load_tasks_from_file()  # 起動時にタスクを読み込む
    api = Api()

    # ウィンドウの作成
    webview.create_window(
        'ToDo App',
        html=html,
        js_api=api,
        width=800,
        height=600,
        resizable=True
    )
    webview.start()

if __name__ == '__main__':
    start_app()

コードを実行すると以下のような画面になります。
この場合、すでにタスクが幾つか登録されています。

コード解説

全体の構造

タスクは、jsonファイルとして保存します。
中身は、ただタスク名の配列が一つあるだけです。
タスクに変更があるとPython内部では、jsonの配列をリストとして扱っていてそれを増減するだけの仕組みです。
そして、保存する時はjsonファイルに変換して保存します。
逆にロードボタンを押したときや起動時にはjsonファイルを読み込みます。
(今気が付きましたが”Load”ボタンは、いらないですね。使う機会がないや。
使うとしたらファイルを手動で書き換えるとかjsonファイルを複数用意して名前を書き換えてロードするとかですね。練習用なのでそのままにしておきます。)

htmlテンプレートをヒアドキュメントとしてコードと同じファイルに持っています。
もちろん本来は、htmlを外部のファイルとして読み込むこともできます。

PythonがわApiクラス内にメソッドを登録して、ブラウザ側からはそれらのメソッドを使うような仕組みになっています。

メインブロック

説明の都合上、コードの解説が前後します。

150~167行
152:タスクを保存したjsonファイルがあれば読み込みます。
153:Apiクラスをインスタンス化します。
156:webview.create.window()でウィンドウを作ります。
158:ウィンドウのテンプレートにヒアドキュメントで定義したhtmlを指定します。
159:Apiをjavascriptから呼び出せるapiとして定義します。

Json関連の関数

11~23行目 load_tasks_from_file() save_tasks_from_file()
Jsonを読み込む関数と保存する関数が2つ定義されています。
13行目:”load_tasks_from_file()”内の”global tasks”は、16行目で代入しているのでglobalを指定しています。代入する場合は断らない限りローカル変数として扱われます。ですのでglobalを指定しない限り6行目の”tasks”には、代入されません。
代入ではなく、”tasks”を変更するだけの時は変更は反映されます。

“save_tasks_from_files()”でglobal宣言が無いのは、”tasks”で読み取るだけだからです。

Apiクラス

25~52行目 class Api(): …
JavaScriptから呼び出すPythonの機能がクラスとしてまとめられています。
内容は、タスクを管理するリスト”tasks”に要素を増やしたり削除したりする機能が中心で
メソッドによってそのメソッドの成否を辞書として返しています。

Htmlテンプレート

54~148行目 html = “””…”””
ここで、htmlと内部でJavaScriptを使って画面の書き換えを行っています。

78~86行目 <body> …
画面は、ここで作られています。

86行目

<ul id="taskList"></ul>

このリストに要素(<li>…</li>)を増減することで画面上のタスクを増減します。

PythonのApiクラスの利用

先にJavaScriptからPythonのApiクラスのメソッドを使う方法を解説します。

90行目

const tasks = await pywebview.api.get_tasks();

この行の様に “pywebview.api.XXX” で登録したメソッド”XXX”が呼び出され値が返されます。
このようにしてHtmlとPythonの間で情報がやり取りされます。

JavaScript

89~102行目 fetchTasks()
この関数は、タスクをいったん全て消してから”tasks”の内容から再度タスク画面を再構築しています。async, awaitを使って非同期に処理を行っています。

104~118行目 addTask()
“tasks”に”task”を追加して、追加された”tasks”から “fetchTasks()” を使ってタスク画面を再構築しています。

120~127行目 removeTask()
“addTask()”と同様のやり方でタスクを削除しています。

129~137行目 loadTasks() saveTasks()

140~144行目
“window.onload”イベントに “loadTasks()” を登録しています。
つまり、画面が読み込まれたらタスクを読み込み画面を構築しています。
その際、0.5秒待ってから “loadTasks()” を行っています。
待たないとうまく描画できません。
本来は、あまり良くない対応かと思います。
“pywebviewready”と言うpywebviewの準備が整ったときに何かするというイベントも用意されているのですがこれを使用してもうまく行きませんでした。
ちょっと、対処療法的なやり方ですがこれで良しとします。

まとめ

PyWebviewを使用すると、PythonとWeb技術を組み合わせて手軽にGUIアプリケーションを作成できます。
Htmlを使えば画面は、自由自在ですね。

今回のTODOアプリでは、Pythonでバックエンド処理とファイル操作を行い、JavaScriptでフロントエンドの操作と画面描画を担当しました。

このアプリを基に、さらなる機能拡張やカスタマイズが可能です。

Commnts