PySide (Qt for Python) は、Qt(キュート)の Python バインディングで、GUI などを構築するためのクロスプラットフォームなライブラリです。Linux/X11, macOS および Microsoft Windows をサポートしています。配布ライセンスは LGPL で公開されています。
本ブログの過去記事 [1] で、PDF ファイルを SQLite のデータベースに格納する PySide6 のサンプルを紹介しました。その後 PostgreSQL でも同じことをしようと試行錯誤していたところ、バイナリをデータベースに格納するのに Base64 のモジュールより、PySide6 の QByteArray [2] クラスを利用した方がシンプルになりそうだったので、書き直すことにしました。
また SQLite と PostgreSQL それぞれに対応したサンプルを、なるべく共通な部分を流用することで一緒に紹介することにしました。
- 選択した PDF ファイルを読み込んでデータベースへ格納します。そして、データベースから読み込んだ内容を(一旦保存して) QPdfView クラスのウィジェット上に表示する、という GUI サンプルを紹介します。
- データベースは最初 SQLite で確認したあと、PostgreSQL でも同じことができるサンプルを作成します。そのため、SQLite と PostgreSQL で共通に使える部分が多くなるように意識してサンプルを作成します。
なお、PDF ファイルのサンプルは、東京国立博物館ニュースの PDF ファイルを利用させていただきました [3]。
下記の OS 環境で動作確認をしています。
![]() |
Fedora Workstation 39 | x86_64 |
Python | 3.11.7 | |
PySide6 | 6.6.1 | |
(libpq-15.3-1.fc39.x86_64) | ||
![]() |
AlmaLinux 9.3 (192.168.0.34) | x86_64 |
PostgreSQL (server) | 15.5 |
【注意】
本稿で紹介するサンプルは、シンプルで判りやすく、かつサイズ(行数)を抑えることを優先したため、想定しうるエラー処理をほとんどしていません。
データベースに SQLite を利用したサンプル
以下にサンプル qt_db_sqlite_pdf.py を示しました(SQLitePDF クラス)。
- qt_db_sqlite_pdf.py(メイン)
- qt_db_common_pdf.py
import os | |
import sys | |
import tempfile | |
from PySide6.QtCore import Qt | |
from PySide6.QtGui import QAction | |
from PySide6.QtPdf import QPdfDocument | |
from PySide6.QtPdfWidgets import QPdfView | |
from PySide6.QtSql import QSqlDatabase, QSqlQuery | |
from PySide6.QtWidgets import ( | |
QApplication, | |
QComboBox, | |
QFileDialog, | |
QMainWindow, | |
QSizePolicy, | |
QToolBar, | |
QWidget, | |
) | |
from qt_db_common_pdf import ( | |
get_content_from_filename, | |
get_list_file, | |
insert_filename_content, | |
) | |
class SQLitePDF(QMainWindow): | |
app_title = 'SQLite & PDF test' | |
def __init__(self): | |
super().__init__() | |
self.con = self.get_connection() | |
self.init_table() | |
self.combo = None | |
self.init_ui() | |
self.setWindowTitle(self.app_title) | |
self.resize(600, 800) | |
self.update_filelist() | |
@staticmethod | |
def create_table(): | |
query = QSqlQuery() | |
sql = """ | |
CREATE TABLE IF NOT EXISTS pdfrepo ( | |
name_file TEXT UNIQUE, | |
content NONE | |
); | |
""" | |
if not query.exec(sql): | |
print(query.lastError()) | |
@staticmethod | |
def get_connection() -> QSqlDatabase: | |
con = QSqlDatabase.addDatabase('QSQLITE') | |
dbname = 'testdb.sqlite' | |
con.setDatabaseName(dbname) | |
return con | |
def init_table(self): | |
if self.con.open(): | |
self.create_table() | |
self.con.close() | |
def init_ui(self): | |
menubar = self.menuBar() | |
menu_file = menubar.addMenu('&File') | |
file_open = QAction('Open', self) | |
file_open.setShortcut('Ctrl+O') | |
file_open.setStatusTip('open PDF file') | |
file_open.triggered.connect(self.show_dialog) | |
menu_file.addAction(file_open) | |
toolbar = QToolBar() | |
self.addToolBar(Qt.ToolBarArea.TopToolBarArea, toolbar) | |
self.combo = combo = QComboBox() | |
combo.setSizePolicy( | |
QSizePolicy.Policy.Expanding, | |
QSizePolicy.Policy.Preferred | |
) | |
combo.currentTextChanged.connect(self.on_current_text_changed) | |
toolbar.addWidget(combo) | |
view = QPdfView(self) | |
view.setPageMode(QPdfView.PageMode.MultiPage) | |
view.setZoomMode(QPdfView.ZoomMode.FitToWidth) | |
self.setCentralWidget(view) | |
def show_dialog(self): | |
dialog = QFileDialog() | |
dialog.setWindowTitle('PDF file selection') | |
dialog.setNameFilters(['PDF files (*.pdf)']) | |
if dialog.exec(): | |
filename = dialog.selectedFiles()[0] | |
basename = os.path.basename(filename) | |
f = open(filename, 'rb') | |
with f: | |
content = f.read() | |
if self.con.open(): | |
insert_filename_content(basename, content) | |
self.con.close() | |
self.update_filelist() | |
self.combo.setCurrentText(basename) | |
def update_filelist(self): | |
list_name_file = list() | |
if self.con.open(): | |
get_list_file(list_name_file) | |
self.con.close() | |
self.combo.clear() | |
for name_file in list_name_file: | |
self.combo.addItem(name_file) | |
def on_current_text_changed(self): | |
filename = self.combo.currentText() | |
if len(filename) == 0: | |
return | |
if self.con.open(): | |
content = get_content_from_filename(filename) | |
self.con.close() | |
if content is not None: | |
filepath = os.path.join(tempfile.gettempdir(), filename) | |
with open(filepath, 'wb') as f: | |
f.write(content) | |
document = QPdfDocument(self) | |
document.load(filepath) | |
view: QWidget | QPdfView = self.centralWidget() | |
view.setDocument(document) | |
def main(): | |
app = QApplication() | |
ex = SQLitePDF() | |
ex.show() | |
sys.exit(app.exec()) | |
if __name__ == '__main__': | |
main() |
SQLite と PostgreSQL で変更無しに利用できるクエリ処理部分を qt_db_common_pdf.py に分離しました。
from PySide6.QtCore import QByteArray | |
from PySide6.QtSql import QSqlQuery | |
def get_content_from_filename(filename: str) -> bytes: | |
byte_array = None | |
content = None | |
query = QSqlQuery() | |
sql = """ | |
SELECT content FROM pdfrepo | |
WHERE name_file = '%s'; | |
""" % filename | |
flag = query.exec(sql) | |
if query.next(): | |
byte_array = query.value(0) | |
if not flag: | |
print(query.lastError()) | |
if byte_array is not None: | |
content = byte_array.data() | |
return content | |
def get_list_file(list_file: list): | |
query = QSqlQuery() | |
sql = 'SELECT name_file FROM pdfrepo;' | |
flag = query.exec(sql) | |
while query.next(): | |
list_file.append(query.value(0)) | |
if not flag: | |
print(query.lastError()) | |
def insert_filename_content(filename: str, content: bytes): | |
sql = 'INSERT INTO pdfrepo VALUES(?, ?);' | |
query = QSqlQuery() | |
query.prepare(sql) | |
query.bindValue(0, filename) | |
query.bindValue(1, QByteArray(content)) | |
if not query.exec(): | |
print(query.lastError()) |
サンプルの説明
初期化
サンプルを実行すると、データベースに接続するインスタンスを生成します。このとき指定した名前のデータベース・ファイル test.sqlite が存在してければ作成されます。
class SQLitePDF(QMainWindow): app_title = 'SQLite & PDF test' def __init__( self ): super ().__init__() self .con = self .get_connection() self .init_table() : : |
データベースに接続するインスタンスの生成は、SQLite と PostgreSQL では異なるので、あとあとのことを考えて get_connection メソッドで処理するようにしました。
@staticmethod def get_connection() - > QSqlDatabase: con = QSqlDatabase.addDatabase( 'QSQLITE' ) dbname = 'testdb.sqlite' con.setDatabaseName(dbname) return con |
その後 init_table メソッドで、使用するデータベースのテーブルの初期化をします。
def init_table( self ): if self .con. open (): self .create_table() self .con.close() |
テーブルの初期化処理は create_table メソッドで処理しています。
@staticmethod def create_table(): query = QSqlQuery() sql = """ CREATE TABLE IF NOT EXISTS pdfrepo ( name_file TEXT UNIQUE, content NONE ); """ if not query. exec (sql): print (query.lastError()) |
なお、get_connection と create_table は、敢えてスタティック・メソッドとして SQLitePDF クラス内に残しています。
その後、プログラムは GUI を生成します。
PDF ファイルを読み込んでみる
実行したサンプルのメニューから File » Open をクリックします。
データベースに格納する PDF ファイルを選択します。
バイナリーデータをデータベースへ格納
ファイルをバイナリモードで読み込んで、パスを除いたファイル名と読み込んだ内容を、データベースに格納します。
def show_dialog( self ): dialog = QFileDialog() dialog.setWindowTitle( 'PDF file selection' ) dialog.setNameFilters([ 'PDF files (*.pdf)' ]) if dialog. exec (): filename = dialog.selectedFiles()[ 0 ] basename = os.path.basename(filename) f = open (filename, 'rb' ) with f: content = f.read() if self .con. open (): insert_filename_content(basename, content) self .con.close() self .update_filelist() self .combo.setCurrentText(basename) |
PySide6 でバイナリ・データをそのままクエリに渡してもデータベースに格納できなかったのですが、QByteArray 型にすると格納できました。QByteArray 型にするということは、読み出すときにも QByteArray 型を扱えるようにする必要があるので、「PySide6 あるいは Qt6 のライブラリを使わなければならない」という縛りができてしまいますが、ここではそれを良しとしました。
このクエリを処理する insert_filename_content 関数は PostgreSQL でも利用します。
def insert_filename_content(filename: str , content: bytes): sql = 'INSERT INTO pdfrepo VALUES(?, ?);' query = QSqlQuery() query.prepare(sql) query.bindValue( 0 , filename) query.bindValue( 1 , QByteArray(content)) if not query. exec (): print (query.lastError()) |
データベースに書き込んだ内容を読み込み直して QPdfView のインスタンスに表示されます。
データベースにあるファイルリストの取得
QPdfView のインスタンス view は、QMainWindow を継承した SQLitePDF クラスに setCentralWidget メソッドで配置されています。
def init_ui( self ): : : combo.currentTextChanged.connect( self .on_current_text_changed) toolbar.addWidget(combo) view = QPdfView( self ) view.setPageMode(QPdfView.PageMode.MultiPage) view.setZoomMode(QPdfView.ZoomMode.FitToWidth) self .setCentralWidget(view) |
QToolBar にある QComboBox に表示するファイルリストは下記の関数のクエリで取得しています。
このクエリを処理する get_list_file 関数は PostgreSQL でも利用します。
def get_list_file(list_file: list ): query = QSqlQuery() sql = 'SELECT name_file FROM pdfrepo;' flag = query. exec (sql) while query. next (): list_file.append(query.value( 0 )) if not flag: print (query.lastError()) |
QComboBox のインスタンスに表示されている文字が変更されるとシグナルが発生して、インスタンス・メソッド on_current_text_changed メソッドが実行されます。
def on_current_text_changed( self ): filename = self .combo.currentText() if len (filename) = = 0 : return if self .con. open (): content = get_content_from_filename(filename) self .con.close() if content is not None : filepath = os.path.join(tempfile.gettempdir(), filename) with open (filepath, 'wb' ) as f: f.write(content) document = QPdfDocument( self ) document.load(filepath) view: QWidget | QPdfView = self .centralWidget() view.setDocument(document) |
このメソッドで、データベースから QComboBox のインスタンスに表示された文字列 = ファイル名に対応するファイルの中身(バイナリ・データ)をデータベースから読み込みます(get_content_from_filename 関数)。
データベースから取り出して元に戻したバイナリにファイル名をつけてテンポラリの領域に保存して、それを QPdfDocument のインスタンスで読み込んで、QPdfView のインスタンス view に渡して表示しています。
【追記】テンポラリ領域に保存しない方法 (2024-01-04)
データベースから取り出したバイナリをファイルに保存せずに QPdfDocument のインスタンスに読み込ませる方が効率的です。あとになって動作確認ができたので、追記します。
def on_current_text_changed( self ): filename = self .combo.currentText() if len (filename) = = 0 : return if self .con. open (): content = get_content_from_filename(filename) self .con.close() if content is not None : byte_array = QByteArray(content) buffer = QBuffer(byte_array) buffer . open (QIODevice.OpenModeFlag.ReadOnly) document = QPdfDocument( self ) document.load( buffer ) view: QWidget | QPdfView = self .centralWidget() view.setDocument(document) |
QByteArray 型を QBuffer のインスタンスに渡す関係で、get_content_from_filename 関数でデータベースから取り出した際に bytes 型に戻したのに、再び QByteArray 型にすることになりました。最初にサンプルを作った流れを踏襲したので無駄な処理になってしまっています。🙇🏻
バイナリーデータをデータベースから取り出す
クエリで取り出した QByteArray 型のオブジェクトから data メソッドで元のバイナリ取り出して返しています。
この get_content_from_filename 関数は PostgreSQL でも利用します。
def get_content_from_filename(filename: str ) - > bytes: byte_array = None content = None query = QSqlQuery() sql = """ SELECT content FROM pdfrepo WHERE name_file = '%s'; """ % filename flag = query. exec (sql) if query. next (): byte_array = query.value( 0 ) if not flag: print (query.lastError()) if byte_array is not None : content = byte_array.data() return content |
複数の PDF ファイルの読み込み
複数の PDF ファイルをデータベースに読み込むと、QComboBox のインスタンスにファイル名が列挙されます。どれかを選択すれば、その PDF ファイルが表示されます。
データベースに PostgreSQL を利用したサンプル
以下にサンプル qt_db_postgres_pdf.py を示しました(PostgresPDF クラス)。
- qt_db_sqlite_pdf.py(SQLite 版のスクリプト)
- qt_db_common_pdf.py(SQLite 版のスクリプト)
- qt_db_postgres_pdf.py(メイン)
- qt_db_postgres_pdf.py(データベース接続用ダイアログ)
import sys | |
from PySide6.QtSql import QSqlDatabase, QSqlQuery | |
from PySide6.QtWidgets import QApplication | |
from qt_db_postgres_dialog import DBInfoDlg | |
from qt_db_sqlite_pdf import SQLitePDF | |
class PostgresPDF(SQLitePDF): | |
app_title = 'PostgreSQL & PDF test' | |
def __init__(self): | |
super().__init__() | |
@staticmethod | |
def create_table(): | |
query = QSqlQuery() | |
sql = """ | |
CREATE TABLE IF NOT EXISTS pdfrepo ( | |
name_file character varying(255) UNIQUE, | |
content bytea | |
); | |
""" | |
if not query.exec(sql): | |
print(query.lastError()) | |
@staticmethod | |
def get_connection() -> QSqlDatabase: | |
con = QSqlDatabase.addDatabase('QPSQL') | |
dict_info = dict() | |
dlg = DBInfoDlg(dict_info) | |
if dlg.exec(): | |
con.setHostName(dict_info['host']) | |
con.setDatabaseName(dict_info['database']) | |
con.setUserName(dict_info['user']) | |
con.setPassword(dict_info['password']) | |
return con | |
def main(): | |
app = QApplication() | |
ex = PostgresPDF() | |
ex.show() | |
sys.exit(app.exec()) | |
if __name__ == '__main__': | |
main() |
PostgreSQL データベースへ接続するのに必要な情報を入力するダイアログ、DBInfoDlg クラスを用意しました (qt_db_postgres_dialog.py)。サンプルの起動の度に入力するのは面倒ですが、安易にパスワードを露わにして処理することに躊躇しました。😅
from PySide6.QtCore import Qt | |
from PySide6.QtWidgets import ( | |
QDialog, | |
QDialogButtonBox, | |
QFrame, | |
QGridLayout, | |
QLabel, | |
QLineEdit, | |
QSizePolicy, | |
QVBoxLayout, | |
QWidget, | |
) | |
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.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): | |
layout = QGridLayout() | |
layout.setContentsMargins(0, 0, 0, 0) | |
layout.setSpacing(1) | |
base.setLayout(layout) | |
for row, key in enumerate(['host', 'database', 'user', 'password']): | |
if key == 'password': | |
self.gen_row(layout, key, row, True) | |
else: | |
self.gen_row(layout, key, row) | |
layout.setColumnStretch(0, 0) | |
layout.setColumnStretch(1, 1) | |
def gen_row(self, grid: QGridLayout, key: str, row: int, flag: int = False): | |
lab = QLabel(key) | |
lab.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Raised) | |
lab.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) | |
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 |
サンプルの説明
さて、メインの PostgresPDF クラスですが、SQLite を利用した前述の SQLitePDF クラスを継承して、PostgresSQL 用にオーバーライドしています。
class PostgresPDF(SQLitePDF): app_title = 'PostgreSQL & PDF test' def __init__( self ): super ().__init__() |
オーバーライドしているのは、ウィンドウに表示するタイトル文字列と、スタティック・メソッドにした下記の二つのメソッドです。
なお、テーブルを作成する SQL は、SQLite と PostgreSQL で共通にできるのかもしれませんが、ここでは別々にしました。
@staticmethod def create_table(): query = QSqlQuery() sql = """ CREATE TABLE IF NOT EXISTS pdfrepo ( name_file character varying(255) UNIQUE, content bytea ); """ if not query. exec (sql): print (query.lastError()) |
データベースとの接続を処理する get_connection メソッドでは、DBInfoDlg クラスのダイアログを表示して、接続に必要な情報を取得しています。
@staticmethod def get_connection() - > QSqlDatabase: con = QSqlDatabase.addDatabase( 'QPSQL' ) dict_info = dict () dlg = DBInfoDlg(dict_info) if dlg. exec (): con.setHostName(dict_info[ 'host' ]) con.setDatabaseName(dict_info[ 'database' ]) con.setUserName(dict_info[ 'user' ]) con.setPassword(dict_info[ 'password' ]) return con |
サンプル qt_db_postgres_pdf.py を実行すると、最初にデータベースへ接続するための情報を入力するダイアログが表示されます。必要事項を正しく入力して OK ボタンをクリックします。入力情報が間違っていてもなにか警告が出るようにはなっていませんのでご注意ください。🙇🏻
あとの操作は、SQLite 版 (qt_db_sqlite_pdf.py) と同じです。
まとめ
データベースを利用する規模や状況にもよるのでしょうが、定型のレポートなど、あまり大きくないサイズのファイルをデータベースにバイナリで格納するはアリだと思っているので、PDF を例にしてサンプルを作ってみました。
また、なにかデータベースを利用した GUI アプリを作るのに、まず試しに SQLite で動作確認をしてから PostgreSQL などのデータベース・サーバーに移行する、ということがよくあります。移行の際、あまり大きな変更をしなくとも PostgreSQL へ移行するのに効果的な方法はないものかと模索している一環として、クラスを継承するというアプローチを取ってみました。
参考サイト
- bitWalk's: SQLite と PDF ファイル ~ PySide6 [2023-12-20]
- QByteArray - Qt for Python
- 東京国立博物館 - 調査・研究・貸与 出版・刊行物 東京国立博物館ニュース

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