質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.21%

tkinterのtreeviewにて、大量のデータを表示させる方法

解決済

回答 3

投稿

  • 評価
  • クリップ 0
  • VIEW 173

hijiri

score 57

tkinterのtreeviewにて、データベースから取得した大量のデータを表示させたいと考えています。

import tkinter as tk
import tkinter.ttk as ttk

class Treelist(tk.Frame):
    def __init__(self, root):
        super().__init__(root)
        self.pack(fill=tk.Y)

        self.tree = ttk.Treeview(self, height=30, show="headings")
        self.hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL, command=self.tree.xview)
        self.scrollbar = ttk.Scrollbar(self, command=self.tree.yview)
        self.tree.configure(xscrollcommand=lambda f, l: self.hscrollbar.set(f, l))
        self.tree.configure(yscrollcommand=lambda f, l: self.scrollbar.set(f, l)) 
        self.scrollbar.pack(side="right", fill="y")
        self.tree.pack(expand=1, fill=tk.BOTH)
        self.hscrollbar.pack(fill="x")

        self.state()
        self.tree_insert()

    def state(self):
        self.tree.configure(column=(1,2,3))
        #ヘッダーテキスト
        self.tree.heading(1,text="種類")
        self.tree.heading(2,text="名前")
        self.tree.heading(3,text="説明")
        #列の幅
        self.tree.column(1, width=100, anchor="center", stretch=tk.NO)
        self.tree.column(2, width=100, anchor="center", stretch=tk.NO)
        self.tree.column(3, width=200, anchor="center", stretch=tk.NO)

    def tree_insert(self):
        self.item = ()
        for i in range(10000):
            self.item = self.item + ((i,"犬","ワンと鳴く"),)
        for i in range(len(self.item)):
            self.num = self.item[i][0]
            self.name = self.item[i][1]
            self.text = self.item[i][2]
            self.tree.insert("", "end", values=(self.num, self.name, self.text))

root = tk.Tk()
root.geometry('900x750')
tree = Treelist(root)
root.mainloop()

self.itemはデータベースから取得して、文字列の一致検索に当てはまるデータを10000件として、仮に置いたものです。今回は、treeviewでの大量データの表示についてお尋ねしたかったので、このように省略させていただきました。

このように、10000件のデータを一気にinsertすると、パソコンの環境次第ではアプリが止まってしまいます。
また、これが1000000件のデータとなると、ハイスペックなパソコンでしか処理が追いつきません。

このような場合、どのようにすれば、メモリをあまり使用せずに表示することができるのでしょうか?

どうぞ宜しくお願いいたします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 3

checkベストアンサー

0

Treeview は大規模なデータの表示に対応してないので、
用途に合う外部ライブラリを探すか、表示するデータの扱いを工夫する必要があります。

具体的なコードは、前の回答以上のものを示せませんが
前提として、

  • 問題点: 全てのデータを insert している
  • 改善策: 表示に必要な部分のみを読み込む実装にする

ページ制にする

1画面に表示できる情報は、画面サイズにより限られているので
必要以上に insert せずに、固定行数のTreeview として運用します。

Treeview のデータの更新手段は2通り

  • ページ切り替えの度に delete/insert する
  • insert した行はそのままで、item メソッド により values 情報を更新する

スクロールバーによるページ遷移

ページ制に出来れば、大規模なデータを表示してるように見せかけるのは、
スクロールバーの移動によるページ遷移を実装します。(スクロールバーのイベントの扱いが難関)

一度に表示してる件数は、数十件のはずなので、
スクロールの度に 全て delete/insert しても、パフォーマンスに影響しませんが、
データベースがネットワーク経由の場合は、クエリ結果をキャッシュする等、
最適化した方が良いケースもあります。(最適化するポイント)

追記: SQL に言及しておくと、SELECT 文に OFFSET, LIMIT 句を使いクエリを構成します。

但し、これで終りではなく、実用化する為には
リサイズ時の表示件数の拡張、マウスホィールやキーボード操作の対応
等といった、雑多な操作への対応も必要です。(手間がかかる部分)

外部ライブラリを使う。他のGUIを選択する。

使ったことはありませんが、実装に当たり参考になるようなものは pandatable
tkinter 以外の GUI だと Qt や Wx が、大規模なデータ表示に対応したウィジェットを提供してます。

