Section 10.4. Text


10.4. Text

It's been said that Tkinter's strongest points may be its Text and Canvas widgets. Both provide a remarkable amount of functionality. For instance, the Tkinter Text widget was powerful enough to implement the web pages of Grail, an experimental web browser coded in Python; Text supports complex font-style settings, embedded images, unlimited undo and redo, and much more. The Tkinter Canvas widget, a general-purpose drawing device, allows for efficient free-form graphics and has been the basis of sophisticated image processing and visualization applications.

In Chapter 12, we'll put these two widgets to use to implement text editors (PyEdit), paint programs (PyDraw), clock GUIs (PyClock), and photo slideshows (PyView). For the purposes of this tour chapter, though, let's start out using these widgets in simpler ways. Example 10-10 implements a simple scrolled-text display, which knows how to fill its display with a text string or file.

Example 10-10. PP3E\Gui\Tour\scrolledtext.py

 # a simple text or file viewer component print 'PP3E scrolledtext' from Tkinter import * class ScrolledText(Frame):     def _ _init_ _(self, parent=None, text='', file=None):         Frame._ _init_ _(self, parent)         self.pack(expand=YES, fill=BOTH)                 # make me expandable         self.makewidgets( )         self.settext(text, file)     def makewidgets(self):         sbar = Scrollbar(self)         text = Text(self, relief=SUNKEN)         sbar.config(command=text.yview)                  # xlink sbar and text         text.config(yscrollcommand=sbar.set)             # move one moves other         sbar.pack(side=RIGHT, fill=Y)                    # pack first=clip last         text.pack(side=LEFT, expand=YES, fill=BOTH)      # text clipped first         self.text = text     def settext(self, text='', file=None):         if file:             text = open(file, 'r').read( )         self.text.delete('1.0', END)                     # delete current text         self.text.insert('1.0', text)                    # add at line 1, col 0         self.text.mark_set(INSERT, '1.0')                # set insert cursor         self.text.focus( )                                    # save user a click     def gettext(self):                                   # returns a string         return self.text.get('1.0', END+'-1c')           # first through last if _ _name_ _ == '_ _main_ _':     root = Tk( )     try:         st = ScrolledText(file=sys.argv[1])              # filename on cmdline     except IndexError:         st = ScrolledText(text='Words\ngo here')         # or not: two lines     def show(event): print repr(st.gettext( ))               # show as raw string     root.bind('<Key-Escape>', show)                      # esc = dump text     root.mainloop( ) 

Like the ScrolledList in Example 10-9, the ScrolledText object in this file is designed to be a reusable component, but it can also be run standalone to display text file contents. Also like the last section, this script is careful to pack the scroll bar first so that it is cut out of the display last as the window shrinks, and arranges for the embedded Text object to expand in both directions as the window grows. When run with a filename argument, this script makes the window shown in Figure 10-15; it embeds a Text widget on the left and a cross-linked Scrollbar on the right.

Figure 10-15. scrolledtext in action


Just for fun, I populated the text file displayed in the window with the following code and command lines (and not just because I happen to live near an infamous hotel in Colorado):

 C:\...\PP3E\Gui\Tour>type temp.py f = open('temp.txt', 'w') for i in range(250):     f.write('%03d)  All work and no play makes Jack a dull boy.\n' % i) f.close( ) C:\...\PP3E\Gui\Tour>python temp.py C:\...\PP3E\Gui\Tour>python scrolledtext.py temp.txt PP3E scrolledtext 

To view a file, pass its name on the command lineits text is automatically displayed in the new window. By default, it is shown in a non-fixed-width font, but we'll pass a font option to the Text widget in the next example to change that.

Notice the PP3E scrolledtext message printed when this script runs. Because there is also a ScrolledText.py file in the standard Python distribution with a very different interface, the one here identifies itself when run or imported, so you can tell which one you've got. If the standard one ever goes away, import the one listed here for a simple text browser, and adjust configuration calls to include a .text qualifier level (the library version subclasses Text, not Frame).

10.4.1. Programming the Text Widget

To understand how this script works at all, though, we have to detour into a few Text widget details here. Earlier we met the EnTRy and Message widgets, which address a subset of the Text widget's uses. The Text widget is much richer in both features and interfacesit supports both input and display of multiple lines of text, editing operations for both programs and interactive users, multiple fonts and colors, and much more. Text objects are created, configured, and packed just like any other widget, but they have properties all their own.

