PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux/X11, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。
当ブログで AlmaLinux に PostgreSQL のサーバを設定した記事をノートにまとめました [1]。その記事に関連して、PySide6 で PostgreSQL サーバーのデータにアクセスする GUI サンプルを作りました。
PySide6 の 機能を利用して、PostgreSQL サーバーにある指定したデータベースにアクセスして、適当なサイズのテーブルの内容をすべて読み込んで表示します。
わざわざ「PySide6 の 機能を利用して」と書いているのは、GUI だけでなく、データベースへのアクセスも PySide6 / Qt6 の機能を利用するということを意図しています。
下記の OS 環境で動作確認をしています。
![]() |
Fedora Workstation 39 | x86_64 |
Python | 3.11.6 | |
PySide6 | 6.6.1 | |
pandas | 2.1.3 | |
(libpq-15.3-1.fc39.x86_64) | ||
![]() |
AlmaLinux 9.3 (192.168.0.34) | x86_64 |
PostgreSQL (server) | 15.3 |
サンプルの説明
単純なサンプルですが、動作の流れを説明します。
サンプル qt_postgres.py を起動すると下記のようなウィンドウが表示されます。
ウィンドウ左上のアイコンをクリックすると、データベースへ接続するのに必要な情報を入力するダイアログが表示されます。
ホスト名(あるいは IP アドレス)、データベース名、テーブル名、そしてデータベースにアクセスするためのユーザー名とそのパスワードを入力して、 OK ボタンをクリックします。
データベースへのアクセスに成功すると、下記のようにテーブルの全内容が表示されます。
このデータは、参考サイト [2] から「東証上場銘柄一覧」の Excel ファイルをダウンロードして、必要な情報だけをテーブル (ticker) にまとめたものです。データ量は 3,827 行(レコード)× 11 カラムです。
他のテーブルで検証していないので保証はできないのですが、テーブルの構成に依存しないようにサンプルを作っています。
ただ、単純にすべてのデータを読み込むだけのサンプルですので、何万行もあるようなテーブルを読み込むと、GUI が表示のためにメモリを消費し尽す可能性があります。ほどほどのサイズのテーブルを読み込むようにしてください。
このサンプルでは、データベースにアクセス後、下記の SQL を発行して、対象のテーブルのカラム名を確認しています。info['table'] はテーブル名(この場合 ticker)をあらわす文字列です。
sql = """ SELECT column_name FROM information_schema.columns WHERE table_name = '%s'; """ % info['table']
次に、下記の SQL を発行してテーブルの全内容を読み込んでいます。
sql = 'SELECT * FROM %s;' % info['table']
データベースにアクセスするクエリは QSqlQuery クラスのインスタンスを利用しています。
読み込んだ内容は、あらかじめカラム名のリストから辞書を作って、カラムのデータリストとして一時保存しておき、Pandas のデータフレームに変換した後 QTableView のモデルに渡して表示しています。
このサンプルの用途では、データベースのテーブルの内容をわざわざ Pandas のデータフレームに変換する必要はありません。しかし、現実にはデータ解析をする関係で、Pandas のデータフレームにしておいた方が都合が良いので、敢えてデータフレームに変換しています。そのため、QTableView のモデルも Pandas のデータフレームを表示することを前提としています。
サンプルスクリプト
サンプルは前述したような単純な機能で、しかもエラーチェックをしていない代物ですが、それでも結構な行数になってしまいました。そこで今回は下記の4つにサンプルを分けました。サンプルを試す場合は同じディレクトリに4つのファイルを保存して、qt_postgres.py を実行してください。
もちろん、確認のためには PostgreSQL 上に読み込むためのデータベースとテーブルが存在している必要があります。
- qt_postgres.py
- qt_postgres_dialog.py
- qt_postgres_funcs.py
- qt_postgres_model.py
qt_postgres.py は、サンプルのメインプログラムで主に GUI の作成しています。
#!/usr/bin/env python | |
# coding: utf-8 | |
import pandas as pd | |
import sys | |
from PySide6.QtSql import QSqlDatabase, QSqlQuery | |
from PySide6.QtWidgets import ( | |
QApplication, | |
QHeaderView, | |
QMainWindow, | |
QStyle, | |
QTableView, | |
QToolBar, | |
QToolButton, | |
QWidget, | |
) | |
from qt_postgres_dialog import DBInfoDlg | |
from qt_postgres_funcs import ( | |
db_get_all_contents, | |
db_get_col_info, | |
) | |
from qt_postgres_model import DataFrameModel | |
class Example(QMainWindow): | |
def __init__(self): | |
super().__init__() | |
self.init_ui() | |
self.setWindowTitle('DB Connection test') | |
self.resize(800, 600) | |
def init_ui(self): | |
# toolbar | |
toolbar = QToolBar() | |
self.add_button_to_toolbar(toolbar) | |
self.addToolBar(toolbar) | |
# table view | |
view = QTableView() | |
view.setAlternatingRowColors(True) | |
self.setCentralWidget(view) | |
# horizontal header of table | |
header = view.horizontalHeader() | |
header.setSectionResizeMode( | |
QHeaderView.ResizeMode.ResizeToContents | |
) | |
def add_button_to_toolbar(self, toolbar: QToolBar): | |
but_connect = QToolButton() | |
but_connect.setText('Connect') | |
but_connect.setToolTip('Connect with PostgreSQL') | |
pixmap_connect = QStyle.StandardPixmap.SP_CommandLink | |
icon_connect = self.style().standardIcon(pixmap_connect) | |
but_connect.setIcon(icon_connect) | |
but_connect.clicked.connect(self.button_clicked) | |
toolbar.addWidget(but_connect) | |
def button_clicked(self): | |
obj: QToolButton = self.sender() | |
if obj.text() == 'Connect': | |
self.show_db_info_dlg() | |
def show_db_info_dlg(self): | |
dict_info = dict() | |
dlg = DBInfoDlg(dict_info) | |
if dlg.exec(): | |
self.db_connection(dict_info) | |
def db_connection(self, dict_info: dict): | |
con = QSqlDatabase.addDatabase('QPSQL') | |
con.setHostName(dict_info['host']) | |
con.setDatabaseName(dict_info['database']) | |
con.setUserName(dict_info['user']) | |
con.setPassword(dict_info['password']) | |
if con.open(): | |
print('connected!') | |
query = QSqlQuery() | |
# table columns | |
list_col = list() | |
db_get_col_info(dict_info, list_col, query) | |
# table contents | |
dict_val = dict() | |
db_get_all_contents(dict_info, dict_val, list_col, query) | |
# close connection | |
con.close() | |
# show table | |
df = pd.DataFrame(dict_val) | |
print(df.shape) | |
model = DataFrameModel(df) | |
view: QWidget | QTableView = self.centralWidget() | |
view.setModel(model) | |
else: | |
print('NOT connected!') | |
def main(): | |
app = QApplication(sys.argv) | |
ex = Example() | |
ex.show() | |
sys.exit(app.exec()) | |
if __name__ == '__main__': | |
main() |
qt_postgres_dialog.py は、データベースへアクセスするための情報を入力するダイアログ部分のクラスを記載しています。
from PySide6.QtCore import Qt | |
from PySide6.QtWidgets import ( | |
QDialog, | |
QDialogButtonBox, | |
QFrame, | |
QGridLayout, | |
QLabel, | |
QLineEdit, | |
QSizePolicy, | |
QVBoxLayout, | |
QWidget, | |
) | |
class Label(QLabel): | |
def __init__(self, text: str): | |
super().__init__(text) | |
self.setLineWidth(1) | |
self.setFrameStyle( | |
QFrame.Shape.StyledPanel | QFrame.Shadow.Raised | |
) | |
self.setSizePolicy( | |
QSizePolicy.Policy.Expanding, | |
QSizePolicy.Policy.Preferred | |
) | |
class Entry(QLineEdit): | |
def __init__(self, key: str): | |
super().__init__() | |
self.key: str = key | |
self.setSizePolicy( | |
QSizePolicy.Policy.Expanding, | |
QSizePolicy.Policy.Preferred | |
) | |
self.setStyleSheet("QLineEdit{background-color:white;}") | |
def getKey(self) -> str: | |
return self.key | |
class DBInfoDlg(QDialog): | |
def __init__(self, dict_info: dict): | |
super().__init__() | |
self.dict_info = dict_info | |
self.setWindowTitle('DB Info') | |
vbox = QVBoxLayout() | |
vbox.setContentsMargins(0, 0, 0, 0) | |
vbox.setAlignment(Qt.AlignmentFlag.AlignTop) | |
self.setLayout(vbox) | |
base = QWidget() | |
self.gen_entries(base) | |
vbox.addWidget(base) | |
dlgbtn = QDialogButtonBox.StandardButton.Ok | |
bbox = QDialogButtonBox(dlgbtn) | |
bbox.accepted.connect(self.accept) | |
vbox.addWidget(bbox) | |
def gen_entries(self, base): | |
grid = QGridLayout() | |
grid.setContentsMargins(0, 0, 0, 0) | |
grid.setSpacing(1) | |
base.setLayout(grid) | |
key = 'host' | |
row = 0 | |
self.gen_row(grid, key, row) | |
key = 'database' | |
row = 1 | |
self.gen_row(grid, key, row) | |
key = 'table' | |
row = 2 | |
self.gen_row(grid, key, row) | |
key = 'user' | |
row = 3 | |
self.gen_row(grid, key, row) | |
key = 'password' | |
row = 4 | |
self.gen_row(grid, key, row, True) | |
grid.setColumnStretch(0, 0) | |
grid.setColumnStretch(1, 1) | |
def gen_row(self, grid: QGridLayout, key: str, row: int, flag: int = False): | |
lab = Label(key) | |
grid.addWidget(lab, row, 0) | |
ent = Entry(key) | |
if flag: | |
ent.setEchoMode(QLineEdit.EchoMode.Password) | |
ent.textChanged.connect(self.entry_changed) | |
grid.addWidget(ent, row, 1) | |
def entry_changed(self, content: str): | |
ent: Entry = self.sender() | |
key = ent.getKey() | |
self.dict_info[key] = content |
qt_postgres_funcs.py は、主に SQL クエリの処理をしています。
from PySide6.QtSql import QSqlQuery | |
def db_get_col_info(info: dict, cols: list, query: QSqlQuery): | |
sql = """ | |
SELECT column_name | |
FROM information_schema.columns | |
WHERE table_name = '%s'; | |
""" % info['table'] | |
query.exec(sql) | |
while query.next(): | |
cols.append(query.value(0)) | |
def db_get_all_contents(info: dict, vals: dict, cols: list, query: QSqlQuery): | |
for key in cols: | |
vals[key] = list() | |
sql = 'SELECT * FROM %s;' % info['table'] | |
query.exec(sql) | |
while query.next(): | |
for i, key in enumerate(cols): | |
vals[key].append(query.value(i)) |
qt_postgres_model.py は、Pandas のデータフレームを QTableView クラスのインスタンス上に表示するためのモデルを実装しています。
from typing import Any | |
import numpy as np | |
import pandas as pd | |
from PySide6.QtCore import ( | |
QAbstractTableModel, | |
QModelIndex, | |
Qt, | |
) | |
class DataFrameModel(QAbstractTableModel): | |
def __init__(self, dataframe: pd.DataFrame, parent=None): | |
QAbstractTableModel.__init__(self, parent) | |
self._dataframe = dataframe | |
def rowCount(self, parent=QModelIndex()) -> int: | |
if parent == QModelIndex(): | |
return len(self._dataframe) | |
return 0 | |
def columnCount(self, parent=QModelIndex()) -> int: | |
if parent == QModelIndex(): | |
return len(self._dataframe.columns) | |
return 0 | |
def data(self, index: QModelIndex, role=Qt.ItemDataRole): | |
if not index.isValid(): | |
return None | |
row = index.row() | |
col = index.column() | |
value = self._dataframe.iloc[row, col] | |
if role == Qt.ItemDataRole.DisplayRole: | |
return str(value) | |
elif role == Qt.ItemDataRole.TextAlignmentRole: | |
if (type(value) is np.int64) | (type(value) is np.float64): | |
flag = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter | |
else: | |
flag = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter | |
return flag | |
return None | |
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> Any: | |
if role == Qt.ItemDataRole.DisplayRole: | |
if orientation == Qt.Orientation.Horizontal: | |
return str(self._dataframe.columns[section]) | |
if orientation == Qt.Orientation.Vertical: | |
return None | |
return None |
参考サイト
Qt の正式な呼称「キュート」を使っていませんが、丁寧な説明です。

にほんブログ村
#オープンソース

0 件のコメント:
コメントを投稿