
PythonでChatGPTと一緒に住所録を作ろう:フィルター編
ほぼ機能がそろいましたね。
細かい所は、出来ていないところがあるのですが
新規、編集、削除、保存、読み込み(ボタンに登録されてない)
は、出来るようになりました。
あと、フィルターが出来れば大まかなところが完了です。
前回の記事(ここまでのソースはこちらをご覧ください)
1.実現方法
早速、ChatGPTにプロンプトを送ります。
先日、ChatGPT Plus では ChatGPT o3-mini-high が
使えるようになったのですがコーディング能力が
かなり向上してますね。
こちらは、無料版で頑張ることにします。
改めてプロンプトを送ります。
「(今までのコードを全て貼り付け) 実行ボタンを押したときにフィルターを掛けて一覧表に表示したい。どのような方針で実現できるだろうか?」
ChatGPT#####
フィルター機能を実装するには、以下の方針が考えられます。
方針
フィルター入力値を取得する
→ filter_button がクリックされたときに、filter_input のテキストを取得する。テーブルのデータを取得する
→ 現在の QTableWidget にある全データをリスト化する。フィルターを適用する
→ 各行のデータを filter_input のキーワードで検索し、一致する行のみを表示する。テーブルを更新する
→ フィルターに一致しない行を非表示(またはテーブルをクリアして一致する行のみ再表示)する。
実装方法
大きく分けて 「非表示にする」 方法と 「一致するデータのみを再描画」 する方法がある。
方法1: 一致しない行を非表示にする(簡単)
QTableWidget の setRowHidden() を使って、一致しない行を非表示にする。
既存のデータはそのままで、フィルターを切るとすぐ元の状態に戻せる。
実装コード
def apply_filter(self):
"""フィルターを適用して一覧を絞り込む"""
filter_text = self.filter_input.text().strip()
for row in range(self.table.rowCount()):
row_matches = False
for col in range(self.table.columnCount()):
item = self.table.item(row, col)
if item and filter_text.lower() in item.text().lower():
row_matches = True
break
# 一致しない行を非表示
self.table.setRowHidden(row, not row_matches)
この方法のメリット
元のデータは維持されるので、フィルターを解除すると元の表示に戻る。
setRowHidden() を使うだけなので処理が軽い。
デメリット
QTableWidget に大量のデータがある場合、動作が遅くなる可能性がある。
方法2: フィルターに一致するデータのみを再描画
一度 QTableWidget をクリアして、一致するデータのみを再描画する。
self.load_data_from_json() からデータをリスト化し、一致するデータだけ QTableWidget に再描画する。
実装コード
def apply_filter(self):
"""フィルターを適用し、一致するデータのみを再表示"""
filter_text = self.filter_input.text().strip().lower()
# JSONからデータを読み込み
try:
with open(DATA_FILE, "r", encoding="utf-8") as file:
all_data = json.load(file)
except FileNotFoundError:
print("ファイルが見つかりません")
return
except json.JSONDecodeError:
print("JSONデータが壊れています")
return
# 一致するデータのみを抽出
filtered_data = [
entry for entry in all_data
if any(filter_text in str(value).lower() for value in entry.values())
]
# テーブルをクリアして再描画
self.table.setRowCount(0)
self.table.setRowCount(len(filtered_data))
for row, entry in enumerate(filtered_data):
for col, key in enumerate(["姓", "姓フリガナ", "名", "名フリガナ", "郵便番号", "住所", "電話番号", "備考"]):
self.table.setItem(row, col, QTableWidgetItem(entry.get(key, "")))
print(f"{len(filtered_data)} 件が一致しました")
この方法のメリット
QTableWidget のリソースを削減できる。
大量のデータを扱っても setRowHidden() を使うより高速。
デメリット
一度データをクリアするので、元に戻すには再度 load_data_from_json() を呼ぶ必要がある。
どちらの方法を採用すべきか?
データ量が少ない(100件未満) → 方法1(setRowHidden) でシンプルに実装
データ量が多い(100件以上) → 方法2(再描画) で高速化
ボタンにイベントを接続
どちらの方法を選んでも、filter_button.clicked.connect(self.apply_filter) を setup_ui() に追加すれば、ボタンを押したときにフィルターが適用される。
self.filter_button.clicked.connect(self.apply_filter)
まとめ
フィルター機能を実装する方法は2つある:
setRowHidden() を使って非表示にする(簡単・データ量が少ない場合向け)
QTableWidget をクリアし、フィルターに一致するデータのみ再描画(データ量が多い場合向け)
どちらを採用するか、データ量に応じて選ぶとよい。
簡単に実装したい場合は方法1、高速化を考慮するなら方法2が適している。
ご#####
ChatGPTは、
1.一致しない行を非表示 : 少量のデータ、簡単
2.一致するデータを再表示 : 大量のデータ、高速
という事なので簡単なほうの1を採用することにします。
2.組み込み
ChatGPTが作った apply_filter() メソッド(1.非表示版)を
クラス AddressBookApp の最後尾に追加しました。
そして、他のボタンの動作を設定した部分の最後尾に
self.filter_button.clicked.connect(self.apply_filter)
を追加します。
これで、実行してみましょう。
うまく動作しています。
コードを確認しましょう。
def apply_filter(self):
"""フィルターを適用して一覧を絞り込む"""
filter_text = self.filter_input.text().strip()
for row in range(self.table.rowCount()): # 行数分繰り返す
row_matches = False
for col in range(self.table.columnCount()): # 列数分繰り返す
item = self.table.item(row, col) # セルを一つづつ取り出す
if item and filter_text.lower() in item.text().lower():
row_matches = True
break
# 一致しない行を非表示
self.table.setRowHidden(row, not row_matches)
filter_text = self.filter_input.text().strip()
フィルターの入力されたテキストからスペースを取り除きます。
if item and filter_text.lower() in item.text().lower():
row_matches = True
break
if ( item ) and ( filter_text.lower() in item.text().lower() ):
分かりやすく()を入れてみました。
item = True であり
filter_text.lower() が item.text().lower() に含まれているときに True
であった場合 判定フラグ row_matches = True となります。
lower() は、すべてを小文字化して比較するために使っています。

