Text
A text widget manages a multi-line text area. Like the canvas widget, Tk's text widget is an immensely flexible and powerful tool that can be used for a wide variety of tasks. It can provide a simple multi-line text area as part of a form. But text widgets can also form the basis for a stylized code editor, an outliner, a web browser, and much more.
Note: Text widgets are part of the classic Tk widgets, not the themed Tk widgets.
Text widgets.
While we briefly introduced text widgets in an earlier chapter, we'll go into more detail here. You'll get a better sense of the level of sophistication they allow. Still, if you plan to do any significant work with the text widget, the reference manual is a well-organized, helpful, and highly-recommended read.
Text widgets are created using the Text
class:
text = Text(parent, width=40, height=10)
Text widgets are created using the tk::text
command:
tk::text .text -width 40 -height 10
Text widgets are created using the TkText
class:
text = TkText.new(parent) {width 40; height 10}
new_tk__text
method, a.k.a. Tkx::tk__text
:
$text = $parent->new_tk__text(-width => 40, -height => 10);
You'll often provide a width (in characters) and height (in lines). As always, you can ask the geometry manager to expand it to fill the available space in the window.
The Basics
If you simply need a multi-line text field for a form, there are only a few things to worry about: create and size the widget (check), provide an initial value, and retrieve the text after a user has submitted the form.
Providing Initial Content
Text widgets start with nothing in them, so we'll need to add any initial content ourselves.
Because text widgets can hold a lot more than plain text, a simple mechanism (like the entry widget's
textvariable
configuration option) isn't sufficient.
Instead, we'll use the widget's insert
method:
text.insert('1.0', 'here is my\ntext to insert')
.text insert 1.0 "here is my\ntext to insert"
text.insert(1.0, "here is my\ntext to insert")
$text->insert("1.0", "here is my\ntext to insert");
The "1.0" here is the position where to insert the text, and can be read as "line 1, character 0". This refers to the first character of the first line. Historically, especially on Unix, programmers tend to think about line numbers as 1-based and character positions as 0-based.
The text to insert is just a string. Because the widget can hold multi-line text, the string we
supply can be multi-line as well. To do this, simply embed \n
(newline) characters in the
string at the appropriate locations.
Retrieving the Text
After users have made any changes and submitted the form (for example), your program can
retrieve the contents of the widget via the get
method:
thetext = text.get('1.0', 'end')
set thetext [.text get 1.0 end]
thetext = t.get("1.0", 'end')
$thetext = $text->get("1.0", "end");
The two parameters are the start and end position; end
has the obvious meaning.
You can provide different start and end positions if you want to obtain only part of the text.
You'll see more on positions shortly.
Customizing Appearance
We previously saw the width
and height
configuration options for text widgets.
Several other options control its appearance. The most useful are:
foreground
:- color to draw the text in
background
:- background color of the widget
padx, pady
:- extra padding along the inside border of the widget
borderwidth
:- width of the border around widget
relief
:- border style:
flat
,raised
,sunken
,solid
,ridge
,groove
Wrapping and Scrolling
What if some lines of text in the widget are very long, longer than the width of the widget? By default,
the text wraps around to the next line. This behavior can be changed with the
wrap
configuration option. It defaults to char
, meaning wrap lines at any character.
Other options are word
to wrap lines only at
word breaks (e.g., spaces), and none
meaning to not wrap lines at all. In the latter case,
some text of longer lines won't be visible unless we attach a horizontal scrollbar to the widget.
(Users can also scroll through the text using arrow keys, even if scrollbars aren't present).
Both horizontal and vertical scrollbars can be attached to the text widget in the same way as with other widgets, e.g., canvas, listbox.
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)
tk::text .t -width 40 -height 5 -wrap none -yscrollcommand ".ys set" -xscrollcommand ".xs set"
ttk::scrollbar .ys -orient vertical -command ".t yview"
ttk::scrollbar .xs -orient horizontal -command ".t xview"
.t insert end "Lorem ipsum...\n...\n... "
grid .t -column 0 -row 0 -sticky nwes
grid .xs -column 0 -row 1 -sticky we
grid .ys -column 1 -row 0 -sticky ns
grid columnconfigure . 0 -weight 1
grid rowconfigure . 0 -weight 1
t = TkText.new(root) {width 40; height 5; wrap "none"}
ys = Tk::Tile::Scrollbar.new(root) {orient 'vertical'; command proc{|*args| t.yview(*args);}}
xs = Tk::Tile::Scrollbar.new(root) {orient 'horizontal'; command proc{|*args| t.xview(*args);}}
t['yscrollcommand'] = proc{|*args| ys.set(*args);}
t['xscrollcommand'] = proc{|*args| xs.set(*args);}
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')
TkGrid.columnconfigure(root, 0, :weight => 1)
TkGrid.rowconfigure(root, 0, :weight => 1)
$t = $mw->new_tk__text(-width => 40, -height => 5, -wrap => 'none');
$ys = $mw->new_ttk__scrollbar(-orient => 'vertical', -command => [$t, "yview"]);
$xs = $mw->new_ttk__scrollbar(-orient => 'horizontal', -command => [$t, "xview"]);
$t->configure(-yscrollcommand => [$ys, 'set']);
$t->configure(-xscrollcommand => [$xs, 'set']);
$t->insert('end', "Lorem ipsum...\n...\n... ");
$t->g_grid(-column => 0, -row => 0, -sticky => 'nwes');
$xs->g_grid(-column => 0, -row => 1, -sticky => 'we');
$ys->g_grid(-column => 1, -row => 0, -sticky => 'ns');
$mw->g_grid_columnconfigure(0, -weight => 1);
$mw->g_grid_rowconfigure(0, -weight => 1);
We can also ask the widget to ensure that a certain part of the text is visible. For example, let's say
we've added more text to the widget than will fit onscreen (so it will scroll). However, we want to ensure
that the top of the text rather than the bottom is visible. We can use the see
method.
text.see('1.0')
.text see 1.0
text.see('1.0')
$text->see("1.0");
Disabling the Widget
Some forms will temporarily disable editing in particular widgets unless certain conditions are met
(e.g., some other options are set to a certain value). To prevent users from changing
a text widget, set the state
configuration option to disabled
. Re-enable editing
by setting this option back to normal
.
text['state'] = 'disabled'
.text configure -state disabled
text['state'] = 'disabled'
$text->configure(-state => "disabled");
As text widgets are part of the classic widgets, the usual state
and instate
methods are not available.
Modifying the Text in Code
While users can modify the text in the text widget interactively, your program can also make
changes. Adding text is done with the insert
method, which we used above to provide an
initial value for the text widget.
Text Positions and Indices
When we specified a position of 1.0
(first line, first character), this was an example of an index.
It tells the insert
method where to put the new text (just before the first line, first character, i.e., at the
very start of the widget). Indices can be specified in a variety of ways. We used another one with the get
method:
end
means just past the end of the text. (Why "just past?" Text is inserted right
before the given index, so inserting at end
will add text to the end of the widget).
Note that Tk will always add a newline at the very end of the text widget.
Here are a few additional examples of indices and how to interpret them:
3.end
:- The newline at the end of line 3.
1.0 + 3 chars
:- Three characters past the start of line 1.
2.end -1 chars
:- The last character before the new line in line 2.
end -1 chars
:- The newline that Tk always adds at the end of the text.
end -2 chars
:- The actual last character of the text.
end -1 lines
:- The start of the last actual line of text.
2.2 + 2 lines
:- The third character (index 2) of the fourth line of text.
2.5 linestart
:- The first character of line 2.
2.5 lineend
:- The position of the newline at the end of line 2.
2.5 wordstart
:- First char. of the word with the char. at index 2.5.
2.5 wordend
:- First char. after the word with the char. at index 2.5.
Some additional things to keep in mind:
- The term
chars
can be abbreviated asc
, andlines
asl
. - Spaces between terms can be omitted, e.g.,
1.0+3c
. - An index past the end of the text (e.g.,
end + 100c
) is interpreted asend
. - Indices wrap to subsequent lines as needed; e.g.,
1.0 + 10 chars
on a line with only five characters will refer to a position on the second line. - Line numbers in indices are interpreted as logical lines, i.e., each line ends only at the "\n." With long lines and wrapping enabled, one logical line may represent multiple display lines. If you'd like to move up or down a single line on the display, you can specify this as, e.g., "1.0 + 2 display lines".
- When indices contain multiple words, make sure they are quoted appropriately so that Tk sees the entire index as one argument.
To determine the canonical position of an index, use the index idx
method. Pass it any
index expression, and it returns the corresponding index in the form line.char
. For example,
to find the position of the last character (ignoring the automatic newline at the end), use:
text.index('end')
.text index end
text.index('end')
$text->index("end")
You can compare two indices using the compare
method, which lets you check for equality,
whether one index is later in the text than the other, etc.
if text.compare(idx1, "==", idx2"): # same position
if {[.text compare idx1 "==" idx2]} { # same position }
if text.compare(idx1, "==", idx2) then # same position
if ($text->compare(idx1, "==", idx2)) { # same position }
Valid operators are ==
, !=
, <
, <=
, >
, and >=
.
Deleting Text
While the insert
method adds new text anywhere in the widget, the delete start ?end?
method removes it.
We can delete either a single character (specified by index) or a range of characters (specified by
start and end indices). In the latter case, characters from (and including) the start index until just before
the end index are deleted (the character at the end index is not deleted). So if we assume for each of these
we start off with "abcd\nefgh"
in the text widget:
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"
.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"
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"
$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"
There is also a replace
method that performs a delete
followed by an insert
at the same location.
Example: Logging Window
Here's a short example using a text widget as an 80x24 logging window for an application. Users don't edit the text widget at all. Instead, the program writes log messages to it. We'd like to display more than 24 lines (so no scrolling). If the log is full, old messages are removed from the top before new ones are added at the end.
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'
package require Tk
grid [text .log -state disabled -width 80 -height 24 -wrap none]
proc writeToLog {msg} {
set numlines [lindex [split [.log index "end - 1 line"] "."] 0]
.log configure -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 configure -state disabled
}
require 'tk'
root = TkRoot.new
@log = TkText.new(root) {state 'disabled';width 80;height 24;wrap 'none'}.grid
def writeToLog(msg)
numlines = @log.index("end - 1 line").split('.')[0].to_i
@log['state'] = :normal
@log.delete(1.0, 2.0) if numlines==24
@log.insert('end', "\n") unless @log.index('end-1c')=='1.0'
@log.insert('end', msg)
@log['state'] = :disabled
end
use Tkx;
$mw = Tkx::widget->new(".");
$log = $mw->new_tk__text(-state => "disabled", -width => 80, -height => 24, -wrap => "none");
$log->g_grid;
sub writeToLog {
my ($msg) = @_;
$numlines = $log->index("end - 1 line");
print $numlines . "\n";
$log->configure(-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->configure(-state => "disabled");
}
Note that because the program placed the widget in a disabled state, we had to re-enable it to make any changes, even from our program.
Formatting with Tags
So far, we've used text widgets when all the text is in a single font. Now it's time to add formatting like bold, italic, strikethrough, background colors, font sizes, and much more. Tk's text widget implements these using a feature called tags.
Tags are objects associated with the text widget. Each tag is referred to via a name chosen by the programmer. Each tag has several configuration options. These are things like fonts and colors that control formatting. Though tags are objects having state, they don't need to be explicitly created but are automatically created the first time the tag name is used.
Adding Tags to Text
Tags can be associated with one or more ranges of text in the widget. As before, ranges are specified via indices.
A single index represents a single character, and a pair of indices represent a range from the start
character to just before the end character. Tags are added to a range of text using the
tag_add
method.
text.tag_add('highlightline', '5.0', '6.0')
.text tag add highlightline 5.0 6.0
text.tag_add('highlightline', 5.0, 6.0)
$text->tag_add("highlightline", "5.0", "6.0");
Tags can also be provided when first inserting text. The insert
method supports an optional parameter
containing a list of one or more tags to add to the text being inserted.
text.insert('end', 'new material to insert', ('highlightline', 'recent', 'warning'))
.text insert end "new material to insert" "highlightline recent warning"
text.insert('end', 'new material to insert', 'highlightline recent warning')
$text->insert_end("new material to insert", "highlightline recent warning");
As the widget's contents are modified (whether by a user or your program), the tags will adjust automatically. For example, if we tagged the text "the quick brown fox" with the tag "nounphrase", and then replaced the word "quick" with "speedy," the tag still applies to the entire phrase.
Applying Formatting to Tags
Formatting is applied to tags via configuration options; these work similarly to configuration options for the entire widget. As an example:
text.tag_configure('highlightline', background='yellow', font='TkFixedFont', relief='raised')
.text tag configure highlightline -background yellow -font "TkFixedFont" -relief raised
text.tag_configure('highlightline', :background=>'yellow', :font=>'TkFixedFont', :relief=>'raised')
$text->tag_configure("highlightline", -background => "yellow", -font => "TkFixedFont", -relief => "raised");
Tags support the following configuration options: background
, bgstipple
, borderwidth
,
elide
, fgstipple
, font
, foreground
, justify
, lmargin1
, lmargin2
,
offset
, overstrike
, relief
, rmargin
, spacing1
, spacing2
,
spacing3
, tabs
, tabstyle
, underline
, and wrap
.
Check the reference manual for detailed descriptions of these. The tag_cget tag option
method allows us to query the configuration options of a tag.
Because multiple tags can apply to the same range of text, there is the possibility of conflict (e.g., two tags
specifying different fonts). A priority order is used to resolve these; the most recently created tags have
the highest priority, but priorities can be rearranged using the tag_raise tag
and tag_lower tag
methods.
More Tag Manipulations
To delete one or more tags altogether, we can use the tag_delete tags
method. This also, of course,
removes any references to the tag in the text. We can also remove a tag from a range of text using the
tag_remove tag start ?end?
method.
Even if that leaves no ranges of text with that tag, the tag object itself still exists.
The tag_ranges tag
method will return a list of ranges in the text that the tag has been applied to. There
are also tag_nextrange tag start ?end?
and tag_prevrange tag start ?end?
methods to
search forward or backward for the first such range from a given position.
The tag_names ?idx?
method, called with no additional parameters, will return a list of all tags currently
defined in the text widget (including those that may not be presently used). If we pass the method an index,
it will return the list of tags applied to just the character at the index.
Finally, we can use the first and last characters in the text having a given tag as indices, the same way we
can use "end" or "2.5". To do so, just specify tagname.first
or tagname.last
.
Differences between Tags in Canvas and Text Widgets
Both canvas and text widgets support "tags" that can be applied to several objects, style them, etc. However, canvas and text tags are not the same and there are substantial differences to take note of.
In canvas widgets, only individual canvas items have configuration options that control their appearance. When we refer to a tag in a canvas, the meaning of that is identical to "all canvas items presently having that tag." The tag itself doesn't exist as a separate object. So in the following snippet, the last rectangle added will not be colored red.
canvas.itemconfigure('important', fill='red')
canvas.create_rectangle(10, 10, 40, 40, tags=('important'))
.canvas itemconfigure important -fill red
.canvas create rectangle 10 10 40 40 -tags important
canvas.itemconfigure('important', :fill => 'red')
TkcRectangle.new(canvas, 10, 10, 40, 40, :tags => 'important')
$canvas->itemconfigure("important", -fill => "red");
$canvas->create_rectangle(10, 10, 40, 40, -tags => "important");
In contrast, with text widgets, it's not the individual characters that retain the state information about appearance, but tags, which are objects in their own right. So in this snippet, the newly added text will be colored red.
text.insert('end', 'first text', ('important'))
text.tag_configure('important', foreground='red')
text.insert('end', 'second text', ('important'))
.text insert end "first text" important
.text tag configure important -foreground red
.text insert end "second text" important
text.insert('end', 'first text', 'important')
text.tag_configure('important', :foreground => 'red')
text.insert('end', 'second text', 'important')
$text->insert_end("first text", "important");
$text->tag_configure("important", -foreground => "red");
$text->insert_end("second text", "important");
Events and Bindings
One very cool thing we can do is define event bindings on tags. That allows us to easily do things like
recognize mouse clicks on particular ranges of text and popup a menu or dialog in response. Different
tags can have different bindings. This saves the hassle of sorting out questions like "what does a click
at this location mean?". Bindings on tags are implemented using the tag_bind
method:
text.tag_bind('important', '<1>', popupImportantMenu)
.text tag bind important <1> "popupImportantMenu"
text.tag_bind('important', '1', proc{popupImportantMenu})
$text->tag_bind("important", "<1>", sub{popupImportantMenu});
Widget-wide event bindings continue to work as they do for every other widget, e.g., to capture a mouse click
anywhere in the text. Besides the normal low-level events, the text widget generates a
<<Modified>>
virtual event whenever a change is made to the
content of the widget, and a <<Selection>>
virtual event
whenever there is a change made to which text is selected.
Selecting Text
We can identify the range of text selected by a user, if any.
For example, an editor may have a toolbar button to bold the selected text. While you can tell when
the selection has changed (e.g., to update whether or not the bold button is active) via the <<Selection>>
virtual event, that doesn't tell you what has been selected.
The text widget automatically maintains a tag named sel
, which refers to the selected text. Whenever the
selection changes, the sel
tag will be updated. So we can find the range of text selected using the
tag_ranges ?tag?
method, passing it sel
as the tag to report on.
Similarly, we can change the selection by using tag_add
to set a new selection, or tag_remove
to remove the selection. The sel
tag cannot be deleted, however.
Though the default widget bindings prevent this from happening, sel
is like any other tag in that it can support
multiple ranges, i.e., disjoint selections. To prevent this from happening, when changing the selection from your
code, make sure to remove any old selection before adding a new one.
The text widget manages the concept of the insertion cursor (where newly typed text will appear) separate from the selection. It does so using a new concept called a mark.
Marks
Marks indicate a particular place in the text. In that respect, they are like indices. However, as the text is modified, the mark will adjust to be in the same relative location. In that way, they resemble tags but refer to a single position rather than a range of text. Marks actually don't refer to a position occupied by a character in the text but specify a position between two characters.
Tk automatically maintains two different marks. The first, named insert
, is the present location of the
insertion cursor. As the cursor is moved (via mouse or keyboard), the mark moves with it. The second mark,
named current
, tracks the position of the character underneath the current mouse position.
To create your own marks, use the widget's mark_set name idx
method, passing it the name of the mark and an index
(the mark is positioned just before the character at the given index). This is also used to move an existing mark to
a different position. Marks can be removed using the mark_unset name
method, passing it the name of the mark.
If you delete a range of text containing a mark, that also removes the mark.
The name of a mark can also be used as an index (in the same way 1.0
or end-1c
are indices).
You can find the next mark (or previous one) from a given index in the text using the mark_next idx
or
mark_previous idx
methods. The mark_names
method will return a list of the names of all marks.
Marks also have a gravity, which can be modified with the mark_gravity name ?direction?
method,
which affects what happens when text is inserted at the mark. Suppose we have the text "ac" with a mark in between that
we'll symbolize with a pipe, i.e., "a|c." If the gravity of that mark is right
(the default), the mark
attaches itself to the "c." If the new text "b" is inserted at the mark, the mark will remain stuck to the "c,"
and so the new text will be inserted before the mark, i.e., "ab|c." If the gravity is instead left
, the mark
attaches itself to the "a," and so new text will be inserted after the mark, i.e., "a|bc."
Images and Widgets
Like canvas widgets, text widgets can contain images and any other Tk widgets (including frames containing many other widgets). In a sense, this allows the text widget to work as a geometry manager in its own right. The ability to add images and widgets within the text opens up a world of possibilities for your program.
Images are added to a text widget at a particular index, with the image specified as an existing Tk image. Other options that allow you to fine-tune padding, etc.
flowers = PhotoImage(file='flowers.gif')
text.image_create('sel.first', image=flowers)
image create photo flowers -file flowers.gif
.text image create sel.first -image flowers
flowers = TkPhotoImage.new(:file => 'flowers.gif')
TkTextImage.new(text, 'sel.first', :image => flowers)
Tkx::image_create_photo( "flowers", -file => "flowers.gif");
$text->image_create("sel.first", -image => "flowers");
Other widgets are added to a text widget in much the same way as images. The widget being added must be a descendant of the text widget in the widget hierarchy.
b = ttk.Button(text, text='Push Me')
text.window_create('1.0', window=b)
ttk::button .text.b -text "Push Me"
.text window create 1.0 -window .text.b
b = Tk::Tile::Button.new(text) {text 'Push Me'}
TkTextWindow.new(text, 1.0, :window => b)
$b = $text->new_ttk__button(-text => "Push Me");
$text->window_create("1.0", -window => $b);
Even More
Text widgets can do many more things. Here, we'll briefly mention just a few more of them. For details on any of these, see the reference manual.
Search
The text widget includes a powerful search
method to locate a piece of text within the
widget. This is useful for a "Find" dialog, as one obvious example. You can search backward or forward from a
particular position or within a given range, specify the search term using exact text, case insensitive, or via
regular expressions, find one or all occurrences of the search term, etc.
Modifications, Undo and Redo
The text widget keeps track of whether changes have been made (useful to know whether it needs to be
saved to a file, for example). We can query (or change) using the edit_modified ?bool?
method. There is also
a complete multi-level undo/redo mechanism, managed automatically by the widget when we set its undo
configuration option to true
. Calling edit_undo
or edit_redo
modifies the current
text using information stored on the undo/redo stack.
Eliding Text
Text widgets can include text that is not displayed. This is known as "elided" text, and is made
available using the elide
configuration option for tags. It can be used to implement
an outliner, a "folding" code editor, or even to bury extra meta-data intermixed with displayed text.
When specifying positions within elided text, you have to be a bit more careful. Methods that work with
positions have extra options to either include or ignore the elided text.
Introspection
Like most Tk widgets, the text widget goes out of its way to expose information about its internal state. We've
seen this in terms of the get
method, widget configuration options, names
and
cget
for both tags and marks, etc. There is even more information available that can be useful
for a wide variety of tasks. Check out the debug
, dlineinfo
, bbox
,
count
, and dump
methods in the reference manual.
Peering
The Tk text widget allows the same underlying text data structure (containing all the text, marks, tags, images, etc.) to be
shared between two or more different text widgets. This is known as peering and is controlled via the
peer
method.
Spotted a mistake? Couldn't find what you were looking for? Suggestions? Let me know!
If you've found this tutorial useful, please check out Modern Tkinter.