文本#

文本小部件管理多行文本区域。与画布小部件一样,Tk的文本小部件是极其灵活且强大的工具,可用于多种任务。它可以作为表单的一部分提供简单的多行文本区域。但是,文本小部件也可以构成风格化的代码编辑器、大纲生成器、网页浏览器等等的基础。

注意:文本小部件是经典Tk小部件的一部分,而不是主题化Tk小部件的一部分。

虽然我们在之前的章节中简要介绍了文本小部件,但在这里我们将更详细地探讨它们。你将更好地理解它们允许的复杂性水平。然而,如果你计划使用文本小部件进行任何重要的工作,参考手册是一本组织良好、有帮助且非常推荐的读物。

文本小部件是通过Text类创建的:

text = Text(parent, width=40, height=10)

通常,你会提供一个宽度(以字符为单位)和高度(以行为单位)。像往常一样,你可以要求几何管理器将其扩展到填充窗口中的可用空间。

基础知识#

如果你只是需要为表单创建一个多行文本字段,那么只有几件事需要注意:创建并调整小部件的大小(检查),提供一个初始值,并在用户提交表单后检索文本。

提供初始内容#

文本小部件一开始是空的,因此我们需要自己添加任何初始内容。由于文本小部件可以容纳的内容远不止纯文本,一个简单的机制(如输入小部件的textvariable配置选项)是不够的。

相反,我们将使用小部件的 insert 方法:

text.insert('1.0', 'here is my\ntext to insert')

这里的 "1.0" 是要插入文本的位置,可以被理解为“第1行,第0个字符”。这指的是第一行的第一个字符。历史上,特别是在Unix系统上,程序员倾向于将行号视为基于1的,而字符位置则基于0。

要插入的文本只是一个字符串。因为小部件可以保存多行文本,我们提供的字符串也可以是多行的。为此,只需在字符串中的适当位置嵌入 (换行符)。

检索文本#

用户进行任何更改并提交表单后(例如),您的程序可以通过get方法检索小部件的内容:

thetext = text.get('1.0', 'end')

这两个参数分别是开始和结束位置;end的含义很明显。如果您只想获取文本的一部分,可以提供不同的起始和结束位置。稍后您会了解更多关于位置的信息。

自定义外观#

我们之前看到了文本小部件的宽度和高度配置选项。还有几个其他选项控制其外观。最有用的包括:

  • foreground: 绘制文本的颜色

  • background: 小部件的背景色

  • padx, pady: 小部件内边框的额外填充

  • borderwidth: 围绕小部件的边框宽度

  • relief: 边框样式:flat, raised, sunken, solid, ridge, groove

文本的换行与滚动#

当小部件中的一些文本行非常长,超出了小部件的宽度时,会发生什么情况呢?默认情况下,文本会自动换到下一行。这种行为可以通过wrap配置选项来改变。其默认值是char,意味着在任何字符处换行。其他选项包括word,即只在单词边界(例如空格)处换行,以及none,意味着根本不换行。在后者的情况下,除非我们为小部件附加一个水平滚动条,否则较长行的文本将无法完全显示。(即使没有滚动条,用户也可以通过箭头键来滚动查看文本)。

水平和垂直滚动条都可以以与其他小部件(例如画布、列表框)相同的方式附加到文本小部件上。

t = Text(root, width = 40, height = 5, wrap = "none")
ys = ttk.Scrollbar(root, orient = 'vertical', command = t.yview)
xs = ttk.Scrollbar(root, orient = 'horizontal', command = t.xview)
t['yscrollcommand'] = ys.set
t['xscrollcommand'] = xs.set
t.insert('end', "Lorem ipsum...\n...\n...")
t.grid(column = 0, row = 0, sticky = 'nwes')
xs.grid(column = 0, row = 1, sticky = 'we')
ys.grid(column = 1, row = 0, sticky = 'ns')
root.grid_columnconfigure(0, weight = 1)
root.grid_rowconfigure(0, weight = 1)

我们也可以要求小部件确保文本的特定部分可见。例如,假设我们在小部件中添加了比屏幕能显示的更多的文本(因此它将会滚动)。然而,我们希望确保顶部的文本而不是底部的文本是可见的。我们可以使用 see 方法。

text.see('1.0')

禁用小部件#

某些表单会暂时禁用特定小部件的编辑功能,除非满足某些条件(例如,其他选项被设定为特定值)。为了阻止用户修改文本小部件,可以将状态配置选项设置为“disabled”。通过将此选项重新设置为“normal”,可以重新启用编辑功能。

text['state'] = 'disabled'

备注

由于文本小部件是经典小部件的一部分,因此通常的 stateinstate 方法不可用。

在代码中修改文本#