クラスの設計に関しては、
Qt(QListWidget/QListView) Wx(ListCtrl/ListView) を参考にした方が良いでしょう。
Qt (QTableView) Wx (dataview) を参考にした方が良いでしょう。
tkinter を使う場合は、ほぼ同等の機能を tkinter へ移植・実装することになります。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/02/23 08:55

    御返事ありがとうございます。

    先日教えていただきましたコードを、ツリービュー用に自分なりにいじってみてはいたのですが、どうしてもツリービューのスクロールバーと関数を紐付けることが出来ずに困っておりました。

    不躾かとは思いますが、もしよろしければツリービューの場合、どのようなコードで動くのか教えていただけませんでしょうか?

    また、教えていただきましたGUIやライブラリも、調べてみようと思います!

    キャンセル

  • 2021/02/23 12:15 編集

    分量的に解説は難しいですが、とりあえず自分でもやってみたところまでコードを提示します。
    Model クラスは、Qt のものを参考にしました。
    https://repl.it/@MiKLTea/TkTableView

    スクロールの扱いは難しい所で、操作により引数が変化します(see _yview メソッド)
    ”moveto" 0.0 ~ 1.0 の範囲 (0.0 が一番上、1.0 が一番下、範囲外までドラッグすると 0以下や1以上)
    "scroll" 1 or -1 "units" (スクロールバー上下の矢印をクリックした時)
    "scroll" 1 or -1 "pages" (スクロールバー外をクリックした時)

    ツリービューの場合、
    - 要素のクリア tree.delete(*tree.get_children())
     この後新たに insert する。(see _update_view メソッド)
    - 表示件数は tree["height"] で行数を指定。

    スクロールを実装する前に、まずはツリービューの表示件数を固定にして、
    次の/前の~件を表示機能をボタン等で実装してみましょう。

    キャンセル

  • 2021/02/23 22:54

    わざわざありがとうございます。

    教えていただきましたように、まずはボタンの切り替えにて表示する形で考えてみたいと思います。

    そのあとに、ぼちぼちと教えていただいたスクロールでの扱いを勉強していきたいと思います。

    キャンセル

0

※ 質問に対する回答ではありません。
質問のコードで気になった部分があったので

    def tree_insert(self):
        self.item = ()
        for i in range(10000):
            # XXX: tuple の連結操作は常に新しいオブジェクトを作成するので、
            # メモリにやさしくありません。self.item にはリストが適してます。
            self.item = self.item + ((i,"犬","ワンと鳴く"),)

        # XXX: 不要なインスタンス変数/スライス操作
        for i in range(len(self.item)):
            self.num = self.item[i][0]
            self.name = self.item[i][1]
            self.text = self.item[i][2]
            self.tree.insert("", "end", values=(self.num, self.name, self.text))
    def tree_insert(self):
        items = []
        for i in range(10000):
             items.append((i, "犬", "ワンと鳴く"))

        tree = self.tree
        for row in items:
            tree.insert("", "end", values=row)

        # 中の要素を編集したい場合は
        for num, name, text in items:
            tree.insert("", "end", values=(num, name, text))

    # 事前リストを作らず
    def tree_insert(self):
        tree_insert = self.tree.insert # ループ内で呼び出すメソッドをローカル変数に入れておく
        for i in range(10000):
            tree_insert("", "end", values=(i, "犬", "ワンと鳴く"))

計算量のオーダー自体は変わらないので、大規模なデータの扱いに対する回答ではありません。
明らかに冗長で効率化・改善が可能な部分の指摘です。


間違いではありませんが、引数が一致してる場合はlambda を省略可能です。

- self.tree.configure(xscrollcommand=lambda f, l: self.hscrollbar.set(f, l))
+ self.tree.configure(xscrollcommand=self.hscrollbar.set)

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/02/23 22:55

    色々と手ほどきいただき、ありがとうございます。

    メソッドをローカル変数に入れとくというのは、目からウロコでした。
    そんなことができるんですね。

    もう少し勉強してみたいと思います!

    キャンセル

  • 2021/02/24 09:05

    固定の引数も一緒に含めるなら、こんな方法も。
    tree_insert = functools.partial(self.tree.insert, "", "end")
    tree_insert(values=(1, 2, 3))

    ループ内で参照するオブジェクトをローカル変数にキャッシュしておくのは、
    数万回規模でループを廻すなら多少影響ある程度で、(乱用し過ぎると)
    却って冗長になる場面もあるので、コードの読みやすさと相談です。

    ちなみに、ここでの効率化は
    - 事前のリストの構築に1万回、ツリーへのinsertに1万回、
    合計2万回の繰り返しをひとまとめに。
    - 繰り返し一回当たりの処理の効率化

    多少の速度アップは見込めるものの、「計算量オーダー」の観点からは、
    処理全体(繰り返しの数)は、データ数に比例している事には変わらないので
    大規模なデータの扱いに対しての解という訳ではありません。

    キャンセル

