23

tkinter と python を学ぼうとしています。テキストウィジェットの行番号を隣接フレームに表示したい

from Tkinter import *
root = Tk()
txt = Text(root)
txt.pack(expand=YES, fill=BOTH)
frame= Frame(root, width=25)
#

frame.pack(expand=NO, fill=Y, side=LEFT)
root.mainloop()

unpythonic というサイトで例を見たことがありますが、txt の行の高さが 6 ピクセルであると想定しています。

私はこのようなことを試みています:

1) キー押下が発生した行を返す関数に Any-KeyPress イベントをバインドします。

textPad.bind("<Any-KeyPress>", linenumber)


def linenumber(event=None):
    line, column = textPad.index('end').split('.')
    #creating line number toolbar
    try:
       linelabel.pack_forget()
       linelabel.destroy()
       lnbar.pack_forget()
       lnbar.destroy()
    except:
      pass
   lnbar = Frame(root,  width=25)
   for i in range(0, len(line)):
      linelabel= Label(lnbar, text=i)
      linelabel.pack(side=LEFT)
      lnbar.pack(expand=NO, fill=X, side=LEFT)

残念ながら、これはフレームに奇妙な数字を与えています。もっと簡単な解決策はありますか?これにアプローチする方法は?

4

5 に答える 5

52

私は比較的簡単な解決策を持っていますが、Tkinter とその下にある tcl/tk テキスト ウィジェットがどのように機能するかについてある程度の知識が必要なため、複雑で理解しにくい可能性があります。ここでは、そのまま使用できる完全なソリューションとして紹介します。これは、非常にうまく機能する独自のアプローチを示していると思うからです。

このソリューションは、使用するフォントに関係なく、異なる行に異なるフォントを使用するかどうか、ウィジェットが埋め込まれているかどうかなどに関係なく機能することに注意してください。

Tkinter のインポート

始める前に、次のコードは、python 3.0 以降を使用している場合、tkinter がこのようにインポートされていることを前提としています。

import tkinter as tk

...またはこれ、python 2.xの場合:

import Tkinter as tk

行番号ウィジェット

行番号の表示に取り組みましょう。やりたいことは、数値を正確に配置できるようにキャンバスを使用することです。redrawカスタム クラスを作成し、関連するテキスト ウィジェットの行番号を再描画するという名前の新しいメソッドを指定します。attachまた、テキスト ウィジェットをこのウィジェットに関連付けるためのメソッドも提供します。

このメソッドは、テキスト ウィジェット自体がメソッドを介してテキスト行の開始位置と終了位置を正確に教えてくれるという事実を利用していますdlineinfo。これにより、キャンバス上のどこに行番号を描画するかを正確に知ることができます。また、行が表示されていない場合にdlineinfo返されるという事実Noneを利用して、行番号の表示をいつ停止するかを知ることができます。

class TextLineNumbers(tk.Canvas):
    def __init__(self, *args, **kwargs):
        tk.Canvas.__init__(self, *args, **kwargs)
        self.textwidget = None

    def attach(self, text_widget):
        self.textwidget = text_widget
        
    def redraw(self, *args):
        '''redraw line numbers'''
        self.delete("all")

        i = self.textwidget.index("@0,0")
        while True :
            dline= self.textwidget.dlineinfo(i)
            if dline is None: break
            y = dline[1]
            linenum = str(i).split(".")[0]
            self.create_text(2,y,anchor="nw", text=linenum)
            i = self.textwidget.index("%s+1line" % i)

これをテキスト ウィジェットに関連付けてからredrawメソッドを呼び出すと、行番号が適切に表示されるはずです。

行番号の自動更新

これは機能しますが、致命的な欠陥があります: を呼び出すタイミングを知っておく必要がありますredraw。キーを押すたびに起動するバインディングを作成できますが、マウス ボタンでも起動する必要があり、ユーザーがキーを押して自動繰り返し機能を使用する場合などを処理する必要があります。行番号も必要です。ウィンドウが拡大または縮小された場合、またはユーザーがスクロールした場合に再描画されるため、数値が変化する可能性のあるすべてのイベントを把握しようとするうさぎの穴に陥ります。

