【完全ガイド】PySide6で実装するタスク管理アプリ開発

【完全ガイド】PySide6で実装するタスク管理アプリ開発

デスクトップアプリケーション開発において、PySide6は最も強力で実用的な選択肢の一つです。
この記事では、PySide6を使用してシンプルながら実用的なタスク管理アプリケーションを作成する方法を、完全初心者でも理解できるよう丁寧に解説します。

完成版では、タスクの追加・削除・完了状態の管理、そしてデータの自動保存機能を持つ本格的なアプリケーションを作成します。

PySide6公式ドキュメント

Qt for Pythonの公式ドキュメント。PySide6の詳細な機能とAPIリファレンス

doc.qt.io

PySide6とは?

なぜPySide6を選ぶのか?

PySide6は、Qt 6フレームワークのPythonバインディングです。Nokia(現Qt Company)が開発する20年以上の歴史を持つ成熟したフレームワークで、Windows、macOS、Linuxで同じコードが動作するクロスプラットフォーム対応が特徴です。

Python GUI開発において、TkinterやKivyなど複数の選択肢がありますが、PySide6が特に優れているのはネイティブな外観です。各OS標準のデザインガイドラインに従った自然な見た目で、ユーザーに違和感を与えません。また、C++で実装されたQtの性能をそのまま活用できるため、Pythonコードでありながら高速なアプリケーションを作成できます。

開発効率の面でも大きなメリットがあります。同じ機能を実装する場合、Tkinterに比べて、PySide6では半分程度で済むことが多く、開発時間を大幅に短縮できます。さらに、100種類以上の豊富なウィジェットが用意されているため、ボタンやリスト、テーブルなどの高機能なUI要素を簡単に組み込めます。

ライセンス面でも重要な優位性があります。PySide6はLGPL v3ライセンスのため商用利用に制限がありませんが、類似のPyQt6はGPLライセンスのため商用プロジェクトでは制約があります。このため、将来的にアプリケーションを商用展開する可能性がある場合、PySide6が安全な選択となります。

【ライセンスの違いについて】

LGPL v3ライセンス(PySide6)
商用アプリケーションでも無料で使用可能で作成したアプリケーションを自由に販売・配布できます。PySide6自体を改変した場合のみソースコード公開が必要です。

GPLライセンス(PyQt6)
商用利用の場合は有料ライセンスの購入が必要。または作成したアプリケーション全体をオープンソース化する必要があります。個人利用・学習目的は無料で使用可能です。

VLC Media Player、VirtualBox、Autodesk Mayaなど、世界的に有名なソフトウェアがQtフレームワークを採用していることも、その信頼性と実績を物語っています。

PySide6選択のメリット

  • ネイティブな外観でプロフェッショナルな見た目
  • 高いパフォーマンスと安定性
  • 短いコードで効率的な開発が可能
  • 商用利用可能なライセンス(LGPL v3)
  • 豊富なウィジェットと充実したドキュメント

 

環境準備・インストール

まず、PySide6をインストールします。Python 3.7以上が必要です。

# PySide6のインストール
pip install PySide6

# インストール確認
python -c "import PySide6; print('PySide6 installed successfully!')"

開発環境の推奨設定

  • IDE:VS Code/Cursor + Python拡張機能
  • Python:3.9以上(型ヒント対応のため)
  • 仮想環境:venvまたはcondaでの環境分離

 

【開発ステップ①】メインウィンドウ作成

最初に、アプリケーションの基盤となるメインウィンドウを作成します。

メインウィンドウ

# task_manager.py
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget

class TaskManagerApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.init_ui()
    
    def init_ui(self):
        """UIの初期設定"""
        # ウィンドウの基本設定
        self.setWindowTitle('タスク管理アプリ')
        self.setGeometry(100, 100, 400, 500) # x, y, width, height
        
        # 中央ウィジェットの設定
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # レイアウト設定
        self.main_layout = QVBoxLayout()
        central_widget.setLayout(self.main_layout)

def main():
    """アプリケーションのメイン実行関数"""
    app = QApplication(sys.argv)
    
    # アプリケーションウィンドウを作成
    task_app = TaskManagerApp()
    task_app.show()
    
    # イベントループの開始
    sys.exit(app.exec())

