【PyQt5 GUI実践】気象データ可視化アプリを作ろう!(2) 地図表示とPython-JavaScript連携の魔法 🗺️
「PyQt5アプリの中に、Webページみたいなリッチなコンテンツを表示したい!」
「アプリ内の地図をクリックしたら、その場所の緯度経度をPythonで取得するにはどうすればいい?」
「デスクトップアプリとWeb技術(JavaScript)を連携させるって、なんだかカッコいい!」
こんにちは! PyQt5探検隊、隊長のPythonistaです! 前回の第1回では、PyQt5を使ってアプリケーションのウィンドウを作成し、レイアウトマネージャでウィジェットの「骨格」を組み上げましたね。これで、本格的なデスクトップアプリ開発の土台ができました。
シリーズ第2回となる今回は、このアプリの最も面白くて核心的な部分に挑戦します! それは、アプリ内にインタラクティブな地図を表示し、ユーザーがクリックした地点の座標(緯度・経度)をPython側で受け取るという機能です。これを実現するために、
- PyQt5アプリ内でWebコンテンツを表示する
QWebEngineView
- そのWebコンテンツ(JavaScript)とPythonのコードを繋ぐ魔法の架け橋
QWebChannel
といった、少し高度ですが非常に強力なテクニックを学んでいきます。この回をマスターすれば、あなたのGUIアプリケーションの表現力とインタラクティビティは飛躍的に向上しますよ!
1. 準備:前回からのコードと必要なライブラリ
今回は、第1回で作成したGUIの骨格コードをベースに機能を追加していきます。また、地図をクリックして取得した座標から地名を取得するために、geopy
というライブラリも使いますので、インストールしておきましょう。
pip install PyQt5 PyQtWebEngine geopy
そして、以下のライブラリをインポートしておきます。
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QDateEdit, QStatusBar
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtCore import QObject, pyqtSlot, QDate, QUrl
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
2. ステップ1:アプリにWebブラウザを埋め込む (`QWebEngineView`)
PyQt5には、QWebEngineView
という、ChromiumベースのWebブラウザエンジンをそのまま埋め込めるウィジェットがあります。これを使うと、アプリ内でWebページを表示したり、HTMLやJavaScriptを実行したりできます。
今回は、このQWebEngineView
に、**Leaflet.js**というオープンソースのJavaScriptライブラリを使ったインタラクティブな地図を表示します。そのためのHTMLコンテンツをPythonの文字列として作成し、setHtml()
メソッドで読み込ませます。
3. ステップ2:PythonとJavaScriptを繋ぐ魔法の架け橋 (`QWebChannel`)
今回のプロジェクトで最も重要なのが、このPythonとJavaScriptの連携です。「地図(JavaScript)でクリックされた座標」を、「GUIアプリ(Python)の変数」に渡すにはどうすればよいでしょうか?
その答えがQWebChannel
です。これは、PythonのオブジェクトをJavaScript側から呼び出せるようにするための通信チャネルを作成する仕組みです。
手順は以下の通りです。
- Python側: JavaScriptから呼び出されるための特別な「受付係」クラス (
QObject
を継承) と、その中の関数 (@pyqtSlot
デコレータを付ける) を用意する。 - 連携設定:
QWebChannel
に、その「受付係」オブジェクトを登録する。 - JavaScript側: 専用のライブラリ (
qwebchannel.js
) を読み込み、登録されたPythonのオブジェクトを呼び出す。
この仕組みを、これから作成するコードの中で見ていきましょう。
4. 実装:コードの全体像と解説
第1回のコードに、地図の表示とPython-JavaScript連携の機能を追加した、第2回時点での完成版コードです。
# (冒頭のimport文は省略)
# --- グローバル設定 ---
geolocator = Nominatim(user_agent="weather_app_for_blog")
reverse = RateLimiter(geolocator.reverse, min_delay_seconds=1)
# --- 地図(JavaScript)とPythonを連携させるためのクラス ---
class MapApi(QObject):
def __init__(self, app_instance):
super().__init__()
self.app = app_instance
# @pyqtSlotデコレータで、このメソッドをJavaScript側から呼び出せるようにする
@pyqtSlot(float, float)
def receive_coords(self, lat, lng):
# JavaScriptから受け取った座標を、メインアプリのメソッドに渡す
self.app.update_location(lat, lng)
# --- GUIメインウィンドウ ---
class WeatherApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("気象データ可視化ツール")
self.setGeometry(100, 100, 1000, 800)
self.lat = 35.6812 # 初期値: 東京駅
self.lon = 139.7671
self.location_name = "東京都千代田区"
self.initUI() # UIの初期化
def initUI(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QHBoxLayout(central_widget)
# --- 左側:コントロールパネル (第1回で作成) ---
controls_layout = QVBoxLayout()
self.location_label = QLabel(f"選択地点: {self.location_name}\n(Lat: {self.lat:.4f}, Lon: {self.lon:.4f})")
controls_layout.addWidget(self.location_label)
# ... (日付選択やボタンの作成は第1回と同じ) ...
self.start_date_edit = QDateEdit(calendarPopup=True)
self.end_date_edit = QDateEdit(calendarPopup=True)
self.fetch_button = QPushButton("データ取得 & グラフ表示")
controls_layout.addWidget(QLabel("開始日:")); controls_layout.addWidget(self.start_date_edit)
controls_layout.addWidget(QLabel("終了日:")); controls_layout.addWidget(self.end_date_edit)
controls_layout.addWidget(self.fetch_button)
controls_layout.addStretch()
main_layout.addLayout(controls_layout, 1)
# --- 右側:地図 ---
self.map_view = QWebEngineView()
main_layout.addWidget(self.map_view, 3)
# --- 地図とPythonの連携設定 ---
self.map_api = MapApi(self)
self.channel = QWebChannel()
self.channel.registerObject("py_api", self.map_api) # 'py_api'という名前でPythonオブジェクトを登録
self.map_view.page().setWebChannel(self.channel)
self.load_map()
self.setStatusBar(QStatusBar(self))
self.statusBar().showMessage("地図をクリックして地点を選択してください。")
def load_map(self):
# Leaflet.jsを使ったインタラクティブな地図のHTMLを生成
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<!-- (中略: Leafletとqwebchannel.jsの読み込み) -->
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
</head>
<body>
<div id="map"></div>
<script>
var map = L.map('map').setView([{self.lat}, {self.lon}], 10);
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png').addTo(map);
var marker = L.marker([{self.lat}, {self.lon}]).addTo(map);
// Pythonとの連携チャネルを初期化
new QWebChannel(qt.webChannelTransport, function (channel) {{
window.py_api = channel.objects.py_api;
}});
// 地図がクリックされたときのイベント
map.on('click', function(e) {{
var lat = e.latlng.lat;
var lng = e.latlng.lng;
marker.setLatLng([lat, lng]);
// Python側のメソッドを呼び出す!
if (window.py_api) {{
py_api.receive_coords(lat, lng);
}}
}});
</script>
</body>
</html>
"""
self.map_view.setHtml(html_content, QUrl("qrc:/"))
def update_location(self, lat, lon):
# JavaScriptからの呼び出しを受けて、地点情報を更新するメソッド
self.lat = lat
self.lon = lon
self.statusBar().showMessage("地名を取得中...")
QApplication.processEvents() # UIの更新を即座に反映
try:
location = reverse(f"{lat}, {lon}", language='ja')
address = location.raw.get('address', {})
prefecture = address.get('state', '')
city = address.get('city', address.get('town', address.get('village', '')))
self.location_name = f"{prefecture}{city}"
self.statusBar().showMessage("地点を選択しました。", 5000)
except Exception:
self.location_name = "地名不明"
self.statusBar().showMessage("地名取得エラー", 5000)
self.location_label.setText(f"選択地点: {self.location_name}\n(Lat: {self.lat:.4f}, Lon: {self.lon:.4f})")
# (if __name__ == '__main__': ... の実行部分は同じ)
このコードのポイント解説:
MapApi
クラス: JavaScriptからの呼び出しを受け付けるための専用クラスです。@pyqtSlot(float, float)
というデコレータを付けたreceive_coords
メソッドが、JavaScriptからの「窓口」となります。QWebChannel
の設定: メインウィンドウの__init__
内で、MapApi
のインスタンスを"py_api"
という名前でチャネルに登録しています。これにより、JavaScript側でwindow.py_api
としてアクセスできるようになります。- HTML/JavaScript内の連携コード:
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
: PyQtに組み込まれている、連携に必須のJavaScriptライブラリを読み込んでいます。new QWebChannel(...)
: JavaScript側で連携チャネルを初期化しています。py_api.receive_coords(lat, lng)
: 地図がクリックされた際に、登録したPython側のreceive_coords
メソッドを、クリックされた緯度(lat)と経度(lng)を引数として呼び出しています。
update_location
メソッド: JavaScriptから呼び出された後、メインウィンドウの緯度・経度を更新し、geopy
を使って地名を取得(逆ジオコーディング)、そして画面のラベル表示を更新しています。
まとめと次回予告
今回は、PyQt5シリーズの第2回として、アプリケーションにインタラクティブなWebコンテンツ(地図)を埋め込み、さらにJavaScriptとPythonを連携させるという、非常に高度で強力なテクニックを学びました。
QWebEngineView
を使って、PyQt5アプリ内にWebコンテンツを表示する方法。QWebChannel
と@pyqtSlot
を使い、JavaScriptからPythonのメソッドを呼び出す「魔法の架け橋」の作り方。geopy
ライブラリを使った、座標から地名への逆ジオコーディング。
これで、私たちのアプリケーションは、ユーザーが地図上で直感的に場所を選択できる、リッチな入力インターフェースを手に入れました。アプリケーションの骨格と入力部分が完成し、いよいよ全ての部品が揃いました。
次回、シリーズ第3回にして最終回! いよいよ、今回実装したGUIの操作と、以前作成したデータ取得・分析・可視化のロジック(頭脳)を完全に統合します。「データ取得 & グラフ表示」ボタンに命を吹き込み、この気象データ可視化アプリケーションを完成させましょう!お楽しみに!
コメント
コメントを投稿