3.もう一つの方法
ChatGPTが提案してくれたもう一つの案、
「一致するデータを再表示」の方について解説します。
実は、こちらも簡単に試してみました。
結果、このやり方はいろいろと面倒という事になりました。
例えば、このコードだとフィルターの根拠は address_book.json に
なっています。
毎回、読み込んでいます。
これだとテーブルで削除などの変更があった場合を考えて
一度データを保存してから再度読み込まないとなりません。
また、フィルターを掛けた画面を表示している場合。
この画面で削除などの変更を掛けた場合 QTableWidget のデータを
直接使えないので普段の削除と違うロジックが必要になります。
address_book.json とデータを突き合わせるなどが必要になります。
このような理由で「一致するデータを再表示」する方法は複雑に
なるので採用を見送りました。
4.全コード
import sys
import json
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton,
QTableWidget, QTableWidgetItem, QHeaderView, QWidget, QLabel, QDialog, QFormLayout
)
DATA_FILE = "address_book.json"
class EditDialog(QDialog):
def __init__(self, parent=None, data=None):
super().__init__(parent)
self.setWindowTitle("編集画面")
self.setGeometry(200, 200, 400, 300)
layout = QFormLayout()
self.last_name_input = QLineEdit(data["姓"] if data else "")
self.last_name_kana_input = QLineEdit(data["姓フリガナ"] if data else "")
self.first_name_input = QLineEdit(data["名"] if data else "")
self.first_name_kana_input = QLineEdit(data["名フリガナ"] if data else "")
self.postal_code_input = QLineEdit(data["郵便番号"] if data else "")
self.address_input = QLineEdit(data["住所"] if data else "")
self.phone_number_input = QLineEdit(data["電話番号"] if data else "")
self.notes_input = QLineEdit(data["備考"] if data else "")
layout.addRow("姓", self.last_name_input)
layout.addRow("姓フリガナ", self.last_name_kana_input)
layout.addRow("名", self.first_name_input)
layout.addRow("名フリガナ", self.first_name_kana_input)
layout.addRow("郵便番号", self.postal_code_input)
layout.addRow("住所", self.address_input)
layout.addRow("電話番号", self.phone_number_input)
layout.addRow("備考", self.notes_input)
self.save_button = QPushButton("保存")
self.cancel_button = QPushButton("キャンセル")
button_layout = QHBoxLayout()
button_layout.addWidget(self.save_button)
button_layout.addWidget(self.cancel_button)
layout.addRow(button_layout)
self.setLayout(layout)
# ボタンのイベント
self.cancel_button.clicked.connect(self.reject)
self.save_button.clicked.connect(self.save_data)
def get_data(self):
"""入力されたデータを辞書形式で返す"""
return {
"姓": self.last_name_input.text(),
"姓フリガナ": self.last_name_kana_input.text(),
"名": self.first_name_input.text(),
"名フリガナ": self.first_name_kana_input.text(),
"郵便番号": self.postal_code_input.text(),
"住所": self.address_input.text(),
"電話番号": self.phone_number_input.text(),
"備考": self.notes_input.text(),
}
def save_data(self):
"""データを取得してダイアログを閉じる"""
self.accept() # ダイアログを閉じる
class AddressBookApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("住所録")
self.setGeometry(100, 100, 800, 600)
# GUIを構築
self.setup_ui()
# データを読み込む
self.load_data_from_json(DATA_FILE)
def setup_ui(self):
"""GUIを構築するメソッド"""
# メインウィジェットとレイアウト
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
# フィルター入力と実行ボタン
filter_layout = QHBoxLayout()
self.filter_input = QLineEdit()
self.filter_input.setPlaceholderText("フィルターを入力")
self.filter_button = QPushButton("実行")
filter_layout.addWidget(self.filter_input)
filter_layout.addWidget(self.filter_button)
main_layout.addLayout(filter_layout)
# 一覧表
self.table = QTableWidget()
self.table.setColumnCount(8)
self.table.setHorizontalHeaderLabels([
"姓", "姓フリガナ", "名", "名フリガナ", "郵便番号", "住所", "電話番号", "備考"
])
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
main_layout.addWidget(self.table)
# ボタン群
button_layout = QHBoxLayout()
self.new_button = QPushButton("新規作成")
self.edit_button = QPushButton("編集")
self.delete_button = QPushButton("削除")
self.save_button = QPushButton("保存")
self.load_button = QPushButton("読み込み")
button_layout.addWidget(self.new_button)
button_layout.addWidget(self.edit_button)
button_layout.addWidget(self.delete_button)
button_layout.addWidget(self.save_button)
button_layout.addWidget(self.load_button)
main_layout.addLayout(button_layout)
# ボタンの動作
self.new_button.clicked.connect(self.open_new_dialog)
self.edit_button.clicked.connect(self.open_edit_dialog)
self.save_button.clicked.connect(self.save_data_to_json)
self.delete_button.clicked.connect(self.delete_selected_row)
self.filter_button.clicked.connect(self.apply_filter)
def load_data_from_json(self, file_path):
try:
# JSONデータの読み込み
with open(file_path, "r", encoding="utf-8") as file:
address_book = json.load(file)
# データをテーブルに追加
self.table.setRowCount(len(address_book)) # 行数を設定
for row, entry in enumerate(address_book):
for col, item in enumerate(["姓", "姓フリガナ", "名", "名フリガナ",
"郵便番号","住所", "電話番号", "備考"]):
cell = QTableWidgetItem(entry.get(item, ""))
self.table.setItem(row, col, cell)
except FileNotFoundError:
print("ファイルが見つかりません")
except json.JSONDecodeError:
print("JSONデータが壊れています")
def save_data_to_json(self):
data = []
for row in range(self.table.rowCount()):
entry = {
"姓": self.table.item(row, 0).text() if self.table.item(row, 0) else "",
"姓フリガナ": self.table.item(row, 1).text() if self.table.item(row, 1) else "",
"名": self.table.item(row, 2).text() if self.table.item(row, 2) else "",
"名フリガナ": self.table.item(row, 3).text() if self.table.item(row, 3) else "",
"郵便番号": self.table.item(row, 4).text() if self.table.item(row, 4) else "",
"住所": self.table.item(row, 5).text() if self.table.item(row, 5) else "",
"電話番号": self.table.item(row, 6).text() if self.table.item(row, 6) else "",
"備考": self.table.item(row, 7).text() if self.table.item(row, 7) else ""
}
data.append(entry)
with open(DATA_FILE, "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=4)
print("データを保存しました")
def open_new_dialog(self):
"""編集画面を開き、データを取得してテーブルに追加"""
dialog = EditDialog(self)
if dialog.exec(): # OK(保存)された場合のみ処理
new_entry = dialog.get_data()
row_position = self.table.rowCount()
self.table.insertRow(row_position)
for col, key in enumerate(["姓", "姓フリガナ", "名", "名フリガナ",
"郵便番号", "住所", "電話番号", "備考"]):
self.table.setItem(row_position, col, QTableWidgetItem(new_entry[key]))
def open_edit_dialog(self):
"""選択した行を編集する"""
selected_items = self.table.selectedItems()
if not selected_items:
print("編集する行を選択してください")
return
# 選択された行のうち、一番小さい行番号を取得
selected_rows = sorted(set(item.row() for item in selected_items))
row = selected_rows[0] # 最小の行番号を選択
# 既存データを取得
current_data = {
"姓": self.table.item(row, 0).text() if self.table.item(row, 0) else "",
"姓フリガナ": self.table.item(row, 1).text() if self.table.item(row, 1) else "",
"名": self.table.item(row, 2).text() if self.table.item(row, 2) else "",
"名フリガナ": self.table.item(row, 3).text() if self.table.item(row, 3) else "",
"郵便番号": self.table.item(row, 4).text() if self.table.item(row, 4) else "",
"住所": self.table.item(row, 5).text() if self.table.item(row, 5) else "",
"電話番号": self.table.item(row, 6).text() if self.table.item(row, 6) else "",
"備考": self.table.item(row, 7).text() if self.table.item(row, 7) else ""
}
# 編集画面を開き、データを渡す
dialog = EditDialog(self, data=current_data)
if dialog.exec(): # OK(保存)された場合のみ処理
updated_data = dialog.get_data()
# データをテーブルに反映
for col, key in enumerate(["姓", "姓フリガナ", "名", "名フリガナ",
"郵便番号", "住所", "電話番号", "備考"]):
self.table.setItem(row, col, QTableWidgetItem(updated_data[key]))
def delete_selected_row(self):
"""選択された行を削除する"""
selected_items = self.table.selectedItems()
if not selected_items:
print("削除する行を選択してください")
return
# 選択された行のインデックスを取得(重複を防ぐためsetでユニークにする)
selected_rows = sorted(set(item.row() for item in selected_items), reverse=True)
# 後ろから削除することで、インデックスのずれを防ぐ
for row in selected_rows:
self.table.removeRow(row)
print("選択した行を削除しました")
def apply_filter(self):
"""フィルターを適用して一覧を絞り込む"""
filter_text = self.filter_input.text().strip()
for row in range(self.table.rowCount()):
row_matches = False
for col in range(self.table.columnCount()):
item = self.table.item(row, col)
if item and filter_text.lower() in item.text().lower():
row_matches = True
break
# 一致しない行を非表示
self.table.setRowHidden(row, not row_matches)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = AddressBookApp()
window.show()
sys.exit(app.exec()
5.まとめ
今回は、住所録にフィルター機能を追加しました。
ChatGPTにフィルターをどのように実現したらよいかを尋ねたところ
2つの方法を提示されました。
1.一致しない行を非表示 : 少量のデータ、簡単
2.一致するデータを再表示 : 大量のデータ、高速
2の方法は、複雑になるので
QTableWidget で 一致しない行を非表示 にすることで フィルターを実現することにしました。
以上、ここまでで大まかな機能は全て実現できました。
次回は、エラー処理とダイアログの表示などの処理を加えて完成させます。
お楽しみに!