10.4.1.1. Text is a Python string

Although the Text widget is a powerful tool, its interface seems to boil down to two core concepts. First, the content of a Text widget is represented as a string in Python scripts, and multiple lines are separated with the normal \n line terminator. The string 'Words\ngo here', for instance, represents two lines when stored in or fetched from a Text widget; it would normally have a trailing \n also, but it doesn't have to.

To help illustrate this point, this script binds the Escape key press to fetch and print the entire contents of the Text widget it embeds:

 C:\...\PP3E\Gui\Tour>python scrolledtext.py PP3E scrolledtext 'Words\ngo here' 'Always look\non the bright\nside of life\n' 

When run with arguments, the script stores a file's contents in the Text widget. When run without arguments, the script stuffs a simple literal string into the widget, displayed by the first Escape press output here (recall that \n is the escape sequence for the line terminator character). The second output here happens when pressing Escape in the shrunken window captured in Figure 10-16.

Figure 10-16. scrolledtext gets a positive outlook


10.4.1.2. String positions

The second key to understanding Text code has to do with the ways you specify a position in the text string. Like the listbox, Text widgets allow you to specify such a position in a variety of ways. In Text, methods that expect a position to be passed in will accept an index, a mark, or a tag reference. Moreover, some special operations are invoked with predefined marks and tagsthe insert cursor is mark INSERT, and the current selection is tag SEL.

10.4.1.2.1. Text indexes

Because it is a multiple-line widget, Text indexes identify both a line and a column. For instance, consider the interfaces of the basic insert, delete, and fetch text operations used by this script:

 self.text.insert('1.0', text)             # insert text at the start self.text.delete('1.0', END)              # delete all current text return self.text.get('1.0', END+'-1c')    # fetch first through last 

In all of these, the first argument is an absolute index that refers to the start of the text string: string '1.0' means row 1, column (rows are numbered from 1 and columns from 0). An index '2.1' refers to the second character in the second row.