用户可以通过文本小部件交互式地修改文本,您的程序也可以进行更改。添加文本是通过insert方法完成的,我们在上文中使用它为文本小部件提供了初始值。

文本位置和索引#

当我们指定位置1.0(第一行,第一个字符)时,这是一个索引的示例。它告诉insert方法在哪里放置新文本(就在第一行,第一个字符之前,即小部件的开始)。索引可以用多种方式指定。我们用get方法使用了另一种方式:end表示文本末尾之后的位置。(为什么是“刚刚超过”?因为文本是插入到给定索引之前的,所以插入到end会在小部件的末尾添加文本)。请注意,Tk总是在文本小部件的末尾添加一个新行。

这里有几个关于索引的额外示例以及如何解释它们:

  • 3.end: 第3行的换行符。

  • 1.0 + 3 chars: 从第1行开始的三个字符之后。

  • 2.end -1 chars: 第2行新行之前的最后一个字符。

  • end -1 chars: Tk总是在文本末尾添加的新行。

  • end -2 chars: 文本的实际最后一个字符。

  • end -1 lines: 文本最后一行的实际起始位置。

  • 2.2 + 2 lines: 第四行文本的第三个字符(索引2)。

  • 2.5 linestart: 第2行的第一个字符。

  • 2.5 lineend: 第2行末尾的新行位置。

  • 2.5 wordstart:索引2.5处的字符所在的单词的第一个字符。

  • 2.5 wordend: 索引2.5处的字符所在的单词之后的第一个字符。

一些需要注意的额外事项:

  • 术语“chars”可以缩写为“c”,而“lines”可以缩写为“l”。

  • 术语之间的空格可以省略,例如,1.0+3c。

  • 超出文本末尾的索引(例如,end + 100c)将被解释为end。

  • 根据需要,索引会换行到下一行;例如,在一个只有五个字符的行上,1.0 + 10个字符将引用第二行上的一个位置。

  • 索引中的行号被解释为逻辑行,即每一行仅在“\n”处结束。对于长行和启用了换行的情况,一个逻辑行可能代表多个显示行。如果您希望在显示中向上或向下移动一行,可以指定为,例如,“1.0 + 2个显示行”。

  • 当索引包含多个单词时,请确保它们被适当地引用,以便Tk将其视为一个参数。

  • 要确定索引的规范位置,请使用index方法。传递任何索引表达式,它返回相应的索引形式line.char。例如,要找到最后一个字符(忽略末尾的自动换行符),可以使用:

text.index('end')

您可以使用compare方法比较两个索引,这使您可以检查它们是否相等,或者一个索引是否在文本中位于另一个索引之后等。

if text.compare(idx1, "==", idx2):  # 同一位置

有效的运算符是 ==!=<<=>>=

删除文本#

虽然插入方法可以在小部件中的任何位置添加新文本,但 delete start?end? 方法则用于移除文本。我们可以删除单个字符(通过索引指定)或一系列字符(通过起始和结束索引指定)。在后一种情况下,从(且包括)起始索引到结束索引之前的所有字符都会被删除(结束索引处的字符不会被删除)。因此,如果我们假设这些操作开始时文本小部件中的内容是 "abcd\nefgh"

text.delete('1.2')  "abd\nefgh"
text.delete('1.1', '1.2')  "acd\nefgh"
text.delete('1.0', '2.0')  "efgh"
text.delete('1.2', '2.1')  "abfgh"

此外,还有 replace 方法,它执行 delete 操作后在同一位置进行 insert

示例:日志窗口#

这里有一个简短的示例,使用一个文本小部件作为应用程序的80x24日志窗口。用户完全不会编辑这个文本小部件。相反,程序将日志消息写入其中。我们希望显示超过24行(因此不滚动)。如果日志已满,旧的消息将从顶部开始删除,然后新的消息添加到末尾。

from tkinter import *
from tkinter import ttk

root = Tk()
log = Text(root, state='disabled', width=80, height=24, wrap='none')
log.grid()

def writeToLog(msg):
    numlines = int(log.index('end - 1 line').split('.')[0])
    log['state'] = 'normal'
    if numlines==24:
        log.delete(1.0, 2.0)
    if log.index('end-1c')!='1.0':
        log.insert('end', '\n')
    log.insert('end', msg)
    log['state'] = 'disabled'

备注

请注意,由于程序将小部件置于禁用状态,我们必须重新启用它才能进行任何更改,即使是通过我们的程序也是如此。

格式化与标签#

到目前为止,我们使用文本小部件时,所有文本都使用单一字体。现在该添加格式了,比如粗体、斜体、删除线、背景颜色、字体大小等。Tk的文本小部件通过一个名为“标签”的功能来实现这些格式。