別の解決策として、何かが変更されるたびにテキスト ウィジェットでイベントを発生させるというものがあります。残念ながら、テキスト ウィジェットには、変更をプログラムに通知する機能が直接サポートされていません。これを回避するには、プロキシを使用してテキスト ウィジェットへの変更をインターセプトし、イベントを生成します。

「https://stackoverflow.com/q/13835207/7432」という質問への回答で、何かが変更されるたびにテキストウィジェットにコールバックを呼び出す方法を示す同様のソリューションを提供しました。今回は、ニーズが少し異なるため、コールバックの代わりにイベントを生成します。

カスタム テキスト クラス

<<Change>>以下は、テキストが挿入または削除されたとき、またはビューがスクロールされたときにイベントを生成するカスタム テキスト ウィジェットを作成するクラスです。

class CustomText(tk.Text):
    def __init__(self, *args, **kwargs):
        tk.Text.__init__(self, *args, **kwargs)

        # create a proxy for the underlying widget
        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)

    def _proxy(self, *args):
        # let the actual widget perform the requested action
        cmd = (self._orig,) + args
        result = self.tk.call(cmd)

        # generate an event if something was added or deleted,
        # or the cursor position changed
        if (args[0] in ("insert", "replace", "delete") or 
            args[0:3] == ("mark", "set", "insert") or
            args[0:2] == ("xview", "moveto") or
            args[0:2] == ("xview", "scroll") or
            args[0:2] == ("yview", "moveto") or
            args[0:2] == ("yview", "scroll")
        ):
            self.event_generate("<<Change>>", when="tail")

        # return what the actual widget returned
        return result        

すべてを一緒に入れて

最後に、これら 2 つのクラスを使用するサンプル プログラムを次に示します。