# アプリケーションの実行
if __name__ == "__main__":
    main()

コードの解説

  • QMainWindow:メインウィンドウの基底クラス
  • QWidget():すべてのUI要素の基底クラス(ボタン、ラベルなどの親)
  • setCentralWidget():メインウィンドウの中央にウィジェットを配置
  • QVBoxLayout():縦方向にウィジェットを配置するレイアウト
  • app.exec():イベントループの開始(アプリケーション実行)

【PySide6の基本概念】

ウィジェット(Widget)
ユーザーインターフェースの構成要素です。
ボタン、テキストボックス、ラベルなど、画面に表示される全ての要素がウィジェットです。

レイアウト(Layout)
ウィジェットをどのように配置するかを決める仕組みです。
PySide6では以下の主要なレイアウトがあります。

  • QVBoxLayout:ウィジェットを縦方向に配置
  • QHBoxLayout:ウィジェットを横方向に配置
  • QGridLayout:ウィジェットを格子状に配置

この時点でアプリケーションを実行してみましょう。

python task_manager.py

空の400×500ピクセルのウィンドウが表示されます。これがアプリケーションの基盤となるメインウィンドウです。

メインウィンドウの表示

【開発ステップ②】タスクUI要素の追加

開発ステップ①の基本ウィンドウにタスク管理に必要なUI要素を追加します。

タスク入力エリアの追加

開発ステップ①のコードに以下のメソッドを追加します。

def create_input_section(self):
    """タスク入力セクションの作成"""
    # ラベル
    input_label = QLabel("新しいタスク:")
    self.main_layout.addWidget(input_label)
    
    # 入力欄とボタンの横並びレイアウト
    input_layout = QHBoxLayout()
    
    # テキスト入力欄
    self.task_input = QLineEdit()
    self.task_input.setPlaceholderText("タスクを入力してください")
    input_layout.addWidget(self.task_input)
    
    # 追加ボタン
    self.add_button = QPushButton("追加")
    self.add_button.setFixedWidth(80)
    input_layout.addWidget(self.add_button)
    
    # 横並びレイアウトをメインレイアウトに追加
    input_widget = QWidget()
    input_widget.setLayout(input_layout)
    self.main_layout.addWidget(input_widget)

 

タスクリストエリアの追加

def create_task_list_section(self):
    """タスクリストセクションの作成"""
    # ラベル
    list_label = QLabel("タスクリスト:")
    self.main_layout.addWidget(list_label)
    
    # タスクリスト
    self.task_list = QListWidget()
    self.task_list.setAlternatingRowColors(True)  # 行の色を交互に変更
    self.task_list.setSelectionMode(
        QAbstractItemView.SelectionMode.ExtendedSelection
    )
    self.main_layout.addWidget(self.task_list)
    
    # 削除ボタン
    self.delete_button = QPushButton("選択したタスクを削除")
    self.delete_button.setEnabled(False)  # 初期状態では無効
    self.main_layout.addWidget(self.delete_button)

そして、init_ui()メソッドの最後に以下を追加します。

def init_ui(self):
    # ...既存のコード...
    
    # UI要素の作成を追加
    self.create_input_section()
    self.create_task_list_section()

コードの解説

  • create_input_section():タスク入力エリアを作成するメソッド
  • create_task_list_section():タスクリスト表示エリアを作成するメソッド
  • QHBoxLayout():入力欄とボタンを横並びに配置するレイアウト
  • QLabel():テキストラベルを表示するウィジェット
  • QLineEdit():テキスト入力欄を提供するウィジェット
  • QPushButton():クリック可能なボタンウィジェット
  • QListWidget():リスト形式でアイテムを表示するウィジェット
  • setAlternatingRowColors():リストの行の色を交互に変更する設定
  • setSelectionMode(ExtendedSelection):複数選択を可能にする設定

【ウィジェットとレイアウトの関係】

addWidget()の役割
作成したウィジェット(ボタン、テキスト入力欄など)をaddWidget()メソッドでレイアウトに追加することで、画面上に表示されます。

レイアウトの階層構造

  1. QVBoxLayout(メインレイアウト)に各セクションを縦に配置
  2. QHBoxLayout(入力セクション)でテキスト入力欄とボタンを横に配置
  3. 各ウィジェットをそれぞれのレイアウトに追加