标签是与文本小部件相关联的对象。每个标签由程序员选择的名称来引用。每个标签有几个配置选项,如控制格式的字体和颜色。虽然标签是有状态的对象,但不需要显式创建,而是在第一次使用标签名称时自动创建。

向文本添加标签#

标签可以与文本小部件中的一个或多个范围的文本关联。如前所述,范围通过索引指定。单个索引表示一个字符,一对索引表示从起始字符到结束字符之前的范围。使用tag_add方法将标签添加到文本范围中。

text.tag_add('highlightline', '5.0', '6.0')

也可以在首次插入文本时提供标签。insert 方法支持一个可选参数,其中包含要添加到插入文本中的一或多个标签列表。

text.insert('end', 'new material to insert', ('highlightline', 'recent', 'warning'))

随着小部件内容的修改(无论是用户还是程序),标签将自动调整。例如,如果我们给文本“the quick brown fox”打上“nounphrase”标签,然后将“quick”替换为“speedy”,标签仍然适用于整个短语。

将格式应用于标签#

通过配置选项将格式应用于标签;这些选项的工作方式类似于对整个小部件的配置选项。例如:

text.tag_configure('highlightline', background='yellow', font='TkFixedFont', relief='raised')

标签支持以下配置选项:background, bgstipple, borderwidth, elide, fgstipple, font, foreground, justify, lmargin1, lmargin2, offset, overstrike, relief, rmargin, spacing1, spacing2, spacing3, tabs, tabstyle, underline, 和 wrap。请查阅参考手册以获取这些选项的详细描述。tag_cget tag option方法允许我们查询标签的配置选项。

由于多个标签可以应用于同一范围的文本,因此可能存在冲突(例如,两个标签指定不同的字体)。使用优先级顺序来解决这些冲突;最近创建的标签具有最高优先级,但可以使用tag_raise tag和tag_lower tag方法重新排列优先级。

更多标签操作#

要完全删除一个或多个标签,可以使用tag_delete tags方法。这也当然会删除文本中对该标签的任何引用。我们还可以使用tag_remove tag start ?end?方法从文本范围中移除标签。即使这样没有剩余任何带有该标签的文本范围,标签对象本身仍然存在。

tag_ranges tag方法将返回文本中应用了该标签的范围列表。还有tag_nextrange tag start ?end?和tag_prevrange tag start ?end?方法,用于从给定位置向前或向后搜索第一个这样的范围。

tag_names ?idx?方法在不传递额外参数的情况下将返回当前定义在文本小部件中的所有标签列表(包括可能未使用的标签)。如果传递一个索引,它将返回仅应用于该索引处字符的标签列表。

最后,我们可以使用文本中具有给定标签的第一个和最后一个字符作为索引,就像我们可以使用“end”或“2.5”一样。为此,只需指定tagname.first或tagname.last。

画布与文本小部件中标签的差异#

画布和文本小部件都支持“标签”,这些标签可以应用于多个对象、对它们进行样式设置等。然而,画布和文本的标签并不相同,存在显著的差异需要注意。

在画布小部件中,只有单独的画布项具有控制其外观的配置选项。当我们在画布中提及一个标签时,其含义等同于“当前拥有该标签的所有画布项”。标签本身并不作为独立的对象存在。因此,在以下代码段中,最后添加的矩形不会变成红色。

canvas.itemconfigure('important', fill='red')
canvas.create_rectangle(10, 10, 40, 40, tags=('important'))

相比之下,在文本小部件中,不是单独的字符保留关于外观的状态信息,而是标签,它们是独立的对象。因此,在以下代码段中,新添加的文本会变成红色。

text.insert('end', 'first text', ('important'))
text.tag_configure('important', foreground='red')
text.insert('end', 'second text', ('important'))

事件与绑定#

一个非常酷的功能是,我们可以在标签上定义事件绑定。这使我们能够轻松实现诸如识别特定文本范围上的鼠标点击并弹出菜单或对话框等功能。不同的标签可以有不同的绑定。这省去了解决诸如“这个位置的点击代表什么?”这样的问题的麻烦。标签上的绑定通过tag_bind方法实现:

text.tag_bind('important', '<1>', popupImportantMenu)

小部件范围内的事件绑定继续像对待其他所有小部件一样工作,例如,捕获文本中任何位置的鼠标点击。除了普通的低级事件外,文本小部件在内容发生更改时会生成一个<<Modified>>虚拟事件,以及在选择的文本发生变化时生成一个<<Selection>>虚拟事件。

选择文本#

我们可以识别用户选择的文本范围(如果有)。例如,编辑器可能有一个工具栏按钮来加粗所选文本。虽然你可以通过<<Selection>>虚拟事件知道选择是否发生了变化(例如,更新加粗按钮是否激活),但这并没有告诉你选择了什么。

