naritoブログ

【お知らせ】
新ブログができました。今後そちらで更新し、このサイトは更新されません(ウェブサイト自体は残しておきます)
このブログの内容に関してコメントしたい場合は、新ブログのフリースペースに書き込んでください

このブログの内容を新ブログに移行中です。このブログで見つからない記事は、新ブログにありま

Tkinterで、行番号付きText

約424日前 2017年10月13日14:40
プログラミング関連
Python Tkinter
今回は行番号付きのTextウィジェットを作成します。

これもスタックオーバーフローのが基です。
https://stackoverflow.com/questions/16369470/tkinter-adding-line-number-to-text-widget/16375233#16375233


まずは順を追って実装していきましょう。
F1キー押下でupdate_line_numbersというメソッドが呼ばれるようにしてみます。

import tkinter as tk
import tkinter.ttk as ttk


class LineFrame(ttk.Frame):
"""行番号付きのフレーム"""

def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.create_widgets()

def create_widgets(self):
pass

def update_line_numbers(self, event=None):
pass


root = tk.Tk()

app = LineFrame(root)
app.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.E, tk.W))

# F1キーで行番号表示
root.bind('<F1>', app.update_line_numbers)
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
root.mainloop()




Textウィジェットそのものに行番号をつけるのではなく、行番号部分は別のウィジェットに任せます。
柔軟なtk.Canvasを使いましょう。

def create_widgets(self):
# 入力欄、行番号欄を作成
self.text = tk.Text(self)
self.line_numbers = tk.Canvas(self, width=40)

# 左から行番号、入力欄
self.line_numbers.grid(row=0, column=0, sticky=(tk.N, tk.S))
self.text.grid(row=0, column=1, sticky=(tk.N, tk.S, tk.W, tk.E))

# テキスト入力欄のみ拡大されるように
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)


見た目



Textウィジェットはスクロールがデフォルトで可能ですが、見た目的にもスクロールバーが欲しい、ということであれば以下のようにしておきましょう。

def create_widgets(self):
# 入力欄、行番号欄、スクロール部分を作成
self.text = tk.Text(self)
self.line_numbers = tk.Canvas(self, width=40)
self.ysb = ttk.Scrollbar(
self, orient=tk.VERTICAL, command=self.text.yview)

# 入力欄にスクロールを紐付け
self.text.configure(yscrollcommand=self.ysb.set)

# 左から行番号、入力欄、スクロールウィジェット
self.line_numbers.grid(row=0, column=0, sticky=(tk.N, tk.S))
self.text.grid(row=0, column=1, sticky=(tk.N, tk.S, tk.W, tk.E))
self.ysb.grid(row=0, column=2, sticky=(tk.N, tk.S))

# テキスト入力欄のみ拡大されるように
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)



見た目。スクロールバーがついてて良いですね。今回はこちらで進めていきます。他に部分に影響はないので、スクロールバーなしのサンプルでも大丈夫です。



この部分のwidth=40ですが、これは幅の指定です。これを消すとCanvasの行番号部分が大きくなり、不恰好になります。
好みに応じて数値の部分は変えてください。

self.line_numbers = tk.Canvas(self, width=40)



では、行番号の描画処理です。

def update_line_numbers(self, event=None):
"""行番号の描画"""
# 現在の行番号を全て消す
self.line_numbers.delete(tk.ALL)

# Textの0, 0座標、つまり一番左上が何行目にあたるかを取得
first_row = self.text.index('@0,0')
first_row_number = int(first_row.split('.')[0])

current = first_row_number
while True:
# dlineinfoは、その行がどの位置にあり、どんなサイズか、を返す
# (3, 705, 197, 13, 18) のように帰る(x,y,width,height,baseline)
dline = self.text.dlineinfo('{0}.0'.format(current))

# dlineinfoに、存在しない行や、スクロールしないと見えない行を渡すとNoneが帰る
if dline is None:
break
else:
y = dline[1] # y座標を取得

# (x座標, y座標, 方向, 表示テキスト)を渡して行番号のテキストを作成
self.line_numbers.create_text(3, y, anchor=tk.NW, text=current)
current += 1



文字を入力し、F1キーを押すとちゃんと行番号が表示されますね。




中身の処理をみていきます。大まかな流れは
1.Textウィジェットの、今見えている一番上の行が本来の何行目かを取得
2.行のy座標を取得する
3.Canvasに、上で得たy座標部分に行番号を挿入する
4.次の行番号を取得
5. 2〜4をループ
という感じです。

def update_line_numbers(self, event=None):



まず最初に、現在の行番号を全て消しておきます。これをしないと行番号の文字が重なってしまいます。

# 現在の行番号を全て消す
self.line_numbers.delete(tk.ALL)