(実際の配置例)
input_layout.addWidget(self.task_input) → テキスト入力欄を横並びレイアウトに追加
input_layout.addWidget(self.add_button) → ボタンを同じ横並びレイアウトに追加
self.main_layout.addWidget(input_widget) → 横並びレイアウト全体をメインレイアウトに追加

この段階で、視覚的に完成されたUIが表示されます。

UIの表示

【開発ステップ③】基本機能の実装

UIができたので、実際にタスクを管理する機能を実装します。このステップでは、イベント処理とデータ管理の基本を学びます。

まず、開発ステップ②のUIコードにイベント処理を追加します。

イベント接続メソッドの追加

既存のUIコードに以下のメソッドを追加します。

def connect_events(self):
    """シグナルをスロットに接続"""
    # 追加ボタンクリック時
    self.add_button.clicked.connect(self.add_task)
    
    # Enterキー押下時(入力欄)
    self.task_input.returnPressed.connect(self.add_task)
    
    # リストアイテム選択時
    self.task_list.itemSelectionChanged.connect(self.on_selection_changed)
    
    # 削除ボタンクリック時
    self.delete_button.clicked.connect(self.delete_task)

そして、init_ui()メソッドの最後に以下を追加します。

def init_ui(self):
    # ...既存のコード...
    
    # イベント接続を追加
    self.connect_events()

【シグナルとスロットの仕組み】

PySide6では、ユーザーの操作(クリック、キー入力など)がシグナルとして発信され、それをスロット(処理関数)で受け取るイベント駆動の仕組みを採用しています。

connect()の役割

  • clicked.connect():ボタンがクリックされた時の処理を接続
  • returnPressed.connect():Enterキーが押された時の処理を接続
  • itemSelectionChanged.connect():リスト選択が変更された時の処理を接続

実行タイミング
接続したスロット関数は、対応するシグナルが発信された瞬間に自動的に実行されます。

 

タスク追加機能の実装

次に、実際にタスクを追加する処理を実装します。

def add_task(self):
    """タスクを追加する"""
    # 入力欄からテキストを取得
    task_text = self.task_input.text().strip()
    
    # 空文字チェック
    if not task_text:
        return
    
    # リストに追加
    self.task_list.addItem(task_text)
    
    # 入力欄をクリア
    self.task_input.clear()
    
    # 入力欄にフォーカスを戻す
    self.task_input.setFocus()
    
    print(f"タスク追加: {task_text}")

コードの解説

  • text().strip():入力欄のテキストを取得し、前後の空白を削除
  • if not task_text:空文字の場合は処理を終了
  • addItem():QListWidgetにアイテムを追加
  • clear():入力欄の内容をクリア
  • setFocus():入力欄にカーソルを戻す

 

タスクの選択状態管理と削除機能の実装

続いて、タスクの選択状態管理と削除機能を実装します。

def on_selection_changed(self):
    """リストの選択状態が変更された時"""
    # 選択されているアイテムがあるかチェック
    has_selection = len(self.task_list.selectedItems()) > 0
    
    # 削除ボタンの有効/無効を切り替え
    self.delete_button.setEnabled(has_selection)

def delete_task(self):
    """選択されたタスクを削除する"""
    # 選択されているアイテムを取得
    selected_items = self.task_list.selectedItems()
    
    if not selected_items:
        return
    
    # 選択されたアイテムを削除
    for item in selected_items:
        row = self.task_list.row(item)
        self.task_list.takeItem(row)
        print(f"タスク削除: {item.text()}")

コードの解説

  • selectedItems():現在選択されているアイテムの一覧を取得
  • setEnabled():ボタンの有効/無効状態を切り替え
  • row():アイテムの行番号を取得
  • takeItem():指定した行のアイテムを削除

これで基本的なタスク管理機能が動作するようになりました。

基本機能の実装

実装された機能

  • テキスト入力欄にタスクを入力
  • 「追加」ボタンまたはEnterキーでタスクを追加
  • リストでタスクを選択
  • 「選択したタスクを削除」ボタンでタスクを削除
  • タスクが選択されていない時は削除ボタンが無効化

 

【開発ステップ④】チェックボックス機能の追加

基本的なタスク管理機能に加えて、タスクの完了状態を視覚的に管理できるチェックボックス機能を実装します。

