PySide6 実践講座 #6 設定画面 — QSettings、ダークテーマ、データバックアップ
ここまでリマインダーの時刻は 21 時に、閉じる動作は非表示にとコードに埋め込まれていました。良い道具はこうした決定をユーザーに返します。今回は設定画面を作り、テーマを着せ、データをエクスポートする、アプリの完成度を引き上げる回です。
QSettings — 設定保存の標準解 #
設定を自前の JSON ファイルで管理することもできますが、Qt にはすでに標準解があります。QSettings はキーと値をプラットフォームごとの標準の場所(macOS は plist、Windows はレジストリ、Linux は ini ファイル)に保存してくれます。第 1 回で setOrganizationName と setApplicationName を設定しておいたおかげで、引数なしで作っても正しい場所に書き込まれます。
# src/daily/settings.py
from PySide6.QtCore import QSettings
class AppSettings:
"""QSettings を包み、キーのタイプミスと型のミスを一か所で防ぐ。"""
def __init__(self) -> None:
self._s = QSettings()
@property
def reminder_hour(self) -> int:
return int(self._s.value("reminder/hour", 21))
@reminder_hour.setter
def reminder_hour(self, value: int) -> None:
self._s.setValue("reminder/hour", value)
@property
def close_to_tray(self) -> bool:
return self._s.value("window/close_to_tray", True, type=bool)
@close_to_tray.setter
def close_to_tray(self, value: bool) -> None:
self._s.setValue("window/close_to_tray", value)
@property
def theme(self) -> str: # "system" | "light" | "dark"
return str(self._s.value("appearance/theme", "system"))
@theme.setter
def theme(self, value: str) -> None:
self._s.setValue("appearance/theme", value)ラッパークラスを一枚かませる理由は、QSettings の有名な罠のためです。保存した値がプラットフォームによっては文字列で返ってくることがあります。 bool を保存したのに "true" という文字列が返ってきて if の判定がひっくり返る事故が典型です。type=bool の指定や明示的な変換をラッパーの中に集めておけば、この罠をアプリ全体で一度だけ処理すれば済みます。キー名も "reminder/hour" のようにグループ/名前形式の定数になり、タイプミスが消えます。
設定画面と動作の接続 #
設定ページ自体は入門で学んだフォームの組み立てです。要点はウィジェットではなく接続です。
# src/daily/ui/settings_page.py(核心のみ)
class SettingsPage(QWidget):
def __init__(self, settings: AppSettings, reminder, main_window) -> None:
super().__init__()
self._settings = settings
hour = QSpinBox()
hour.setRange(0, 23)
hour.setValue(settings.reminder_hour)
hour.valueChanged.connect(self._on_hour_changed)
close_tray = QCheckBox("閉じるボタンでトレイに隠す")
close_tray.setChecked(settings.close_to_tray)
close_tray.toggled.connect(self._on_close_mode_changed)
...
def _on_hour_changed(self, value: int) -> None:
self._settings.reminder_hour = value
self._reminder.set_hour(value) # 実行中のリマインダーに即反映原則は 2 行です。保存と適用を一緒にやる。 設定値を QSettings に書いて終わりにすると、「再起動しないと反映されない」アプリになります。第 5 回の Reminder に set_hour メソッドを足し、closeEvent がフラグの代わりに settings.close_to_tray を読むように直せば、設定はその場で生きて動きます。そして 「OK/キャンセル」ダイアログではなく即時適用を選びました。ローカルツールアプリの設定は、変えたらすぐ反映されるほうが今の慣例に合います。
テーマ — スタイルシートの差し替えとシステム検知 #
Qt ウィジェットの外見は、CSS に似たスタイルシート(QSS)で変えられます。テーマ実装の骨組みは「ダーク用 QSS 文字列とライト用 QSS 文字列を用意しておき、app.setStyleSheet で差し替える」です。
# src/daily/ui/theme.py(抜粋)
DARK_QSS = """
QWidget { background: #1e1f22; color: #e6e6e6; }
QPushButton { background: #2d333b; border: 1px solid #444c56; padding: 6px 12px; border-radius: 4px; }
QPushButton:hover { background: #39404a; }
QListView { background: #16171a; border: none; }
"""
def apply_theme(app: QApplication, mode: str) -> None:
if mode == "system":
scheme = app.styleHints().colorScheme() # OS のライト/ダーク設定
mode = "dark" if scheme == Qt.ColorScheme.Dark else "light"
app.setStyleSheet(DARK_QSS if mode == "dark" else "")押さえるポイントが 2 つあります。第一に、“system” モードは styleHints().colorScheme() で OS の設定を読み、colorSchemeChanged シグナルにつないでおけば、OS のテーマが変わったときアプリも追従します。第二に、第 4 回のヒートマップのように自分で描くウィジェットはスタイルシートの影響を受けません。 ハードコードしていた色をパレットやテーママジュールの定数から読むように直さないと、テーマ切り替えに反応しません。カスタム描画の隠れたコストがこういうところに現れます。
データのエクスポート — ユーザーのデータはユーザーのもの #
ローカルアプリの最後の礼儀は、データを人質に取らないことです。2 つを提供します。
def export_csv(repo: HabitRepository, path: Path) -> None:
import csv
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["habit", "day"])
for habit in repo.active_habits():
for day in sorted(repo.checked_days(habit.id)):
writer.writerow([habit.name, day])- CSV エクスポート: 保存先は
QFileDialog.getSaveFileNameでユーザーが選びます。表計算ソフトで開ける形式であること自体が、「自分のデータを持ち出せる」という信頼になります。 - DB バックアップ: sqlite3 の
Connection.backup()を使えば、使用中の DB も安全にコピーできます。ファイルをそのままコピーするより安全な、SQLite が提供する正式なバックアップ経路です。リポジトリにbackup_to(path)メソッドとして追加しておきます。
まとめ #
- 設定は QSettings が標準です。プラットフォームごとの保存場所を自動処理し、型の罠(文字列で返る bool)はラッパークラスで一度に防ぎます。
- 設定は保存と同時に適用します。再起動しないと反映されない設定は未完成です。
- テーマは QSS の差し替えが骨組みで、system モードは colorScheme の検知と変更シグナルで作ります。自分で描くウィジェットは色をテーマ定数から読むように直す必要があります。
- CSV エクスポートと backup() ベースの DB バックアップで、ユーザーデータの移動の自由を保証します。
- 機能はこれで完成です。次回はこのアプリが完成状態であり続けるための装置、pytest-qt テストです。