【Tkinter開発】第5回 Tkinterでファイルを指定されたら読み込む【クラス間のデータやり取り】

Python,エンジニアリングPython,Tkinter

クラス間でデータのやり取りをする

前回までにMenuクラス内でファイルを指定できるようにした。

実際のファイル読み込みはTablesクラス内で処理したいので、読み込んだファイルパス文字列はTablesクラスに渡す。
これをするために、クラス間の共通変数を導入する。

クラス間共通変数propsの導入

クラス間の共通変数propsはTablesクラスとMenuクラスをそれぞれ呼び出しているApplicaitonクラス内のメンバ変数として定義する。

このメンバ変数の名前はpropsとしたが、使われていなければ名前は何でもよい。

ただし、propsの型は辞書型、リスト型、クラスのインスタンスにする必要がある。(つまり、数値、文字列等のスカラーがNG)
この辺の処理はPythonの「参照」の仕様を利用しており、Tkinterの仕様とは関係ない。
今回は辞書型を利用するが、規模の大きいアプリケーションならば、クラスのインスタンスを利用するのが安全である。

まずはTablesクラス

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

        self.props = props

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

    ...中略...

Menuクラス

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

        self.props = props

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

        ...中略...

Applicationクラス

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()

これでクラス間でデータのやり取りができるようになった。

例:使い方としては以下のようにする。(MenuクラスからTablesクラスにデータを送りたいとき)

class Menu:

    ...中略...

    def action(self):
        values = 1
        self.props["values"] = values

Menuクラスで定義されたaction()関数が実行されたときに、propsに新たなキー"values"として値をセットする。

class Tables:

     ...中略...

     def recieve(self):
         values = self.props.get("values")

Tablesクラスで定義されたrecieve()関数が実行されると、self.propsにキー"values"を指定して値を取り出せる。
(キー"values"が定義されていない可能性を考慮してget()で取り出している)

これはPythonの参照の仕様を利用しており、self.props変数自体を “self.props = {}" のように再定義してしまうと、利用できなくなってしまうので注意が必要である。

複雑すぎて理解が追いつかないんだけど、、

今はpropsをこのように定義すると、propsの中身を引き継げると思ってくれれば大丈夫です

ファイルパスを取得したら、Tablesクラスにデータを渡す

さっそく上記の機能を使って、ファイルダイアログでファイルパスを取得したら、Tablesクラスに値を渡して開けるようにする。

class Menu(tk.Menu):

    ...中略...

    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>>")

ファイルダイアログでファイルが指定された場合、self.propsに"open_file_name"にファイルパスをセットする。

そして、独自イベントとして"<<OpenFile>>"を発生させる。
これにより、Tablesクラスはファイルが指定されたことを検知できるようになり、self.propsからファイルパスを取り出す。

Tablesクラスは以下のようにする。

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

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

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

    ...中略...

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

        print(file_name)

tkinterで、イベントの検知はbind()関数またはbind_all()関数を利用する。
(bind_all()はtk.Tkインスタンスの階層をまたいで検知できる。)

“<<OpenFile>>"イベントを検知したらself.open_file()関数を呼び出す。

関数内では先程定義された"open_file_name"キーの値を取り出し、ファイルパスを取得できる。

ここまで実装するとファイルダイアログでファイルを指定すると、ファイルパスがprintされるはずである。

Tkinterにはクラス間でデータをやり取りする仕組みは用意されてないのかしら

いろいろ調べたけどTkinterにはクラス間でデータをやり取りする機能(tk.Tkインスタンスを経由したデータやり取り機能)がないようなので、このようなやり方にしたよ
他にはクリップボードを使ったやり方があるんだけど、もっと複雑なのでやめたました、、、

XMLデータを読み込んで表に表示させる

XMLデータを読み込んでパースする

ファイルパスが取得できるようになったので、Tablesクラスのopen_file()を書き換えて、XMLデータをパースする。
パースする処理は別途_parse_xml_data()関数に書いている。

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


class Tables(tk.Frame):

  ...中略...

    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

主題から外れるので、_perse_xml_data()関数の説明は割愛する。(Python標準のxmlパーサを使っている。)
RSS用のXMLファイルなので、それ以外の用途に使いたい場合はアレンジしてみてください。

open_file()関数内のimport_table_data()はすでに定義されており、この関数にデータを渡すと表内にデータを挿入できる。

サンプルデータ行の削除

最後に、__init__()で事前に入れていたサンプルデータはいらないので、この辺の処理を削除すればOK。

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

        self.props = props

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

        # サンプルデータの追加
        ...この辺を削除...

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

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

    ...中略...

起動すると事前データがないので、真っ白になる。

第5回 Tkinterの起動

ファイルの「ファイルを開く」を押すとファイルダイアログが開き、ファイルを指定できる。

第5回 ファイルダイアログ

ファイルを開くと、、、

第5回 ファイルの読み込み

ようやく読み込みまで出来ました

ファイルを読み込んで表示させるまで結構大変ね、、
でもGUIアプリっぽくなったわね


というわけで、次回は表のデータをCSVファイルとして出力する機能を実装します

あとは今までやってきたことの応用で出来そうね