カスタムウィジェットの作成

開発ステップ③のadd_task()メソッドを以下のように変更します。

def add_task(self):
    """タスクを追加する"""
    # 入力欄からテキストを取得
    task_text = self.task_input.text().strip()
    
    # 空文字チェック
    if not task_text:
        return
    
    # カスタムウィジェット(チェックボックス付き)を作成
    self.create_task_item(task_text)
    
    # 入力欄をクリア
    self.task_input.clear()
    
    # 入力欄にフォーカスを戻す
    self.task_input.setFocus()
    
    print(f"タスク追加: {task_text}")

 

チェックボックス付きタスクアイテムの実装

新しく以下のメソッドを追加します。

def create_task_item(self, task_text):
    """チェックボックス付きのタスクアイテムを作成"""
    # リストアイテムを作成
    list_item = QListWidgetItem()
    
    # チェックボックス付きウィジェットを作成
    task_widget = QWidget()
    task_layout = QHBoxLayout()
    task_layout.setContentsMargins(5, 2, 5, 2)  # 余白を設定
    
    # チェックボックス
    checkbox = QCheckBox()
    checkbox.stateChanged.connect(lambda state, item=list_item: self.on_task_completed(state, item))
    task_layout.addWidget(checkbox)
    
    # タスクテキストラベル
    task_label = QLabel(task_text)
    task_layout.addWidget(task_label)
    
    # 右側に余白を追加
    task_layout.addStretch()
    
    task_widget.setLayout(task_layout)
    
    # リストアイテムのサイズを設定
    list_item.setSizeHint(task_widget.sizeHint())
    
    # リストに追加
    self.task_list.addItem(list_item)
    self.task_list.setItemWidget(list_item, task_widget)
    
    # タスクテキストをアイテムに保存(削除時に使用)
    list_item.setData(Qt.ItemDataRole.UserRole, task_text)

コードの解説

  • QListWidgetItem():リスト内の個別アイテムを作成
  • QCheckBox():チェックボックスウィジェットを作成
  • stateChanged.connect():チェックボックスの状態変化を監視
  • setContentsMargins():ウィジェットの内側余白を設定
  • addStretch():レイアウトに伸縮可能なスペースを追加
  • setSizeHint():アイテムの推奨サイズを設定(カスタムウィジェットのサイズに合わせる)
  • addItem():QListWidgetにリストアイテムを追加
  • setItemWidget():リストアイテムにカスタムウィジェットを関連付け
  • setData(Qt.ItemDataRole.UserRole, data):アイテムに任意のデータを保存(削除時の識別に使用)

【lambdaを使ったイベント接続の解説】

lambda関数の必要性
チェックボックスのstateChangedシグナルは、状態(state)しか渡しませんが、どのタスクアイテムが変更されたかも知る必要があります。

lambda state, item=list_item: self.on_task_completed(state, item)

  • state:チェックボックスから自動的に渡される状態値
  • item=list_item:現在のアイテムをデフォルト引数としてキャプチャ

【リストアイテムとカスタムウィジェットの関係】

リストにカスタムウィジェットを表示するには、以下のように2つのステップが必要です。

addItem(list_item)

リストに「スペース」を確保します。この時点では何も表示されていない空白行が追加されます。

setItemWidget(list_item, task_widget)

確保したスペースにチェックボックス付きウィジェットを配置することで、初めてチェックボックスとテキストが表示されます。

つまり、addItem(list_item)で「スペースを作り」、setItemWidget(list_item, task_widget)で「内容を表示する」という分業になっています。

 

(補足)データの管理とQt.ItemDataRole.UserRole

このタスク管理アプリでは、各タスクが2つの要素(表示用のウィジェット(チェックボックスとテキスト)とデータ)で構成されています。

list_item.setData(Qt.ItemDataRole.UserRole, task_text)は、リストアイテムにタスクのテキストデータを保存するメソッドです。なぜこれが必要かというと、ユーザーがタスクを削除したい時に「どのタスクを削除するか」を特定する必要があるためです。

削除時のデータ取得の流れ

  1. ユーザーがリストでタスクを選択
  2. 「削除」ボタンをクリック
  3. プログラムは選択されたアイテムから、保存されたタスクテキストを取得
  4. そのテキストを使ってコンソールに「○○を削除しました」と出力