文本小部件自动维护一个名为sel的标签,它指的是所选文本。每当选择发生变化时,sel标签都会更新。因此,我们可以使用tag_ranges ?tag?方法找到所选文本的范围,将sel作为标签传递给它以报告。

同样,我们可以通过使用tag_add设置新的选择或使用tag_remove删除选择来改变选择。然而,sel标签不能被删除。

备注

尽管默认的小部件绑定防止这种情况发生,但sel就像其他任何标签一样,它可以支持多个范围,即不连续的选择。为了防止这种情况发生,当你从代码中更改选择时,确保在添加新选择之前移除任何旧选择。

文本小部件管理插入光标的概念(新输入的文本将出现的位置)与选择分开。它通过一个新的概念称为标记来实现这一点。

标记#

标记在文本中指示特定位置。在这方面,它们类似于索引。然而,随着文本的修改,标记会调整以保持相同的相对位置。这样,它们类似于标签,但指的是单个位置而非一段文本范围。实际上,标记并不指文本中字符占据的位置,而是指定两个字符之间的位置。

Tk自动维护两种不同的标记。第一种名为insert,是插入光标的当前位置。随着光标移动(通过鼠标或键盘),标记随之移动。第二种标记名为current,跟踪当前鼠标位置下的字符位置。

要创建自己的标记,使用widget的mark_set name idx方法,传递标记的名称和一个索引(标记位于给定索引处的字符之前)。这也用于将现有标记移动到不同位置。使用mark_unset name方法可以移除标记,传递标记的名称。如果你删除包含标记的文本范围,也会移除该标记。

标记的名称也可以作为索引使用(与1.0或end-1c等索引相同)。你可以使用mark_next idx或mark_previous idx方法从文本中的给定索引找到下一个(或上一个)标记。mark_names方法将返回所有标记名称的列表。

标记还具有引力,可以使用mark_gravity name ?direction?方法进行修改,这会影响在标记处插入文本时的情况。假设我们有文本"ac",中间有一个我们用竖线符号表示的标记,即"a|c"。如果该标记的引力是右(默认值),则标记附着于"c"。如果在标记处插入新文本"b",则标记将继续附着于"c",因此新文本将被插入到标记之前,即"ab|c"。如果引力是左,则标记附着于"a",因此新文本将被插入到标记之后,即"a|bc"。

图像和微件#

与画布微件类似,文本微件可以包含图像和其他任何Tk微件(包括包含许多其他微件的框架)。从某种意义上说,这允许文本微件本身充当几何管理器。在文本中添加图像和微件的能力为您的程序打开了一个充满可能性的世界。

图像被添加到文本微件中的特定索引处,图像被指定为现有的Tk图像。其他选项允许您微调填充等。

flowers = PhotoImage(file='flowers.gif')
text.image_create('sel.first', image=flowers)

以相同的方式将其他微件添加到文本微件中。要添加的微件必须是文本微件在微件层次结构中的后代。

b = ttk.Button(text, text='Push Me')
text.window_create('1.0', window=b)

更多功能#

文本微件可以做更多的事情。在这里,我们简要提及其中的一些。有关这些功能的详细信息,请参阅参考手册。

搜索#

文本微件包含一个强大的搜索方法,用于在微件内定位一段文本。这对于“查找”对话框是一个明显的例子。您可以从特定位置向前或向后搜索,或者在给定范围内进行搜索,使用精确文本、不区分大小写或通过正则表达式指定搜索词,找到搜索词的一次或所有出现等。

修改、撤销和重做#

文本微件会跟踪是否已进行了更改(例如,知道是否需要保存到文件中很有用)。我们可以使用edit_modified?bool?方法查询(或更改)。还有一个完整的多级撤销/重做机制,当我们将其撤销配置选项设置为true时,该机制由微件自动管理。调用edit_undo或edit_redo会根据存储在撤销/重做堆栈上的信息修改当前文本。

省略文本#

文本小部件可以包含不显示的文本,这被称为“省略”文本,通过标签的elide配置选项使其可用。它可以用来实现大纲视图、可折叠的代码编辑器,甚至用来在显示的文本中嵌入额外的元数据。当指定省略文本内的位置时,需要更加小心。处理位置的方法有额外选项,可以选择包括或忽略省略文本。

自省#

像大多数Tk小部件一样,文本小部件也努力展示其内部状态的信息。我们已经看到这一点,例如get方法、小部件配置选项、标签和标记的名称以及cget等。还有更多信息可供使用,这对各种任务都非常有用。请查阅参考手册中的debug、dlineinfo、bbox、count和dump方法。

对视#

Tk文本小部件允许同一个底层文本数据结构(包含所有文本、标记、标签、图像等)在两个或多个不同的文本小部件之间共享。这被称为对视,并通过peer方法控制。