目次
サンプル・プログラム
cardGame2.py | サンプル・プログラム:カードをシャフルして配る(GUI版) |
cardGame2.py | カードをシャフルして配るプログラムのGUI部。 |
pahooCardGame.py | クラス・ファイル:カードゲーム基本クラス。 |
pahooValidate.py | バリデーション・モジュール。 |
./image/*.png | カード画像データ。 |
サンプル・プログラムを動かしてみよう
準備ができたら、"cardGame2.py" を実行してみてほしい。上図のようなウィンドウを表示する。
画面にカードをグラフィカルに表示し、キーボードの入力の代わりに、チェックボックスとボタンを使ってカードを交換したり、アプリを終了できるようにした.
pywebviewと JavaScript
ユーザー定義関数やクラスをうまく作ることで、いままでに作った資産(プログラム)を新しいアプリケーションに流用できる。
pywebview は Python の外部ライブラリだ。pywebview のメソッドは、Python プログラムからみると、何らかのデータを返したり、何らかの作用を行う。
詳しい動きは後述するが、pywebview がブラウザ・モジュールをウィンドウとして表示し、そこにHTML/CSS/JavaScriptを表示する。ここに書いた JavaScriptプログラムと Python プログラムの間では、Apiクラス という名前のユーザー定義クラスを用意することで、データ受渡を行う。
これを JavaScript側からみると下図のように、Apiクラス を介して、Python で作られたクラウドサービスを利用しているように見える。もちろん Python も同じPCにあるから、クラウドサービスではないし、インターネットとも接続する必要がなく、PC内で完結している。
JavaScriptによるプログラミングについては、当サイトの「JavaScriptによるプログラミング入門」が参考になるだろう。
Apiクラス を使うことで、"cardGame1.py" とインタフェースがどう変わったかを説明しよう。JavaScriptプログラム(HTMLファイル)とPythonプログラムの関係は上図のようになる。
まず出力の方だが、プログラム "cardGame1.py" では、カードのリストを画面に表示していた。プログラム "cardGame2.py" では、カードのリストを Apiクラスを通じて "cardGame2.html" に書かれたJavaScriptに渡し、JavaScript側で、これを画像ファイル名に変換して画面に表示する。データは、JavaScriptが処理しやすいように JSON形式で渡す。
入力の方は、プログラム "cardGame1.py" では、キーボードからコマンド入力していた。今回は、"cardGame2.html" に書かれた JavaScriptでチェックボックスやボタンの状態をイベント監視し、これをコマンドに変換して Apiクラスを通じて "cardGame2.py" に文字列 command(str) を渡してやる。
こうすることで、Python プログラ "cardGame2.py" は、カードのリストを出力したり、入力されたコマンドを処理するだけで、"cardGame1.py" と処理内容がほとんど同じになる。
カードの初期化
class Api:
def initGame(self):
"""初期化する.
Args:
None
Returns:
None
"""
# 初期化
try:
self.cg = pahooCardGame(1)
self.cg.shuffleStocks()
except ValueError as errmsg:
pv.dispErrorMessageAndExit(errmsg)
self.playerId = 0 # プレイヤーID
self.cg.stocks2hands(5, self.playerId)
print(self.cg.hands)
response = { "hands": self.cg.hands, "message": "開始" }
return json.dumps(response)
今回用意する Apiクラス は、インスタンス化したときの初期化メソッドは必要ない。
メソッド initGame は、"cardGame1.py" の冒頭で行ったカードの初期化処理をまとめたものである。
処理が終わったら、手札のリストと Python プログラムからのメッセージを下記構造の応答JSONデータにして、JavaScriptに渡す。応答JSONデータの構造は、これ以降のメソッドでも共通である。
コマンドを解釈し実行する
def execCommand(command, cg, id):
"""コマンドを解釈し実行する.
Args:
command(str): コマンド
cg(cardGame): cardGameオブジェクト
Returns:
bool: True 実行成功 / False 処理終了, str: メッセージ
Note:
終了コマンドでプログラムを終了する.
"""
# 終了コマンド
if (command == "q"):
# webviewウィンドウを開いていれば閉じる
if "window" in globals():
window.destroy()
# プログラム終了
sys.exit()
# やり直しコマンド
elif (command == "c"):
cg.__init__()
cg.shuffleStocks()
cg.stocks2hands(5, id)
return True, "やり直し"
# 交換する手札番号リストを生成
numList = [item.strip() for item in command.split(",")]
maxNum = cg.countHands(id)
nums = []
for ss in numList:
try:
nums.append(pv.validateNumber(ss, "int", 1, maxNum, label="手札"))
except ValueError as errmsg:
return True, errmsg
# 手札番号リストに重複がないかどうかバリデーション
if isDuplivacates(nums): return True, "交換する手札番号が重複しています"
# 手札番号をソートする
nums.sort()
# 手札を1枚ずつ交換する
cnt = 1
for i in nums:
cg.hands2layouts(i - cnt, id)
cnt += 1
if not cg.stocks2hands(1, id): return True, "山札がなくなりました"
print("command = " + command)
print(cg.hands[0])
return True, f"{nums}を交換しました"
class Api:
def initGame(self):
"""初期化する.
Args:
None
Returns:
None
"""
# 初期化
try:
self.cg = pahooCardGame(1)
self.cg.shuffleStocks()
except ValueError as errmsg:
pv.dispErrorMessageAndExit(errmsg)
self.playerId = 0 # プレイヤーID
self.cg.stocks2hands(5, self.playerId)
print(self.cg.hands)
response = { "hands": self.cg.hands, "message": "開始" }
return json.dumps(response)
外部ブラウザで指定URLを開く
def openBrowser(self, url):
"""外部ブラウザで指定ULを開く.
Args:
url(str): URL
Returns:
None
"""
webbrowser.open(url)
今回用意する Apiクラス として用意したメソッドは、この3つだけだ。
Pythonメイン・プログラム
# メイン・プログラム =======================================================
# 初期値
WIDTH = 600 # webviewウィンドウの幅(ピクセル)
HEIGHT = 680 # webviewウィンドウの高さ(ピクセル)
HTML_FILE = "./cardGame2.html" # HTMLファイル名(webview部分)
# pywebviewを使ってウィンドウを表示する.
api = Api()
window = webview.create_window("カードゲーム by Python", HTML_FILE, js_api=api, width=WIDTH, height=HEIGHT)
webview.start(http_server=False, debug=False)
次に、pywebview を importしたことで利用できるようになる webviewオブジェクトにある create_windowメソッドを使ってウィンドウ・オブジェクト Window を用意する。create_window(タイトル, HTMLファイル名, js_api=Apiオブジェクト名, width=ウィンドウの幅, height=ウィンドウの高さ) のようにして使う。
最後に、start メソッドでウィンドウを表示する。start(http_server=HTTPサーバを使うかどうか, debug=デバッグウィンドウを表示するかどうか) のようにして使う。
デスクトップアプリ開発と同じで、start メソッドを実行すると、destroy メソッドを実行するためウィンドウは表示したままになり、Pythonは終了しない。"cardGame1.py" のように繰り返し処理(無限ループ)は必要なくなり、JavaScriptからのイベント駆動により処理を進めていく。
JavaScriptメイン・プログラム
227: // メイン・プログラム ======================================================
228: window.onload = function() {
229: //タイトル等をセット
230: document.title = TITLE;
231: document.getElementById('title').innerHTML = TITLE + ' <span style="font-size:small;">' + getLastModified() + '版</span>';
232: document.getElementById('reference').innerHTML =`
233: ※参考サイト <span style="color:blue; text-decoration:underline;" onClick="openBrowser('${REFERENCE}')">${REFERENCE}</span>
234: `;
235: //スタイルシートをセットする.
236: document.getElementById('help').style.width = WIDTH + 'px';
237:
238: //ゲーム開始する.
239: startGame();
240: }
メイン・プログラムは、window.onload メソッドを使って、HTMLファイル読み込み時に実行する処理となる。
前半部分で、基本的なタグ情報やスタイルシートをセットする。
最後に、今回のカードゲームを開始するユーザー関数 startGame を実行する。
ゲーム開始する
73: /**
74: * カード情報を初期化する.
75: * Pythonプログラム initGame() を呼び出す.
76: * @param なし
77: * @return なし
78: */
79: async function initGame() {
80: //Pythonへコマンドを送る.
81: let json = await pywebview.api.initGame();
82:
83: //Pythonからの応答を取得する.
84: let response = JSON.parse(json);
85: console.log(response);
86:
87: //メッセージと手札セットを表示する.
88: dispCards(response);
89: }
initGame関数は、前述の Apiクラスに用意した initGameを呼び出す。Apiクラスのメソッドは非同期で実行するので(この点でもクラウドサービスに似ている)、応答が返るまで待機する awaitを指定する。これにともない、initGame関数は asyncで定義する。
前述の通り、Apiクラスの応答は JSON形式なので、parse メソッドを使って配列 response へ代入する。
最後に、応答データをユーザー定義関数 dispCard に渡す。
カードを表示する
132: /**
133: * メッセージと手札セット(5枚)を表示する.
134: * @param Object response Pythonプログラムからの応答
135: * @return なし
136: */
137: function dispCards(response) {
138: //メッセージを表示
139: document.getElementById('message').innerHTML = response.message;
140:
141: //表示領域をクリア
142: document.getElementById('hands').innerHTML = '';
143:
144: //1枚ずつ表示する
145: response.hands[0].forEach(function (card, n) {
146: image = card2image(card);
147: dispCard(n + 1, image);
148: });
149: }
手札は5枚が1セットの配列になっており、forEachメソッドを使って、1枚ずつ表示するユーザー定義関数 dispCards を呼び出す。
104: /**
105: * 手札を1枚表示する.
106: * @param Integer n 手札番号(1~5)
107: * @param String img カードの画像ファイル名
108: * @return なし
109: */
110: function dispCard(n, img) {
111: let id = 'hand' + ('00' + n).substr(-2) //オブジェクトID
112: let scale = WIDTH / ((WIDTH_CARD + 10) * NUM_CARDS); //画像表示倍率
113: let height = Math.floor(HEIGHT_CARD * scale); //表示画像の高さ
114: let width = Math.floor(WIDTH_CARD * scale); //表示画像の幅
115:
116: //上からの相対位置(固定;ピクセル)
117: let top = 0;
118: //左からの相対位置(ピクセル)
119: let left = 16;
120:
121: let html = `
122: <div style="position:relative; top:${top}px; left:${left}px;">
123: <img src="${img}" style="width:${width}px; height:${height}px;">
124: <div style="text-align:center;">
125: <input id="${id}" type="checkbox" value="${n}">
126: </div>
127: </div>
128: `;
129: document.getElementById('hands').innerHTML += html;
130: }
91: /**
92: * カード情報配列に対応する画像ファイル名を取得する.
93: * @param Array card カード情報 [スート,ナンバー]
94: * @return String 画像ファイル名
95: */
96: function card2image(card) {
97: let suit = card[0];
98: let num = card[1];
99: let image = IMAGE_FOLDER + suit + '_' + ('00' + num).substr(-2) + '.png';
100:
101: return image;
102: }
次回予告
最後に、関数やメソッドを別の関数やメソッドで上書き(オーバーライド)する目的と、その方法について学ぶ。
コラム:3層アーキテクチャ、再び
Python プログラムを1つの巨大な関数と考えてみてほしい。この関数には、入力(引数)が必要で、出力(戻り値)が発生する。
cardGame1.py では、入力(command)にキーボードを、出力(response)に画面を割り当てた。キーボードも画面もCUI=文字列ベースで動作するから、いずれもstr型のデータを入出力すればよい。
cardGame2.py では、入出力をブラウザベースに切り替えた。入力(command)はApiクラスのメソッドの引数となるデータだから、str型のままでよい。出力(response)もstr型のままでもよかったのだが、これを解釈して画像ファイルを呼び出す処理が必要だったので、JavaScriptが解釈しやすいJSON形式に変更した。
このシリーズでは取り上げないが、ここまで出来ていれば、Pythonプログラムをクラウドサービスとしてリリースし、ブラウザアプリとしてカードゲームを楽しむ cardGameX.py を作ることができるだろう。このとき、入出力はhttp通信になるので、入力(command)はPOST渡しするXMLかJSON形式に変更した方がいいだろう。
このように3層を別々に開発することで、開発管理がしやすくなるだけでなく、拡張性ももたせることができる。
そこで、「1.4 ブラウザを使ったGUI」で紹介した外部ライブラリ pywebview を利用することにする。pywebview のインストール方法については、「1.4 ブラウザを使ったGUI」をご覧いただきたい。