SQLite は、パブリックドメインの軽量なリレーショナルデータベース管理システム (RDBMS) です。他の多くのデータベース管理システムとは対照的に、サーバとしてではなくアプリケーションに組み込んで利用するデータベースです。 一般的な RDBMS と違い、API は単純にライブラリを呼び出すだけであり、データの保存に単一のファイルを使用することが特徴です。
選択した PDF ファイルを読み込んで SQLite のデータベースに格納します。そして、データベースから読み込んだ内容を(一旦保存して) QPdfView クラスのウィジェットで開く、という GUI サンプルを紹介します。
- 本ブログの過去記事 [1] を PySide2 から PySide6 に変更して、PySide6 / Qt6 の機能をなるべく利用できるような内容に見直しました。
下記の OS 環境で動作確認をしています。
![]() |
Fedora Workstation 39 | x86_64 |
Python | 3.11.6 | |
PySide6 | 6.6.1 |
少し長いサンプルになってしまいましたが、以下にサンプル qt_sqlite_pdf.py を示しました。
import base64 | |
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, | |
) | |
def create_table(): | |
query = QSqlQuery() | |
sql = """ | |
CREATE TABLE IF NOT EXISTS file ( | |
name_file TEXT UNIQUE, | |
content NONE | |
); | |
""" | |
if not query.exec(sql): | |
print(query.lastError()) | |
def get_content_from_filename(filename: str) -> bytes: | |
content = None | |
query = QSqlQuery() | |
sql = """ | |
SELECT content FROM file | |
WHERE name_file = "%s"; | |
""" % filename | |
query.exec(sql) | |
if query.next(): | |
content_str = query.value(0) | |
content = base64.b64decode(content_str.encode()) | |
return content | |
def get_list_file(list_file: list): | |
query = QSqlQuery() | |
sql = 'SELECT name_file FROM file;' | |
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_str: str): | |
sql = 'INSERT INTO file VALUES(?, ?);' | |
query = QSqlQuery() | |
query.prepare(sql) | |
query.bindValue(0, filename) | |
query.bindValue(1, content_str) | |
""" | |
note: This does not work! | |
query.bindValue( | |
1, content, | |
type=QSql.ParamTypeFlag.In | QSql.ParamTypeFlag.Binary | |
) | |
""" | |
if not query.exec(): | |
print(query.lastError()) | |
class Example(QMainWindow): | |
def __init__(self): | |
super().__init__() | |
self.dbname = 'test.sqlite' | |
self.con = QSqlDatabase.addDatabase('QSQLITE') | |
self.con.setDatabaseName(self.dbname) | |
self.init_table() | |
self.combo = None | |
self.init_ui() | |
self.update_filelist() | |
self.setWindowTitle('SQLite & PDF Test') | |
self.resize(600, 800) | |
def init_table(self): | |
if self.con.open(): | |
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 = QComboBox() | |
self.combo.setSizePolicy( | |
QSizePolicy.Policy.Expanding, | |
QSizePolicy.Policy.Preferred | |
) | |
self.combo.currentTextChanged.connect(self.on_current_text_changed) | |
toolbar.addWidget(self.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 = base64.b64encode(f.read()) | |
content_str: str = content.decode() | |
if self.con.open(): | |
insert_filename_content(basename, content_str) | |
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 = Example() | |
ex.show() | |
sys.exit(app.exec()) | |
if __name__ == '__main__': | |
main() |
サンプルの説明
初期化
サンプルを実行すると、データベースに接続するインスタンスを生成します。このとき指定した名前のデータベースファイル test.sqlite が存在してければ作成されます。
class Example(QMainWindow): def __init__( self ): super ().__init__() self .dbname = 'test.sqlite' self .con = QSqlDatabase.addDatabase( 'QSQLITE' ) self .con.setDatabaseName( self .dbname) self .init_table() : : |
その後、使用するデータベースのテーブルの初期化をします。
def init_table( self ): if self .con. open (): create_table() self .con.close() |
テーブルの初期化処理は下記のように単純なものです。
def create_table(): query = QSqlQuery() sql = """ CREATE TABLE IF NOT EXISTS file ( name_file TEXT UNIQUE, content NONE ); """ if not query. exec (sql): print (query.lastError()) |
その後、プログラムは 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 = base64.b64encode(f.read()) content_str: str = content.decode() if self .con. open (): insert_filename_content(basename, content_str) self .con.close() self .update_filelist() self .combo.setCurrentText(basename) |
PySide2 を利用した過去記事 [1] では、sqlite3 モジュールを利用して、なにも考えずにバイナリをデータベースに格納できたのですが、PySide6 では QSqlQuery のインスタンスで同じようにしたところ、バイナリを書き込むことができませんでした。
PySide6 ではクエリのタイプにバイナリを指定できるのですが、これでは解決できなかったので、やむなく Python の標準ライブラリで利用できる Base64 でエンコードした byte 型のデータ列を文字列にデコードして格納しています。
def insert_filename_content(filename: str , content_str: str ): sql = 'INSERT INTO file VALUES(?, ?);' query = QSqlQuery() query.prepare(sql) query.bindValue( 0 , filename) query.bindValue( 1 , content_str) """ note: This does not work! query.bindValue( 1, content, type=QSql.ParamTypeFlag.In | QSql.ParamTypeFlag.Binary ) """ if not query. exec (): print (query.lastError()) |
データベースに書き込んだ内容を読み込み直して QPdfView のインスタンスに表示されます。
QPdfView のインスタンス view は、QMainWindow を継承した Example クラスに setCentralWidget メソッドで配置されています。
def init_ui( self ): : : self .combo.currentTextChanged.connect( self .on_current_text_changed) toolbar.addWidget( self .combo) view = QPdfView( self ) view.setPageMode(QPdfView.PageMode.MultiPage) view.setZoomMode(QPdfView.ZoomMode.FitToWidth) self .setCentralWidget(view) |
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 のインスタンスに表示された文字列 = ファイル名に対応するファイルの中身をデータベースから読み込みます。
クエリの処理は下記のようなっています。ここで、取り出した文字列を byte 型にエンコードして、Base64 のデコードをして元のバイナリに戻しています(こうやって説明すると、なんだか面倒くさいことをしていると思います、今後の改善課題です)。
def get_content_from_filename(filename: str ) - > bytes: content = None query = QSqlQuery() sql = """ SELECT content FROM file WHERE name_file = "%s"; """ % filename query. exec (sql) if query. next (): content_str = query.value( 0 ) content = base64.b64decode(content_str.encode()) return content |
データベースから取り出して元に戻したバイナリにファイル名をつけてテンポラリの領域に保存して、それを QPdfDocument のインスタンスで読み込んで、QPdfView のインスタンス view に渡して表示しています。
複数の PDF ファイルの読み込み
複数の PDF ファイルをデータベースに読み込むと、QComboBox のインスタンスにファイル名が列挙されます。どれかを選択すれば、その PDF ファイルが表示されます。
既知の問題
PySide6 / Qt6 の PDF を扱うモジュールをまだ使い慣れていないためか、標準出力に下記のような警告が出ます。特定の PDF ファイルで警告が出るのですが、それが PDF ファイルの問題なのか特定できていません。対応方法が判ったら追記します。
qt.pdf.links: skipping link with invalid page number
qt.pdf.links: skipping link with invalid page number
qt.pdf.links: skipping link with invalid page number
:
:
:
参考サイト
- bitWalk's: 【備忘録】SQLite のデータベースでバイナリデータを扱う [2021-02-18]
- QPdfDocument - Qt for Python
- QPdfView - Qt for Python

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

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