Like the listbox, text indexes can also be symbolic names: the END in the preceding delete call refers to the position just past the last character in the text string (it's a Tkinter variable preset to string 'end'). Similarly, the symbolic index INSERT (really, string 'insert') refers to the position immediately after the insert cursorthe place where characters would appear if typed at the keyboard. Symbolic names such as INSERT can also be called marks, described in a moment.

For added precision, you can add simple arithmetic extensions to index strings. The index expression END+'-1c' in the get call in the previous example, for instance, is really the string 'end-1c' and refers to one character back from END. Because END points to just beyond the last character in the text string, this expression refers to the last character itself. The -1c extension effectively strips the trailing \n that this widget adds to its contents (and may add a blank line if saved in a file).

Similar index string extensions let you name characters ahead (+1c), name lines ahead and behind (+2l, -2l), and specify things such as line ends and word starts around an index (lineend, wordstart). Indexes show up in most Text widget calls.

10.4.1.2.2. Text marks

Besides row/column identifier strings, you can also pass positions as names of markssymbolic names for a position between two characters. Unlike absolute row/column positions, marks are virtual locations that move as new text is inserted or deleted (by your script or your user). A mark always refers to its original location, even if that location shifts to a different row and column over time.

To create a mark, call the text mark_set method with a string name and an index to give its logical location. For instance, this script sets the insert cursor at the start of the text initially, with a call like the first one here:

 self.text.mark_set(INSERT, '1.0')             # set insert cursor to start self.text.mark_set('linetwo', '2.0')          # mark current line 2 

The name INSERT is a predefined special mark that identifies the insert cursor position; setting it changes the insert cursor's location. To make a mark of your own, simply provide a unique name as in the second call here and use it anywhere you need to specify a text position. The mark_unset call deletes marks by name.

10.4.1.2.3. Text tags

In addition to absolute indexes and symbolic mark names, the Text widget supports the notion of tagssymbolic names associated with one or more substrings within the Text widget's string. Tags can be used for many things, but they also serve to represent a position anywhere you need one: tagged items are named by their beginning and ending indexes, which can be later passed to position-based calls.

For example, Tkinter provides a built-in tag name, SELa Tkinter name preassigned to string 'sel'which automatically refers to currently selected text. To fetch the text selected (highlighted) with a mouse, run either of these calls:

 text = self.text.get(SEL_FIRST, SEL_LAST)      # use tags for from/to indexes text = self.text.get('sel.first', 'sel.last')  # strings and constants work 

The names SEL_FIRST and SEL_LAST are just preassigned variables in the Tkinter module that refer to the strings used in the second line here. The text get method expects two indexes; to fetch text names by a tag, add .first and .last to the tag's name to get its start and end indexes.

To tag a substring, call the Text widget's tag_add method with a tag name string and start and stop positions (text can also be tagged as added in insert calls). To remove a tag from all characters in a range of text, call tag_remove:

 self.text.tag_add('alltext', '1.0', END)  # tag all text in the widget self.text.tag_add(SEL, index1, index2)    # select from index1 up to index2 self.text.tag_remove(SEL, '1.0', END)     # remove selection from all text 

The first line here creates a new tag that names all text in the widgetfrom start through end positions. The second line adds a range of characters to the built-in SEL selection tagthey are automatically highlighted, because this tag is predefined to configure its members that way. The third line removes all characters in the text string from the SEL tag (all selections are unselected). Note that the tag_remove call just untags text within the named range; to really delete a tag completely, call tag_delete instead.

You can map indexes to tags dynamically too. For example, the text search method returns the row.column index of the first occurrence of a string between start and stop positions. To automatically select the text thus found, simply add its index to the built-in SEL tag:

 where = self.text.search(target, INSERT, END)  # search from insert cursor pastit = where + ('+%dc' % len(target))        # index beyond string found self.text.tag_add(SEL, where, pastit)          # tag and select found string self.text.focus( )                                # select text widget itself 

If you want only one string to be selected, be sure to first run the tag_remove call listed earlierthis code adds a selection in addition to any selections that already exist (it may generate multiple selections in the display). In general, you can add any number of substrings to a tag to process them as a group.

To summarize: indexes, marks, and tag locations can be used anytime you need a text position. For instance, the text see method scrolls the display to make a position visible; it accepts all three kinds of position specifiers:

 self.text.see('1.0')          # scroll display to top self.text.see(INSERT)         # scroll display to insert cursor mark self.text.see(SEL_FIRST)      # scroll display to selection tag 

Text tags can also be used in broader ways for formatting and event bindings, but I'll defer those details until the end of this section.

10.4.2. Adding Text-Editing Operations

Example 10-11 puts some of these concepts to work. It adds support for four common text-editing operationsfile save, text cut and paste, and string find searchingby subclassing ScolledText to provide additional buttons and methods. The Text widget comes with a set of default keyboard bindings that perform some common editing operations too, but they roughly mimic the Unix Emacs editor and are somewhat obscure; it's more common and user friendly to provide GUI interfaces to editing operations in a GUI text editor.

Example 10-11. PP3E\Gui\Tour\simpleedit.py

 ######################################################### # add common edit tools to scrolled text by inheritance; # composition (embedding) would work just as well here; # this is not robust! see PyEdit for a feature superset; ######################################################### from Tkinter import * from tkSimpleDialog import askstring from tkFileDialog   import asksaveasfilename from quitter        import Quitter from scrolledtext   import ScrolledText                   # here, not Python's class SimpleEditor(ScrolledText):                         # see PyEdit for more     def _ _init_ _(self, parent=None, file=None):         frm = Frame(parent)         frm.pack(fill=X)         Button(frm, text='Save',  command=self.onSave).pack(side=LEFT)         Button(frm, text='Cut',   command=self.onCut).pack(side=LEFT)         Button(frm, text='Paste', command=self.onPaste).pack(side=LEFT)         Button(frm, text='Find',  command=self.onFind).pack(side=LEFT)         Quitter(frm).pack(side=LEFT)         ScrolledText._ _init_ _(self, parent, file=file)         self.text.config(font=('courier', 9, 'normal'))     def onSave(self):         filename = asksaveasfilename( )         if filename:             alltext = self.gettext( )                         # first through last             open(filename, 'w').write(alltext)            # store text in file     def onCut(self):         text = self.text.get(SEL_FIRST, SEL_LAST)         # error if no select         self.text.delete(SEL_FIRST, SEL_LAST)             # should wrap in try         self.clipboard_clear( )         self.clipboard_append(text)     def onPaste(self):                                    # add clipboard text         try:             text = self.selection_get(selection='CLIPBOARD')             self.text.insert(INSERT, text)         except TclError:             pass                                          # not to be pasted     def onFind(self):         target = askstring('SimpleEditor', 'Search String?')         if target:             where = self.text.search(target, INSERT, END)  # from insert cursor             if where:                                      # returns an index                 print where                 pastit = where + ('+%dc' % len(target))    # index past target                #self.text.tag_remove(SEL, '1.0', END)      # remove selection                 self.text.tag_add(SEL, where, pastit)      # select found target                 self.text.mark_set(INSERT, pastit)         # set insert mark                 self.text.see(INSERT)                      # scroll display                 self.text.focus( )                             # select text widget if _ _name_ _ == '_ _main_ _':     try:         SimpleEditor(file=sys.argv[1]).mainloop( )    # filename on command line     except IndexError:         SimpleEditor().mainloop( )                    # or not 

This, too, was written with one eye toward reusethe SimpleEditor class it defines could be attached or subclassed by other GUI code. As I'll explain at the end of this section, though, it's not yet as robust as a general-purpose library tool should be. Still, it implements a functional text editor in a small amount of portable code. When run standalone, it brings up the window in Figure 10-17 (shown running in Windows); index positions are printed on stdout after each successful find operation:

 C:\...\PP3E\Gui\Tour>python simpleedit.py simpleedit.py PP3E scrolledtext 14.4 24.4 

Figure 10-17. simpleedit in action


The save operation pops up the common save dialog that is available in Tkinter and is tailored to look native on each platform. Figure 10-18 shows this dialog in action on Windows. Find operations also pop up a standard dialog box to input a search string (Figure 10-19); in a full-blown editor, you might want to save this string away to repeat the find again (we will, in Chapter 12's PyEdit discussion).

Figure 10-18. Save pop-up dialog on Windows


Figure 10-19. Find pop-up dialog


10.4.2.1. Using the clipboard

Besides Text widget operations, Example 10-11 applies the Tkinter clipboard interfaces in its cut-and-paste functions. Together, these operations allow you to move text within a file (cut in one place, paste in another). The clipboard they use is just a place to store data temporarilydeleted text is placed on the clipboard on a cut, and text is inserted from the clipboard on a paste. If we restrict our focus to this program alone, there really is no reason that the text string cut couldn't simply be stored in a Python instance variable. But the clipboard is actually a much larger concept.

The clipboard used by this script is an interface to a system-wide storage space, shared by all programs on your computer. Because of that, it can be used to transfer data between applications, even ones that know nothing of Tkinter. For instance, text cut or copied in a Microsoft Word session can be pasted in a SimpleEditor window, and text cut in SimpleEditor can be pasted in a Microsoft Notepad window (try it). By using the clipboard for cut and paste, SimpleEditor automatically integrates with the window system at large. Moreover, the clipboard is not just for the Text widgetit can also be used to cut and paste graphical objects in the Canvas widget (discussed next).

As used in this script, the basic Tkinter clipboard interface looks like this:

 self.clipboard_clear( )                               # clear the clipboard self.clipboard_append(text)                       # store a text string on it text = self.selection_get(selection='CLIPBOARD')  # fetch contents, if any 

All of these calls are available as methods inherited by all Tkinter widget objects because they are global in nature. The CLIPBOARD selection used by this script is available on all platforms (a PRIMARY selection is also available, but is only generally useful on X Windows, so we'll ignore it here). Notice that the clipboard selection_get call throws a TclError exception if it fails; this script simply ignores it and abandons a paste request, but we'll do better later.

10.4.2.2. Composition versus inheritance

As coded, SimpleEditor uses inheritance to extend ScrolledText with extra buttons and callback methods. As we've seen, it's also reasonable to attach (embed) GUI objects coded as components, such as ScrolledText. The attachment model is usually called composition; some people find it simpler to understand and less prone to name clashes than extension by inheritance.

To give you an idea of the differences between these two approaches, the following sketches the sort of code you would write to attach ScrolledText to SimpleEditor with changed lines in bold font (see the file simpleedit-2.py on the book's examples distribution for a complete composition implementation). It's mostly a matter of passing in the right parents and adding an extra st attribute name to get to the Text widget's methods:

 class SimpleEditor(Frame):      def _ _init_ _(self, parent=None, file=None):         Frame._ _init_ _(self, parent)         self.pack( )         frm = Frame(self)         frm.pack(fill=X)         Button(frm, text='Save',  command=self.onSave).pack(side=LEFT)                                            ...more...         Quitter(frm).pack(side=LEFT)         self.st = ScrolledText(self, file=file)            # attach, not subclass         self.st.text.config(font=('courier', 9, 'normal'))      def onSave(self):         filename = asksaveasfilename( )         if filename:             alltext = self.st.gettext( )                    # go through attribute             open(filename, 'w').write(alltext)     def onCut(self):         text = self.st.text.get(SEL_FIRST, SEL_LAST)          self.st.text.delete(SEL_FIRST, SEL_LAST)                                                ...more... 

The window looks identical when such code is run. I'll let you be the judge of whether composition or inheritance is better here. If you code your Python GUI classes right, they will work under either regime.

10.4.2.3. It's called "Simple" for a reason

Finally, before you change your system registry to make SimpleEditor your default text file viewer, I should mention that although it shows the basics, it's something of a stripped-down version of the PyEdit example we'll meet in Chapter 12. In fact, you should study that example now if you're looking for more complete Tkinter text-processing code in general. There, we'll also use more advanced text operations, such as the undo/redo interface, case-insensitive searches, and more. Because the Text widget is so powerful, it's difficult to demonstrate more of its features without the volume of code that is already listed in the PyEdit program.

I should also point out that SimpleEditor not only is limited in function, but also is just plain carelessmany boundary cases go unchecked and trigger uncaught exceptions that don't kill the GUI, but are not handled or reported. Even errors that are caught are not reported to the user (e.g., a paste, with nothing to be pasted). Be sure to see the PyEdit example for a more robust and complete implementation of the operations introduced in SimpleEditor.

10.4.3. Advanced Text and Tag Operations

Besides position specifiers, text tags can also be used to apply formatting and behavior to all characters in a substring and all substrings added to a tag. In fact, this is where much of the power of the Text widget lies:

  • Tags have formatting attributes for setting color, font, tabs, and line spacing and justification; to apply these to many parts of the text at once, associate them with a tag and apply formatting to the tag with the tag_config method, much like the general config widget we've been using.

  • Tags can also have associated event bindings, which let you implement things such as hyperlinks in a Text widget: clicking the text triggers its tag's event handler. Tag bindings are set with a tag_bind method, much like the general widget bind method we've already met.

With tags, it's possible to display multiple configurations within the same Text widget; for instance, you can apply one font to the Text widget at large and other fonts to tagged text. In addition, the Text widget allows you to embed other widgets at an index (they are treated like a single character), as well as images.

Example 10-12 illustrates the basics of all these advanced tools at once and draws the interface captured in Figure 10-20. This script applies formatting and event bindings to three tagged substrings, displays text in two different font and color schemes, and embeds an image and a button. Double-clicking any of the tagged substrings (or the embedded button) with a mouse triggers an event that prints a "Got tag event" message to stdout.

Figure 10-20. Text tags in action


Example 10-12. PP3E\Gui\Tour\texttags.py

 # demo advanced tag and text interfaces from Tkinter import * root = Tk( ) def hello(event): print 'Got tag event' # make and config a Text text = Text( ) text.config(font=('courier', 15, 'normal'))                  # set font for all text.config(width=20, height=12) text.pack(expand=YES, fill=BOTH) text.insert(END, 'This is\n\nthe meaning\n\nof life.\n\n')   # insert six lines # embed windows and photos btn = Button(text, text='Spam', command=lambda: hello(0))    # embed a button btn.pack( ) text.window_create(END, window=btn)                          # embed a photo text.insert(END, '\n\n') img = PhotoImage(file='../gifs/PythonPowered.gif') text.image_create(END, image=img) # apply tags to substrings text.tag_add('demo', '1.5', '1.7')                       # tag 'is' text.tag_add('demo', '3.0', '3.3')                       # tag 'the' text.tag_add('demo', '5.3', '5.7')                       # tag 'life' text.tag_config('demo', background='purple')             # change colors in tag text.tag_config('demo', foreground='white')              # not called bg/fg here text.tag_config('demo', font=('times', 16, 'underline')) # change font in tag text.tag_bind('demo', '<Double-1>', hello)               # bind events in tag root.mainloop( ) 

Such embedding and tag tools could ultimately be used to render a web page. In fact, Python's standard htmllib HTML parser module can help automate web page GUI construction. As you can probably tell, though, the Text widget offers more GUI programming options than we have space to list here. For more details on tag and text options, consult other Tk and Tkinter references. Right now, art class is about to begin.




Programming Python
Programming Python
ISBN: 0596009259
EAN: 2147483647
Year: 2004
Pages: 270
Authors: Mark Lutz

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net