2023-12-17

PostgreSQL に接続 ~ PySide6

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 の作成しています。

qt_postgres.py
#!/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()
view raw qt_postgres.py hosted with ❤ by GitHub

qt_postgres_dialog.py は、データベースへアクセスするための情報を入力するダイアログ部分のクラスを記載しています。

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 クエリの処理をしています。

qt_postgres_funcs.py
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 クラスのインスタンス上に表示するためのモデルを実装しています。

qt_postgres_model.py
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

参考サイト

  1. bitWalk's: [AlmaLinux 9] PostgreSQL の設定 [2023-12-13]
  2. その他統計資料 | 日本取引所グループ

 

Qt の正式な呼称「キュート」を使っていませんが、丁寧な説明です。

ブログランキング・にほんブログ村へ bitWalk's - にほんブログ村 にほんブログ村 IT技術ブログ オープンソースへ
にほんブログ村

オープンソース - ブログ村ハッシュタグ
#オープンソース



0 件のコメント: