【Tkinter開発】 Tkinterでファイル読み取りGUIを開発する【まとめ】

Python,エンジニアリングPython,Tkinter,シリーズものまとめ

Tkinterで作ったファイル読み込みGUIのまとめです。

全体のソースコードはここに記述する。

アーカイブ

ソースコード

アプリケーションは

python main.py

で起動する。

main.py

import tkinter as tk

from application import Application


def main():
    root = tk.Tk()
    app = Application(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()

application.py

import tkinter as tk

from tables import Tables
from menu import Menu


class Application(tk.Frame):
    def __init__(self, master: tk.Tk):
        super().__init__(master)
        self.title = "Tkinter Application"

        master.geometry("800x600")
        master.title(self.title)

        # クラス間で共通で使われる変数
        props: dict = {}

        Menu(self.master, props)

        self.mainframe = tk.Frame(self.master)
        self.mainframe.grid_rowconfigure(0, weight=1)
        self.mainframe.grid_columnconfigure(0, weight=1)
        self.mainframe.pack()

        self.tables = Tables(self.mainframe, props)
        self.tables.grid()

tables.py

import tkinter as tk
from tkinter import ttk
import xml.etree.ElementTree as ET
import csv


class Tables(tk.Frame):
    def __init__(self, master: tk.Tk, props: dict):
        super().__init__(master)

        self.props = props

        # テーブルの作成
        self.create_table()

        # # テーブルへデータ挿入
        # self.import_table_data(sample_data)

        # テーブルデータの詳細ディスプレイ
        self.create_display()

        # アプリケーション全体で独自イベントを捉える
        self.master.bind_all("<<OpenFile>>", lambda _: self.open_file())
        self.master.bind_all("<<SaveFile>>", lambda _: self.export_csv_data())

    def create_table(self):
        # 表の列名
        columns = [
            {"name": "Title", "width": 200},
            {"name": "Description", "width": 300},
            {"name": "Link", "width": 190},
            {"name": "Date", "width": 90},
        ]

        # Treeviewウィジェットを作成
        self.treeview = ttk.Treeview(
            self.master, columns=[col["name"] for col in columns], show="headings"
        )

        # 列名を設定
        for col in columns:
            name = col["name"]
            width = col["width"]
            self.treeview.heading(name, text=name)
            self.treeview.column(name, width=width)

        # スクロールバーを追加
        scrollbar = ttk.Scrollbar(
            self.master, orient="vertical", command=self.treeview.yview
        )
        self.treeview.configure(yscrollcommand=scrollbar.set)

        # グリッドに配置
        self.treeview.grid(row=0, column=0, sticky="nsew")
        scrollbar.grid(row=0, column=1, sticky="ns")

        # グリッド設定
        self.master.grid_rowconfigure(0, weight=1)
        self.master.grid_columnconfigure(0, weight=1)

        # Treeviewの選択イベントをバインド
        self.treeview.bind("<ButtonRelease-1>", lambda _: self.show_selected_row())

    def import_table_data(self, data):
        # 既存テーブルのデータを削除
        for item in self.treeview.get_children():
            self.treeview.delete(item)

        for row in data:
            self.treeview.insert("", "end", values=row)

    def create_display(self):
        # 表示フレーム
        self.display_frame = ttk.Frame(self.master)
        self.display_frame.grid(row=1, column=0)

        # ラベル, エントリを作成
        self.title_label = tk.Label(self.display_frame, text="Title: ")
        self.title_label.grid(row=1, column=0, sticky="w")
        self.title_entry = tk.Entry(self.display_frame, width=100)
        self.title_entry.grid(row=1, column=1, sticky="we")

        self.description_label = tk.Label(self.display_frame, text="Description: ")
        self.description_label.grid(row=2, column=0, sticky="w")
        self.description_text = tk.Text(self.display_frame, width=100, height=3)
        self.description_text.grid(row=2, column=1, sticky="we")

        self.link_label = tk.Label(self.display_frame, text="Link: ")
        self.link_label.grid(row=3, column=0, sticky="w")
        self.link_entry = tk.Entry(self.display_frame, width=100)
        self.link_entry.grid(row=3, column=1, sticky="we")

        self.date_label = tk.Label(self.display_frame, text="Date: ")
        self.date_label.grid(row=4, column=0, sticky="w")
        self.date_entry = tk.Entry(self.display_frame, width=100)
        self.date_entry.grid(row=4, column=1, sticky="we")

        self.ok_button = tk.Button(
            self.display_frame, text="OK", command=self.insert_display_data
        )
        self.ok_button.grid(row=5, columnspan=2)

    def show_selected_row(self):
        selected_item = self.treeview.selection()

        if selected_item:
            # 選択した行の値を取得
            values = self.treeview.item(selected_item, "values")
            title, description, link, date = values[0], values[1], values[2], values[3]

            # Entryに表示
            self.title_entry.delete(0, "end")
            self.title_entry.insert(0, title)

            # Textに表示
            self.description_text.delete(1.0, "end")
            self.description_text.insert(1.0, description)

            # Entryに表示
            self.link_entry.delete(0, "end")
            self.link_entry.insert(0, link)

            # Entryに表示
            self.date_entry.delete(0, "end")
            self.date_entry.insert(0, date)

    def insert_display_data(self):
        # 詳細ディスプレイの情報を表に反映する
        # 詳細ディスプレイの値を取得
        title = self.title_entry.get()
        description = self.description_text.get(1.0, "end").strip()
        link_entry = self.link_entry.get()
        date = self.date_entry.get()

        # 表に入力
        selected_item = self.treeview.selection()
        if selected_item:
            self.treeview.set(selected_item, column=0, value=title)
            self.treeview.set(selected_item, column=1, value=description)
            self.treeview.set(selected_item, column=2, value=link_entry)
            self.treeview.set(selected_item, column=3, value=date)

    def open_file(self):
        # ファイルパスの取得
        file_name = self.props.get("open_file_name")

        xml_data = self._parse_xml_data(file_name)

        self.import_table_data(data=xml_data)

    def _parse_xml_data(self, file_path):
        xml_data = []

        # ElementTreeオブジェクトの作成
        tree = ET.parse(file_path)
        root = tree.getroot()
        # ルート以下のchannelを取得
        channel = root.find(".//channel")
        # channel以下のitemを取得
        for item in channel.findall(".//item"):
            # item以下の情報を取得する
            title = item.find(".//title")
            title_data = title.text if title is not None else ""
            description = item.find(".//description")
            description_data = description.text if description is not None else ""
            link = item.find(".//link")
            link_data = link.text if link is not None else ""
            date = item.find(".//pubDate")
            date_data = date.text if date is not None else ""

            xml_data.append([title_data, description_data, link_data, date_data])

        return xml_data

    def export_csv_data(self):
        # テーブルのデータを取得
        csv_data = [
            self.treeview.item(item_id, "values")
            for item_id in self.treeview.get_children()
        ]

        file_name = self.props.get("save_file_name")
        with open(file_name, "w", newline="", encoding="utf-8") as f:
            csv_writer = csv.writer(f, quoting=csv.QUOTE_ALL)
            csv_writer.writerows(csv_data)

menu.py

import os

import tkinter as tk
from tkinter import filedialog


class Menu(tk.Menu):
    def __init__(self, master: tk.Tk, props: dict):
        super().__init__(master)

        self.props = props

        # メニューバー
        menubar = tk.Menu(self.master)

        menu_file = tk.Menu(menubar, tearoff=0)
        menu_file.add_command(label="ファイルを開く", command=self.open_filedialog)
        menu_file.add_command(label="名前を付けて保存", command=self.save_filedialog)
        # Ctrl+Sで保存
        self.master.bind("<Control-Key-s>", func=lambda _: self.save_filedialog())

        menubar.add_cascade(label="ファイル", menu=menu_file)

        # メニューバーの設定
        self.master.config(menu=menubar)

    def open_filedialog(self):
        # ファイルフィルタ
        file_type = [("XMLファイル", "*.xml"), ("", "*")]
        # 最初に開くフォルダ
        initial_directory_path = os.path.abspath(os.path.dirname(__file__))

        file_name = filedialog.askopenfilename(
            filetypes=file_type, initialdir=initial_directory_path
        )

        # ファイルを開いたら, イベントを発生させる
        if file_name != "":
            self.props["open_file_name"] = file_name
            self.master.event_generate("<<OpenFile>>")

    def save_filedialog(self):
        # ファイルフィルタ
        file_type = [("CSVファイル", ".csv")]
        # 最初に開くフォルダ
        if self.props.get("open_file_name") is not None:
            # ファイルを開いたパスのフォルダにする
            initial_directory_path = os.path.abspath(
                os.path.dirname(self.props["open_file_name"])
            )
        else:
            # ファイルを開いていなければ実行しているパスのフォルダ
            initial_directory_path = os.path.abspath(os.path.dirname(__file__))

        file_name = filedialog.asksaveasfilename(
            filetypes=file_type,
            initialdir=initial_directory_path,
            defaultextension="csv",
        )

        if file_name != "":
            self.props["save_file_name"] = file_name
            self.master.event_generate("<<SaveFile>>")