class Example(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        self.text = CustomText(self)
        self.vsb = tk.Scrollbar(self, orient="vertical", command=self.text.yview)
        self.text.configure(yscrollcommand=self.vsb.set)
        self.text.tag_configure("bigfont", font=("Helvetica", "24", "bold"))
        self.linenumbers = TextLineNumbers(self, width=30)
        self.linenumbers.attach(self.text)

        self.vsb.pack(side="right", fill="y")
        self.linenumbers.pack(side="left", fill="y")
        self.text.pack(side="right", fill="both", expand=True)

        self.text.bind("<<Change>>", self._on_change)
        self.text.bind("<Configure>", self._on_change)

        self.text.insert("end", "one\ntwo\nthree\n")
        self.text.insert("end", "four\n",("bigfont",))
        self.text.insert("end", "five\n")

    def _on_change(self, event):
        self.linenumbers.redraw()

...そしてもちろん、これをファイルの最後に追加して、ブートストラップします。

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(side="top", fill="both", expand=True)
    root.mainloop()
于 2013-05-04T14:14:12.093 に答える
4

これが私の同じことをしようとする試みです。上記の Bryan Oakley の回答を試してみました。見た目も機能も優れていますが、パフォーマンスには代償が伴います。ウィジェットに大量の行をロードするたびに、それを行うのに長い時間がかかります。これを回避するために、通常のTextウィジェットを使用して行番号を描画しました。

テキスト ウィジェットを作成し、行を追加するメインのテキスト ウィジェットの左側にグリッドを配置します。それを と呼びましょうtextarea。に使用するのと同じフォントも使用していることを確認してくださいtextarea

self.linenumbers = Text(self, width=3)
self.linenumbers.grid(row=__textrow, column=__linenumberscol, sticky=NS)
self.linenumbers.config(font=self.__myfont)

行番号ウィジェットに追加されたすべての行を右揃えにするタグを追加して、それを呼び出しましょうline:

self.linenumbers.tag_configure('line', justify='right')

ユーザーが編集できないようにウィジェットを無効にする

self.linenumbers.config(state=DISABLED)

uniscrollbarここで難しいのは、1 つのスクロールバーを追加することです。それを呼び出して、メイン テキスト ウィジェットと行番号テキスト ウィジェットの両方を制御しましょう。そのためには、最初に 2 つのメソッドが必要です。1 つはスクロールバーによって呼び出され、新しい位置を反映するために 2 つのテキスト ウィジェットを更新できます。もう 1 つは、テキスト領域がスクロールされるたびに呼び出され、更新されます。スクロールバー:

def __scrollBoth(self, action, position, type=None):
    self.textarea.yview_moveto(position)
    self.linenumbers.yview_moveto(position)

def __updateScroll(self, first, last, type=None):
    self.textarea.yview_moveto(first)
    self.linenumbers.yview_moveto(first)
    self.uniscrollbar.set(first, last)

uniscrollbarこれで、次のものを作成する準備が整いました。

    self.uniscrollbar= Scrollbar(self)
    self.uniscrollbar.grid(row=self.__uniscrollbarRow, column=self.__uniscrollbarCol, sticky=NS)
    self.uniscrollbar.config(command=self.__scrollBoth)
    self.textarea.config(yscrollcommand=self.__updateScroll)
    self.linenumbers.config(yscrollcommand=self.__updateScroll)

出来上がり!これで、行番号付きの非常に軽量なテキスト ウィジェットができました。

ここに画像の説明を入力

于 2016-05-07T10:23:46.650 に答える
2

unpythonic というサイトで例を見てきましたが、txt の行の高さが 6 ピクセルであると想定しています。

比較:

# 各行は少なくとも 6 ピクセルの高さだと仮定する
step = 6

step- プログラムが新しい行のテキスト ウィジェットをチェックする頻度 (ピクセル単位)。テキスト ウィジェットの行の高さが 30 ピクセルの場合、このプログラムは 5 つのチェックを実行し、1 つの数字だけを描画します。
フォントが非常に小さい場合は、6 未満の値に設定できます。
1 つの条件があります。textウィジェット内のすべてのシンボルは 1 つのフォントを使用する必要があり、数字を描画するウィジェットは同じフォントを使用する必要があります。

# http://tkinter.unpythonic.net/wiki/A_Text_Widget_with_Line_Numbers
class EditorClass(object):
    ...


    self.lnText = Text(self.frame,
                    ...
                    state='disabled', font=('times',12))
    self.lnText.pack(side=LEFT, fill='y')
    # The Main Text Widget
    self.text = Text(self.frame,
                        bd=0,
                        padx = 4, font=('times',12))
    ...
于 2013-05-04T11:48:02.233 に答える
1

ここに記載されている各ソリューションを徹底的に読み、そのうちのいくつかを自分で試した後、Brian Oakley のソリューションをいくつか変更して使用することにしました。これはより効率的な解決策ではないかもしれませんが、迅速かつ簡単に実装できる方法を探している人にとっては十分なはずです。これは原理的にも単純です。

同じ方法で線を描画しますが、イベントを生成する代わりに<<Change>>、キー押下、スクロール、左クリック イベントをテキストにバインドし、左クリック イベントをスクロールバーにバインドします。たとえば、貼り付けコマンドが実行されたときにエラーが発生しないようにするために、2ms実際に行番号を再描画する前に待機します。

編集:これは FoxDot のソリューションにも似ていますが、行番号を常に更新する代わりに、バインドされたイベントでのみ更新されます

以下は、スクロールの私の実装とともに、遅延が実装されたサンプルコードです

import tkinter as tk


# This is a scrollable text widget
class ScrollText(tk.Frame):
    def __init__(self, master, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        self.text = tk.Text(self, bg='#2b2b2b', foreground="#d1dce8", 
                            insertbackground='white',
                            selectbackground="blue", width=120, height=30)

        self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL, command=self.text.yview)
        self.text.configure(yscrollcommand=self.scrollbar.set)

        self.numberLines = TextLineNumbers(self, width=40, bg='#313335')
        self.numberLines.attach(self.text)

        self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.numberLines.pack(side=tk.LEFT, fill=tk.Y, padx=(5, 0))
        self.text.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        self.text.bind("<Key>", self.onPressDelay)
        self.text.bind("<Button-1>", self.numberLines.redraw)
        self.scrollbar.bind("<Button-1>", self.onScrollPress)
        self.text.bind("<MouseWheel>", self.onPressDelay)

    def onScrollPress(self, *args):
        self.scrollbar.bind("<B1-Motion>", self.numberLines.redraw)

    def onScrollRelease(self, *args):
        self.scrollbar.unbind("<B1-Motion>", self.numberLines.redraw)

    def onPressDelay(self, *args):
        self.after(2, self.numberLines.redraw)

    def get(self, *args, **kwargs):
        return self.text.get(*args, **kwargs)

    def insert(self, *args, **kwargs):
        return self.text.insert(*args, **kwargs)

    def delete(self, *args, **kwargs):
        return self.text.delete(*args, **kwargs)

    def index(self, *args, **kwargs):
        return self.text.index(*args, **kwargs)

    def redraw(self):
        self.numberLines.redraw()


'''THIS CODE IS CREDIT OF Bryan Oakley (With minor visual modifications on my side): 
https://stackoverflow.com/questions/16369470/tkinter-adding-line-number-to-text-widget'''


class TextLineNumbers(tk.Canvas):
    def __init__(self, *args, **kwargs):
        tk.Canvas.__init__(self, *args, **kwargs, highlightthickness=0)
        self.textwidget = None

    def attach(self, text_widget):
        self.textwidget = text_widget

    def redraw(self, *args):
        '''redraw line numbers'''
        self.delete("all")

        i = self.textwidget.index("@0,0")
        while True :
            dline= self.textwidget.dlineinfo(i)
            if dline is None: break
            y = dline[1]
            linenum = str(i).split(".")[0]
            self.create_text(2, y, anchor="nw", text=linenum, fill="#606366")
            i = self.textwidget.index("%s+1line" % i)


'''END OF Bryan Oakley's CODE'''

if __name__ == '__main__':
    root = tk.Tk()
    scroll = ScrollText(root)
    scroll.insert(tk.END, "HEY" + 20*'\n')
    scroll.pack()
    scroll.text.focus()
    root.after(200, scroll.redraw())
    root.mainloop()

また、Brian Oakley のコードでは、マウス ホイールを使用してスクロールすると (スクロール可能なテキストがいっぱいになると)、一番上の数字の行が時々グリッチアウトして実際のテキストと同期しなくなることに気付きました。最初に遅延を追加します。Scrolled Text ウィジェットの独自の実装でのみテストしただけなので、このバグは私のソリューションに固有のものである可能性があります。

于 2020-03-09T18:24:25.663 に答える
1

上記の Bryan Oakley の回答に基づいて使用した非常に簡単な方法があります。行われた変更をリッスンする代わりに、メソッドを使用してウィジェットを単に「更新」します。このself.after()メソッドは、数ミリ秒後に呼び出しをスケジュールします。それを行う非常に簡単な方法。この例では、インスタンス化時にテキスト ウィジェットをアタッチしますが、必要に応じて後で行うこともできます。

class TextLineNumbers(tk.Canvas):
        def __init__(self, textwidget, *args, **kwargs):
        tk.Canvas.__init__(self, *args, **kwargs)
        self.textwidget = textwidget
        self.redraw()

    def redraw(self, *args):
        '''redraw line numbers'''
        self.delete("all")

        i = self.textwidget.index("@0,0")
        while True :
            dline= self.textwidget.dlineinfo(i)
            if dline is None: break
            y = dline[1]
            linenum = str(i).split(".")[0]
            self.create_text(2,y,anchor="nw", text=linenum)
            i = self.textwidget.index("%s+1line" % i)

        # Refreshes the canvas widget 30fps
        self.after(30, self.redraw)
于 2017-01-04T09:48:36.523 に答える