0

要点のみに絞りコンパクトなサイズに出来たので、投稿しておきます。


改善したいコード(問題点: 大規模な数になるとパフォーマンスが落ちる)

import tkinter as tk
from tkinter import ttk

N = 1000 # データの件数

root = tk.Tk()
tree = ttk.Treeview(root, height=20, show="headings", columns=[1])
tree.pack()
for num in range(N): # XXX: 数が大規模になると時間が掛かる
    tree.insert("", tk.END, values=(num,))
root.mainloop()
  • Treeview の height は、画面内に表示する行数
    表示されてるウィジェットの高さと必ず一致するわけではない事が注意点。
  • columns=[1] は適当な値。

一度に表示する件数を一定数に制限する。

Next ボタンで次のページのデータを読み込む実装にします。

import tkinter as tk
from tkinter import ttk

N = 1000 # データ件数
PAGE_SIZE = 10 # 一度に表示する件数

root = tk.Tk()
tree = ttk.Treeview(root, height=PAGE_SIZE, show="headings", columns=[1])
tree.pack()

def update_tree_items(offset=0, size=PAGE_SIZE):
    tree.delete(*tree.get_children())

    # 改善点: データの数 N の値に関わらず、一定件数 (PAGE_SIZE) しか insert しない。
    for num in range(offset, min(N, offset+size)):
        tree.insert("", tk.END, values=(num,))

offset = 0
def next_page():
    global offset
    offset += PAGE_SIZE
    update_tree_items(offset, PAGE_SIZE)

button = ttk.Button(root, text="Next", command=next_page)
button.pack()

root.after_idle(update_tree_items, offset, PAGE_SIZE)
root.mainloop()
  • サンプル提示が目的&コード量を少なくする為、
    前のページ~等々は実装してません。
  • コードを短くするためにグローバル変数にしてますが、
    実際は、関数内であれば nonlocal や、クラスを使ってインスタンス変数に。

スクロールバーによるページ遷移

ページ単位の読込が実装出来れば、
ページ遷移をスクロールバーの位置から求める事で、
大規模なデータをツリービュー内に表示してるように見せかける事が可能です。

import tkinter as tk
from tkinter import ttk

N = 1000 # データの件数
PAGE_SIZE = 10 # 一度に表示する件数

root = tk.Tk()
tree = ttk.Treeview(root, height=PAGE_SIZE, show="headings", columns=[1])
tree.pack(fill=tk.BOTH, side=tk.LEFT, expand=tk.YES)


def update_tree_items(offset=0, size=PAGE_SIZE):
    # 表示中の要素を削除
    tree.delete(*tree.get_children())

    # offset から表示する要素のみを追加
    for num in range(offset, min(N, offset+size)):
        tree.insert("", tk.END, values=(num,))


def yview(command, amount, unit=None):
    if command == "moveto":
        # スクロールバー位置から表示するデータの offset を計算
        # 有効なスクロール範囲は 0.0 ~ 1.0 (範囲外までドラッグすると超えることも)
        # offset 範囲は 0 ~ N - PAGE_SIZE
        offset = max(0, min(N-PAGE_SIZE, int(float(amount) * N)))

        # 表示するアイテムを更新
        update_tree_items(offset, PAGE_SIZE)

        # スクロールバー位置の計算
        first = offset / N
        last = (offset + PAGE_SIZE) / N
        vbar.set(first, last)


vbar = ttk.Scrollbar(root, orient=tk.VERTICAL, command=yview)
vbar.pack(side=tk.RIGHT, fill=tk.Y)

root.after_idle(yview, "moveto", 0.0)
root.mainloop()

ここから先、キー操作・マウスホィール対応や他のスクロールイベントは
コメントに書いた URL のコードを参考にして下さい。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2021/02/24 17:21

    丁寧にコードまで示していただきまして、本当にありがとうございます!

    教えていただいたものを読み解いてみながら、もっと勉強していきます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.21%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る