Qt.ItemDataRole.UserRoleは、Qtフレームワークが内部で使用する領域とは別の開発者が自由に使える保存領域です。これにより、表示されている内容とは独立してデータを安全に管理できます。

つまり、見た目(ウィジェット)とデータ(テキスト)を分離することで、削除時やその他の操作時に正確な情報を取得できる仕組みになっています。

 

完了状態の処理と見た目の更新

チェックボックスの状態変化を処理するメソッドを追加します。

def on_task_completed(self, state, item):
    """タスクの完了状態が変更された時"""
    task_text = item.data(Qt.ItemDataRole.UserRole)
    
    # 完了状態を判定
    completed = state == Qt.CheckState.Checked.value

    if completed:
        print(f"タスク完了: {task_text}")

    else:
        print(f"タスク未完了: {task_text}")

    # 見た目を更新
    self.update_task_appearance(item, completed=completed)

def update_task_appearance(self, item, completed):
    """タスクの見た目を更新(完了/未完了)"""
    widget = self.task_list.itemWidget(item)
    if widget:
        # ラベルを取得(レイアウトの2番目の要素)
        label = widget.layout().itemAt(1).widget()
        if completed:
            # 完了時: 取り消し線 + グレーアウト
            label.setStyleSheet("text-decoration: line-through; color: gray;")
        else:
            # 未完了時: 通常表示
            label.setStyleSheet("")

コードの解説

  • Qt.CheckState.Checked.value:チェックボックスがチェックされた状態の値
  • data(Qt.ItemDataRole.UserRole):保存されたユーザーデータ(タスクテキスト)を取得
  • itemWidget():リストアイテムに設定されたカスタムウィジェットを取得
  • itemAt(1):レイアウト内の2番目のウィジェット(ラベル)を取得
  • setStyleSheet():ウィジェットのスタイル(CSS風)を設定して見た目を変更

【チェックボックスの状態判定】

completed = state == Qt.CheckState.Checked.value

この実装は、チェックボックスの現在の状態を判定しています。

Qt.CheckState.Checked.valueは、チェックボックスがチェックされた状態を表す数値(通常は2)です。チェックボックスから渡されるstateパラメータがこの値と等しい場合、タスクが完了状態であることを意味します。

逆に、チェックが外された場合はQt.CheckState.Unchecked.value(通常は0)が渡されるため、比較結果はFalseになり、未完了状態として処理されます。

 

削除機能の更新

既存のdelete_task()メソッドも、新しいデータ保存方法に対応するよう更新が必要です:

def delete_task(self):
    """選択されたタスクを削除する"""
    selected_items = self.task_list.selectedItems()
    
    if not selected_items:
        return
    
    # 選択されたアイテムを削除
    for item in selected_items:
        task_text = item.data(Qt.ItemDataRole.UserRole)  # 保存されたタスクテキストを取得
        row = self.task_list.row(item)
        self.task_list.takeItem(row)
        print(f"タスクを削除しました: {task_text}")

この段階で実行すると、以下の機能が利用できます。

チェックボックス機能の実装

新しく追加された機能

  • 各タスクにチェックボックスが表示
  • チェックボックスをクリックして完了/未完了を切り替え
  • 完了したタスクは取り消し線とグレーアウトで表示
  • チェックボックスの状態に応じた視覚的フィードバック

 

【開発ステップ⑤】データの永続化

これまでのタスク管理アプリは、アプリを閉じるとすべてのタスクが消えてしまいます。最後のステップとして、タスクをJSONファイルに保存・読み込みする機能を実装し、データを永続化させます。

データファイルの設定

__init__メソッドにデータファイルの設定を追加します。

def __init__(self):
    super().__init__()
    self.data_file = "tasks.json"  # データファイル名
    self.init_ui()
    self.load_tasks()  # アプリ起動時にタスクを読み込む

変更点の解説

  • self.data_file:保存先のJSONファイル名を設定
  • self.load_tasks():アプリ起動時に保存されたタスクを読み込む

 

データの保存機能

タスクをJSONファイルに保存するsave_tasks()メソッドを追加します。

def save_tasks(self):
    """タスクをJSONファイルに保存"""
    tasks = []
    
    # 現在のタスクリストからデータを収集
    for i in range(self.task_list.count()):
        item = self.task_list.item(i)
        task_data = item.data(Qt.ItemDataRole.UserRole)
        if task_data:
            tasks.append(task_data)
    
    try:
        # JSONファイルに保存
        with open(self.data_file, "w", encoding="utf-8") as f:
            json.dump(tasks, f, ensure_ascii=False, indent=2)
            print(f"タスク保存:{len(tasks)}件")
            
    except Exception as e:
        print(f"保存エラー:{e}")

【JSONファイル保存の仕組み】

データ収集の流れ

リスト内の全てのアイテムから、Qt.ItemDataRole.UserRoleに保存されているタスクデータ(テキストと完了状態)を取得し、Pythonのリスト形式にまとめます。

(ファイル保存のオプション)
ensure_ascii=Falseで日本語を正しく保存し、indent=2で読みやすい形式で出力します。

 

データの読み込み機能

アプリ起動時に保存されたタスクを読み込むload_tasks()メソッドを追加します。

def load_tasks(self):
    """JSONファイルからデータを読み込む"""
    if not os.path.exists(self.data_file):
        print("データファイルが存在しません")
        return
    
    try:
        with open(self.data_file, "r", encoding="utf-8") as f:
            tasks = json.load(f)
        
        for task_data in tasks:
            text = task_data.get("text", "")
            completed = task_data.get("completed", False)
            if text:  # 空でないタスクを追加
                self.create_task_item(text, completed)
        
        print(f"タスクを読み込み:{len(tasks)}件")
        
    except Exception as e:
        print(f"読み込みエラー:{e}")

【データ読み込み時の安全対策】

os.path.exists()でファイルが存在するかチェックしています。初回起動時はファイルが存在しないため、この処理がないとエラーが発生します。

task_data.get()でデータを安全に取得し、キーが存在しない場合はデフォルト値を使用します。

 

既存メソッドの修正

データを永続化するため、タスクの追加・削除・状態変更時に保存処理を追加します。

①タスク追加時の保存

add_task()メソッドに保存処理を追加

def add_task(self):
    # ... 既存の処理 ...
    
    # タスクを保存
    self.save_tasks()
    
    print(f"タスク追加: {task_text}")

②タスク削除時の保存

delete_task()メソッドに保存処理を追加

def delete_task(self):
    # ... 既存の削除処理 ...
    
    self.save_tasks()

③チェックボックス状態変更時の保存

on_task_completed()メソッドに保存処理を追加

def on_task_completed(self, state, item):
    # ... 既存の処理 ...
    
    # データを更新して保存
    task_data["completed"] = completed
    item.setData(Qt.ItemDataRole.UserRole, task_data)
    self.save_tasks()

 

データ構造の変更

開発ステップ④では単純な文字列でしたが、完了状態も保存するため辞書形式に変更します。

# 変更前(開発ステップ④)
list_item.setData(Qt.ItemDataRole.UserRole, task_text)

# 変更後(開発ステップ⑤)
list_item.setData(Qt.ItemDataRole.UserRole, {"text": task_text, "completed": completed})

 

create_task_item()メソッドの更新

読み込み時に完了状態を反映できるよう、completedパラメータを追加

def create_task_item(self, task_text, completed=False):
    # ... 既存のウィジェット作成処理 ...
    
    # チェックボックス
    checkbox = QCheckBox()
    checkbox.setChecked(completed)  # 読み込み時の状態を設定
    
    # ... 残りの処理 ...
    
    # タスクテキストと完了状態をアイテムに保存
    list_item.setData(Qt.ItemDataRole.UserRole, {"text": task_text, "completed": completed})
    
    # 見た目を状態に合わせて更新
    self.update_task_appearance(list_item, completed=completed)

この段階で実行すると、以下の機能が利用できます。

新しく追加された機能

  • タスクの自動保存(追加・削除・完了状態変更時)
  • アプリ起動時の自動読み込み
  • JSONファイルによるデータ永続化
  • エラーハンドリング(ファイルが存在しない場合など)

 

完成したコード

これまでの全ステップを統合した最終的なコードです。

import json
import os
import sys

from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
    QAbstractItemView,
    QApplication,
    QCheckBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QListWidget,
    QListWidgetItem,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class TaskManagerApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.data_file = "tasks.json"  # データファイル名
        self.init_ui()
        self.load_tasks()  # アプリ起動時にタスクを読み込む

    def init_ui(self):
        # 基本設定
        self.setWindowTitle("タスク管理アプリ")
        self.setGeometry(100, 100, 400, 500)

        # 中央ウィジェット設定
        central_widget = QWidget()
        self.setCentralWidget(central_widget)

        # メインレイアウト
        self.main_layout = QVBoxLayout()
        central_widget.setLayout(self.main_layout)

        # UI要素の作成
        self.create_input_section()
        self.create_task_list_section()

        # イベント接続
        self.connect_events()

    def create_input_section(self):
        """入力セクション"""
        # ラベル
        input_label = QLabel("新しいタスク")
        self.main_layout.addWidget(input_label)

        # 入力値とボタンの横並びレイアウト
        input_layout = QHBoxLayout()

        # テキスト入力値
        self.task_input = QLineEdit()
        self.task_input.setPlaceholderText("タスクを入力してください")
        input_layout.addWidget(self.task_input)

        # 追加ボタン
        self.add_button = QPushButton("追加")
        self.add_button.setFixedWidth(80)
        input_layout.addWidget(self.add_button)

        # 横並びのレイアウトをメインレイアウトに追加
        input_widget = QWidget()
        input_widget.setLayout(input_layout)
        self.main_layout.addWidget(input_widget)

    def create_task_list_section(self):
        """リストセクション"""
        # ラベル
        list_label = QLabel("タスクリスト")
        self.main_layout.addWidget(list_label)

        # タスクリスト
        self.task_list = QListWidget()
        self.task_list.setAlternatingRowColors(True)
        self.task_list.setSelectionMode(
            QAbstractItemView.SelectionMode.ExtendedSelection
        )
        self.main_layout.addWidget(self.task_list)

        # 削除ボタン
        self.delete_button = QPushButton("削除(複数選択可)")
        self.delete_button.setEnabled(False)
        self.main_layout.addWidget(self.delete_button)

    def connect_events(self):
        """イベントを関数に接続"""
        # 追加ボタンをクリック時
        self.add_button.clicked.connect(self.add_task)

        # Enterキーを押下時(入力欄)
        self.task_input.returnPressed.connect(self.add_task)

        # リストのタスクを選択時
        self.task_list.itemSelectionChanged.connect(self.on_selection_changed)

        # 削除ボタンをクリック時
        self.delete_button.clicked.connect(self.delete_task)

    def add_task(self):
        """タスクを追加"""
        # 入力値からテキストを取得
        task_text = self.task_input.text().strip()

        # 空文字をチェック
        if not task_text:
            return

        # リストに追加
        self.create_task_item(task_text)

        # 入力値をクリア
        self.task_input.clear()

        # 入力欄にフォーカスを戻す
        self.task_input.setFocus()

        # タスクを保存
        self.save_tasks()

        print(f"タスク追加: {task_text}")

    def create_task_item(self, task_text, completed=False):
        """チェックボックス付きのタスクアイテムを作成"""
        # リストアイテムを作成
        list_item = QListWidgetItem()

        # チェックボックス付きウィジェットを作成
        task_widget = QWidget()
        task_layout = QHBoxLayout()
        task_layout.setContentsMargins(5, 2, 5, 2)

        # チェックボックス
        checkbox = QCheckBox()
        checkbox.setChecked(completed)  # 読み込み時の状態を設定
        checkbox.stateChanged.connect(
            lambda state, item=list_item: self.on_task_completed(state, item)
        )
        task_layout.addWidget(checkbox)

        # タスクテキストラベル
        task_label = QLabel(task_text)
        task_layout.addWidget(task_label)

        # 右側に余白を追加
        task_layout.addStretch()

        # ウィジェットにレイアウトを設定
        task_widget.setLayout(task_layout)

        # リストアイテムのサイズ設定
        list_item.setSizeHint(task_widget.sizeHint())

        # リストに追加
        self.task_list.addItem(list_item)
        self.task_list.setItemWidget(list_item, task_widget)

        # タスクテキストと完了状態をアイテムに保存
        list_item.setData(Qt.ItemDataRole.UserRole, {"text": task_text, "completed": completed})

        # 見た目を状態に合わせて更新
        self.update_task_appearance(list_item, completed=completed)

    def on_task_completed(self, state, item):
        """タスクの完了状態が変更された時"""
        task_data = item.data(Qt.ItemDataRole.UserRole)
        task_text = task_data["text"]

        # 完了状態を判定
        completed = state == Qt.CheckState.Checked.value

        if completed:
            print(f"タスク完了: {task_text}")
        else:
            print(f"タスク未完了: {task_text}")

        # 見た目を更新
        self.update_task_appearance(item, completed=completed)

        # データを更新して保存
        task_data["completed"] = completed
        item.setData(Qt.ItemDataRole.UserRole, task_data)
        self.save_tasks()

    def update_task_appearance(self, item, completed):
        """タスクの見た目を更新(完了/未完了)"""
        widget = self.task_list.itemWidget(item)
        if widget:
            # ラベルを取得(レイアウトの2番目の要素)
            label = widget.layout().itemAt(1).widget()
            if completed:
                # 完了時: 取り消し線 + グレーアウト
                label.setStyleSheet("text-decoration: line-through; color: gray;")
            else:
                # 未完了時: 通常表示
                label.setStyleSheet("")

    def on_selection_changed(self):
        """リストの選択状態が変更された時"""
        # タスクがあるかチェック
        has_selection = len(self.task_list.selectedItems()) > 0

        # 削除ボタンの切り替え
        self.delete_button.setEnabled(has_selection)

    def delete_task(self):
        """選択したタスクを削除する"""
        # 選択されているタスクを取得する
        selected_task = self.task_list.selectedItems()

        if not selected_task:
            return

        # 選択されたタスクを削除する
        for task in selected_task:
            task_data = task.data(Qt.ItemDataRole.UserRole)
            task_text = task_data["text"]
            row = self.task_list.row(task)
            self.task_list.takeItem(row)
            print(f"タスク削除:{task_text}")

        self.save_tasks()

    def save_tasks(self):
        """タスクをJSONファイルに保存"""
        tasks = []

        # 現在のタスクリストからデータを収集
        for i in range(self.task_list.count()):
            item = self.task_list.item(i)
            task_data = item.data(Qt.ItemDataRole.UserRole)
            if task_data:
                tasks.append(task_data)

        try:
            # JSONファイルに保存
            with open(self.data_file, "w", encoding="utf-8") as f:
                json.dump(tasks, f, ensure_ascii=False, indent=2)
                print(f"タスク保存:{len(tasks)}件")

        except Exception as e:
            print(f"保存エラー:{e}")

    def load_tasks(self):
        """JSONファイルからデータを読み込む"""
        if not os.path.exists(self.data_file):
            print("データファイルが存在しません")
            return

        try:
            with open(self.data_file, "r", encoding="utf-8") as f:
                tasks = json.load(f)

            for task_data in tasks:
                text = task_data.get("text", "")
                completed = task_data.get("completed", False)
                if text:  # 空でないタスクを追加
                    self.create_task_item(text, completed)

            print(f"タスクを読み込み:{len(tasks)}件")

        except Exception as e:
            print(f"読み込みエラー:{e}")


def main():
    app = QApplication(sys.argv)

    # アプリケーションウィンドウを作成
    window = TaskManagerApp()
    window.show()

    # イベントループの開始
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

まとめ

基本的なタスク管理機能とチェックボックス機能、そしてデータの永続化が完成しました!この記事で学んだ技術を応用すれば、さらに高度な機能を追加できます:

  • タスクの優先度設定
  • 期日管理機能
  • 検索・フィルタリング機能
  • 異なるファイル形式での保存(CSV、XMLなど)

より高度なPySide6アプリケーションに興味がある方は、以下のポートフォリオをご覧ください。

RX Scanner - 処方箋OCRアプリケーション

Tesseract OCRを活用した医療機関向け処方箋画像認識・薬剤データ管理デスクトップアプリケーション。PySide6、OpenCV、SQLiteを使用した実践的なプロジェクト


さらに詳しく学ぶ

PySide6公式ドキュメント - Qt for Python

PySide6の公式ドキュメント。より高度なウィジェットと機能を学べる包括的なリファレンス

docs.python.org
拡大画像