当我注意到如果长行不可见时,ScrolledText 小部件的水平滚动条将消失时,我一直在修改 PAGE 滚动小部件的实现。事实证明这与水平滚动条无关,是 原始 Tk 行为 :
pathName xview
返回包含两个元素的列表。每个元素都是 0 到 1 之间的实数分数;它们一起描述了窗口中可见的文档水平范围的部分。例如,如果第一个元素为 0.2,第二个元素为 0.6,则 20% 的文本位于左侧屏幕外,中间 40% 在窗口中可见,40% 的文本位于屏幕外 - 分数仅指窗口中实际可见的行:如果窗口中的行都很短,因此它们完全可见,则返回的分数将为 0 和 1,即使文本中还有比窗口宽得多的其他行。
使用自动隐藏滚动条,垂直滚动会使水平滚动条不断缩小、变宽,有时会消失(稍后会影响垂直滚动条)。
如何我绕过这个?
相关来源(主要由 PAGE 生成,还有我为另一个滚动小部件添加的一些内容):
class AutoScroll(object):
"""Configure the scrollbars for a widget."""
def __init__(self, master, cbl=None):
try:
vsb = ttk.Scrollbar(master, orient='vertical', command=self.yview)
except:
pass
hsb = ttk.Scrollbar(master, orient='horizontal', command=self.xview)
try:
self.configure(yscrollcommand=self._autoscroll(vsb, cbl))
except:
pass
self.configure(xscrollcommand=self._autoscroll(hsb, cbl))
self.grid(column=0, row=0, sticky='nsew')
try:
vsb.grid(column=1, row=0, sticky='ns')
except:
pass
hsb.grid(column=0, row=1, sticky='ew')
master.grid_columnconfigure(0, weight=1)
master.grid_rowconfigure(0, weight=1)
# Copy geometry methods of master (taken from ScrolledText.py)
methods = (
Pack.__dict__.keys() |
Grid.__dict__.keys() |
Place.__dict__.keys()
)
for meth in methods:
if (meth[0] != "_" and meth not in ("config", "configure") and
meth not in type(self).__bases__[0].__dict__):
setattr(self, meth, getattr(master, meth))
def clscroll(self, cbl):
# Checklist elements scroll
root.update()
for i, w in enumerate(cbl[1]):
bb = ttk.Treeview.bbox(self, i, "#0")
if bb == "":
w.place_forget()
else:
x, y, _, _ = bb
w.place(x=x+(tvhh-2)/2, y=y+tvrh/2, anchor='center')
cbl[0].place(x=x+(tvhh-2)/2, anchor='center')
root.update()
# @staticmethod
def _autoscroll(self, sbar, cbl):
"""Hide and show scrollbar as needed."""
def wrapped(first, last):
if cbl:
self.clscroll(cbl)
first, last = float(first), float(last)
if first <= 0 and last >= 1:
sbar.grid_remove()
else:
sbar.grid()
sbar.set(first, last)
return wrapped
def __str__(self):
return str(self.master)
def _create_container(func):
"""Creates a ttk Frame with a given master, and use this new frame to
place the scrollbars and the widget."""
def wrapped(cls, master, **kw):
container = ttk.Frame(master)
return func(cls, container, **kw)
return wrapped
class ScrolledText(AutoScroll, Text):
"""A standard Tkinter Text widget with scrollbars that will
automatically show/hide as needed."""
@_create_container
def __init__(self, master, **kw):
Text.__init__(self, master, **kw)
AutoScroll.__init__(self, master)
由 TheLizzard 编辑
更短的 mre:
import tkinter as tk
class MyScrollbar(tk.Canvas):
def __init__(self, master):
super().__init__(master, bg="red", height=10, width=1,
highlightthickness=0)
def set(self, low, high):
if (low == "0.0") and (high == "1.0"):
self.pack_forget()
elif (low != "0.0") or (high != "1.0"):
self.pack(side="bottom", fill="x")
if __name__ == "__main__":
root = tk.Tk()
scrollbar = MyScrollbar(root)
scrollbar.pack(side="bottom", fill="x")
text = tk.Text(root, width=30, height=10, wrap="none",
xscrollcommand=scrollbar.set)
text.insert("end", "aa"+"\n"*12+"a"*31)
text.pack(side="top", fill="both")
# Scroll up/down to see the scrollbar (in red) disappear and reappear
要解决这个问题,需要在 Tkinter 的 Text 小部件中跟踪最长行的宽度,并在水平滚动条的逻辑中使用它,而不是依赖
xview
返回的值。
以下是一种修改后的
AutoScroll
类,它可以实现这一点:
import tkinter as tk
from tkinter import ttk
class AutoScroll(object):
"""为小部件配置滚动条."""
def __init__(self, master, text_widget, cbl=None):
self.text_widget = text_widget # 存储对 Text 小部件的引用
self.max_line_width = 0 # 初始化最长行的宽度
vsb = ttk.Scrollbar(master, orient='vertical', command=self.yview)
hsb = ttk.Scrollbar(master, orient='horizontal', command=self.xview)
self.configure(yscrollcommand=self._autoscroll(vsb, cbl))
self.configure(xscrollcommand=self._autoscroll(hsb, cbl))
self.grid(column=0, row=0, sticky='nsew')
vsb.grid(column=1, row=0, sticky='ns')
hsb.grid(column=0, row=1, sticky='ew')
master.grid_columnconfigure(0, weight=1)
master.grid_rowconfigure(0, weight=1)
# 绑定 Text 小部件的修改事件
text_widget.bind("<KeyRelease>", self.update_max_line_width)
text_widget.bind("<Configure>", self.update_max_line_width)
# ... (其他方法保持不变)
def update_max_line_width(self, event=None):
"""更新最长行的宽度."""
font = self.text_widget.cget("font") or ''
if isinstance(font, tuple):
font = font[0]
font_width = tk.font.Font(font=font).measure("0")
lines = self.text_widget.get("1.0", "end").splitlines()
self.max_line_width = max([font_width * len(line) for line in lines] + [0])
def _autoscroll(self, sbar, cbl):
"""根据需要隐藏和显示滚动条."""
def wrapped(first, last):
if cbl:
self.clscroll(cbl)
first, last = float(first), float(last)
# 水平滚动条逻辑:
if sbar.cget("orientation") == "horizontal":
text_width = self.text_widget.winfo_width() # 获取 Text 小部件的宽度
if self.max_line_width > text_width: # 如果最长行比 Text 小部件宽
sbar.grid()
else:
sbar.grid_remove()
else:
if first <= 0 and last >= 1:
sbar.grid_remove()
else:
sbar.grid()
sbar.set(first, last)
return wrapped
# ... (其他方法保持不变)
在这个修改后的代码中:
-
我们在
AutoScroll
类中添加了一个update_max_line_width
方法,该方法计算并存储 Text 小部件中最长行的像素宽度。 -
我们在 Text 小部件上绑定了
<KeyRelease>
和<Configure>
事件,以便在文本内容或窗口大小更改时更新最长行的宽度。 -
我们在
_autoscroll
方法中添加了逻辑,根据最长行的宽度和 Text 小部件的宽度来控制水平滚动条的显示和隐藏。
使用此修改后的
AutoScroll
类,即使
wrap=none
并且长行不可见,水平滚动条也会按预期工作。