web-dev-qa-db-ja.com

Tkinterのウィジェットのグループにスクロールバーを追加する

ログファイルからエントリを解析し、Tkinterを使用してエントリの内容を表示するためにPythonを使用しています。これまでは優れていました。スクロールバーを追加したいのですが、これは非常に簡単なはずですが、わかりません。

ドキュメントは、List、Textbox、Canvas、およびEntryウィジェットのみがスクロールバーインターフェイスをサポートすることを暗示しています。これらのいずれも、ウィジェットのグリッドを表示するのに適しているようには見えません。 Canvasウィジェットに任意のウィジェットを配置することは可能ですが、絶対座標を使用する必要があるように見えるため、グリッドレイアウトマネージャーを使用できませんか?

私はウィジェットグリッドをフレームに入れようとしましたが、それはスクロールバーインターフェースをサポートしていないようですので、これは機能しません:

mainframe = Frame(root, yscrollcommand=scrollbar.set)

誰でもこの制限を回避する方法を提案できますか?スクロールバーを追加するだけで、PyQtで書き直して実行可能イメージのサイズをこれほど大きくする必要はありません。

55
Simon Hibbs

概要

スクロールバーはいくつかのウィジェットにしか関連付けることができず、ルートウィジェットとFrameはそのウィジェットグループの一部ではありません。

最も一般的な解決策は、キャンバスウィジェットを作成し、スクロールバーをそのウィジェットに関連付けることです。次に、そのキャンバスに、ラベルウィジェットを含むフレームを埋め込みます。フレームの幅/高さを決定し、それをキャンバスのscrollregionオプションにフィードして、スクロール領域がフレームのサイズと正確に一致するようにします。

キャンバスにテキストアイテムを直接描画するのはそれほど難しくないため、キャンバスにフレームを埋め込むソリューションが複雑すぎると思われる場合は、このアプローチを再検討する必要があります。グリッドを作成しているので、各テキストアイテムの座標は、特に各行が同じ高さの場合(おそらく単一のフォントを使用している場合)、非常に簡単に計算できます。

キャンバスに直接描画するには、使用しているフォントの行の高さを把握するだけです(それにコマンドがあります)。そして、各y座標はrow*(lineheight+spacing)です。 x座標は、各列の最も幅の広いアイテムに基づく固定数になります。すべての列にタグを付けると、1つのコマンドで列内のすべてのアイテムのx座標と幅を調整できます。

オブジェクト指向ソリューション

オブジェクト指向のアプローチを使用した、フレームに埋め込まれたキャンバスソリューションの例を次に示します。

import tkinter as tk

class Example(tk.Frame):
    def __init__(self, root):

        tk.Frame.__init__(self, root)
        self.canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
        self.frame = tk.Frame(self.canvas, background="#ffffff")
        self.vsb = tk.Scrollbar(root, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.vsb.set)

        self.vsb.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas.create_window((4,4), window=self.frame, anchor="nw", 
                                  tags="self.frame")

        self.frame.bind("<Configure>", self.onFrameConfigure)

        self.populate()

    def populate(self):
        '''Put in some fake data'''
        for row in range(100):
            tk.Label(self.frame, text="%s" % row, width=3, borderwidth="1", 
                     relief="solid").grid(row=row, column=0)
            t="this is the second column for row %s" %row
            tk.Label(self.frame, text=t).grid(row=row, column=1)

    def onFrameConfigure(self, event):
        '''Reset the scroll region to encompass the inner frame'''
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

if __== "__main__":
    root=tk.Tk()
    Example(root).pack(side="top", fill="both", expand=True)
    root.mainloop()

手続き型ソリューション

オブジェクトを使用しないソリューションは次のとおりです。

import tkinter as tk

def populate(frame):
    '''Put in some fake data'''
    for row in range(100):
        tk.Label(frame, text="%s" % row, width=3, borderwidth="1", 
                 relief="solid").grid(row=row, column=0)
        t="this is the second column for row %s" %row
        tk.Label(frame, text=t).grid(row=row, column=1)

def onFrameConfigure(canvas):
    '''Reset the scroll region to encompass the inner frame'''
    canvas.configure(scrollregion=canvas.bbox("all"))

root = tk.Tk()
canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
frame = tk.Frame(canvas, background="#ffffff")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)

vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4,4), window=frame, anchor="nw")

frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))

populate(frame)

root.mainloop()

注: python 2.xでこの作業を行うには、importステートメントでTkinterではなくtkinterを使用します

97
Bryan Oakley

スクロール可能にする

この便利なクラスを使用して、ウィジェットを含むフレームをスクロール可能にします。次の手順を実行します:

  1. フレームを作成する
  2. 表示する(パック、グリッドなど)
  3. スクロール可能にする
  4. 内部にウィジェットを追加します
  5. update()メソッドを呼び出します

import tkinter as tk
from tkinter import ttk

class Scrollable(tk.Frame):
    """
       Make a frame scrollable with scrollbar on the right.
       After adding or removing widgets to the scrollable frame, 
       call the update() method to refresh the scrollable area.
    """

    def __init__(self, frame, width=16):

        scrollbar = tk.Scrollbar(frame, width=width)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)

        self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar.config(command=self.canvas.yview)

        self.canvas.bind('<Configure>', self.__fill_canvas)

        # base class initialization
        tk.Frame.__init__(self, frame)         

        # assign this obj (the inner frame) to the windows item of the canvas
        self.windows_item = self.canvas.create_window(0,0, window=self, anchor=tk.NW)


    def __fill_canvas(self, event):
        "Enlarge the windows item to the canvas width"

        canvas_width = event.width
        self.canvas.itemconfig(self.windows_item, width = canvas_width)        

    def update(self):
        "Update the canvas and the scrollregion"

        self.update_idletasks()
        self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))

使用例

root = tk.Tk()

header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack()
body.pack()
footer.pack()

ttk.Label(header, text="The header").pack()
ttk.Label(footer, text="The Footer").pack()


scrollable_body = Scrollable(body, width=32)

for i in range(30):
    ttk.Button(scrollable_body, text="I'm a button in the scrollable frame").grid()

scrollable_body.update()

root.mainloop()
4
Tarqez