indexメソッドに「@x,y」と渡すと、その座標に近い行番号を返します。今回は行番号だけでいいので、1.5 のような文字列から1を取り出します。

# Textの0, 0座標、つまり一番左上が何行目にあたるかを取得
first_row = self.text.index('@0,0')
first_row_number = int(first_row.split('.')[0])



上で得た行番号を基に、その行のy座標を取得します。dlineinfoです。
そのy座標を基に、Canvasに行番号を挿入していきます。

current = first_row_number
while True:
# dlineinfoは、その行がどの位置にあり、どんなサイズか、を返す
# (3, 705, 197, 13, 18) のように帰る(x,y,width,height,baseline)
dline = self.text.dlineinfo('{0}.0'.format(current))

# dlineinfoに、存在しない行や、スクロールしないと見えない行を渡すとNoneが帰る
if dline is None:
break
else:
y = dline[1] # y座標を取得

# (x座標, y座標, 方向, 表示テキスト)を渡して行番号のテキストを作成
self.line_numbers.create_text(3, y, anchor=tk.NW, text=current)
current += 1



現在はF1キーで行番号が描画されますが、これは自動で行われるべきです。

Tkinterで、Textウィジェットにon_change、on_scrollイベントを作る
https://torina.top/detail/406/
で作った、on_change, on_scrollイベント付きのTextウィジェットを使えば簡単に実装可能です。

それを実装したコードは、以下のようになります。

import tkinter as tk
import tkinter.ttk as ttk


class CustomText(tk.Text):
"""Textの、イベントを拡張したウィジェット"""

def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.tk.eval('''
proc widget_proxy {widget widget_command args} {
set result [uplevel [linsert $args 0 $widget_command]]
if {([lrange $args 0 1] == {xview moveto}) ||
([lrange $args 0 1] == {xview scroll}) ||
([lrange $args 0 1] == {yview moveto}) ||
([lrange $args 0 1] == {yview scroll})} {
event generate $widget <<Scroll>> -when tail
}
if {([lindex $args 0] in {insert replace delete})} {
event generate $widget <<Change>> -when tail
}
# return the result from the real widget command
return $result
}
''')
self.tk.eval('''
rename {widget} _{widget}
interp alias {{}} ::{widget} {{}} widget_proxy {widget} _{widget}
'''.format(widget=str(self)))


class LineFrame(ttk.Frame):
"""行番号付きのフレーム"""

def __init__(self, master=None, **kwargs):
super().__init__(master, **kwargs)
self.create_widgets()
self.create_event()

def create_event(self):
"""イベントの設定"""
# テキスト内でのスクロール時
self.text.bind('<<Scroll>>', self.update_line_numbers)

# テキストの変更時
self.text.bind('<<Change>>', self.update_line_numbers)

# ウィジェットのサイズが変わった際。行番号の描画を行う
self.text.bind('<Configure>', self.update_line_numbers)

def create_widgets(self):
# 入力欄、行番号欄、スクロール部分を作成
self.text = CustomText(self)
self.line_numbers = tk.Canvas(self, width=40)
self.ysb = ttk.Scrollbar(
self, orient=tk.VERTICAL, command=self.text.yview)

# 入力欄にスクロールを紐付け
self.text.configure(yscrollcommand=self.ysb.set)

# 左から行番号、入力欄、スクロールウィジェット
self.line_numbers.grid(row=0, column=0, sticky=(tk.N, tk.S))
self.text.grid(row=0, column=1, sticky=(tk.N, tk.S, tk.W, tk.E))
self.ysb.grid(row=0, column=2, sticky=(tk.N, tk.S))

# テキスト入力欄のみ拡大されるように
self.columnconfigure(1, weight=1)
self.rowconfigure(0, weight=1)

def update_line_numbers(self, event=None):
"""行番号の描画."""
# 現在の行番号を全て消す
self.line_numbers.delete(tk.ALL)

# Textの0, 0座標、つまり一番左上が何行目にあたるかを取得
first_row = self.text.index('@0,0')
first_row_number = int(first_row.split('.')[0])

current = first_row_number
while True:
# dlineinfoは、その行がどの位置にあり、どんなサイズか、を返す
# (3, 705, 197, 13, 18) のように帰る(x,y,width,height,baseline)
dline = self.text.dlineinfo('{0}.0'.format(current))

# dlineinfoに、存在しない行や、スクロールしないと見えない行を渡すとNoneが帰る
if dline is None:
break
else:
y = dline[1] # y座標を取得

# (x座標, y座標, 方向, 表示テキスト)を渡して行番号のテキストを作成
self.line_numbers.create_text(3, y, anchor=tk.NW, text=current)
current += 1


root = tk.Tk()

app = LineFrame(root)
app.grid(column=0, row=0, sticky=(tk.N, tk.S, tk.E, tk.W))

root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
root.mainloop()