Ответ 1
У меня есть относительно надежное решение, но оно сложное и, вероятно, будет трудно понять, потому что оно требует некоторых знаний о том, как работает Tkinter и базовый текстовый виджет tcl/tk. Я представлю его здесь как полное решение, которое вы можете использовать как есть, потому что я думаю, что это иллюстрирует уникальный подход, который работает достаточно хорошо.
Обратите внимание, что это решение работает независимо от того, какой шрифт вы используете, и используете ли вы разные шрифты в разных строках, встроенные виджеты и т.д.
Импорт Tkinter
Прежде чем мы начнем, следующий код предполагает, что tkinter импортируется, как это, если вы используете python 3.0 или выше:
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
. Вы можете создать привязку, которая срабатывает при каждом нажатии клавиши, но вы также должны запускать кнопки мыши, и вам придется обрабатывать случай, когда пользователь нажимает клавишу и использует функцию автоматического повтора и т.д. Номера строк также необходимы перерисовываться, если окно выросло или сжалось, или пользователь прокручивает, поэтому мы попадаем в отверстие кролика, пытаясь выяснить все возможные события, которые могут привести к изменению чисел.
Существует еще одно решение, которое заключается в том, что текстовый виджет запускает событие всякий раз, когда что-то меняется. К сожалению, текстовый виджет не имеет прямой поддержки для уведомления о программе изменений. Чтобы обойти это, мы можем использовать прокси, чтобы перехватывать изменения в текстовом виджете и генерировать событие для нас.
В ответе на вопрос "привязка к движению курсора не меняет знак INSERT" Я предложил аналогичное решение, которое показывает, как заставить текстовый виджет вызывать обратный вызов всякий раз что-то меняется. На этот раз вместо обратного вызова мы создадим событие, так как наши потребности немного отличаются.
Пользовательский класс текста
Вот класс, который создает пользовательский текстовый виджет, который будет генерировать событие <<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
Объединяя все вместе
Наконец, вот примерная программа, которая использует эти два класса:
class Example(tk.Frame):
def __init__(self, *args, **kwargs):
tk.Frame.__init__(self, *args, **kwargs)
self.text = CustomText(self)
self.vsb = tk.Scrollbar(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()