Example 9-11
puts some
of these concepts to work. It extends
Example 9-10
to add support for
four common text-editing operations—file save, text cut and paste, and
string find searching—by subclassingScrolledText
to
provide additional buttons and methods. TheText
widget comes with a set of default
keyboard bindings that perform some common editing operations, too, but
they might not be what is expected on every platform; it’s more common
and user friendly to provide GUI interfaces to editing operations in a
GUI text editor.
Example 9-11. PP4E\Gui\Tour\simpleedit.py
"""
add common edit tools to ScrolledText by inheritance;
composition (embedding) would work just as well here;
this is not robust!--see PyEdit for a feature superset;
"""
from tkinter import *
from tkinter.simpledialog import askstring
from tkinter.filedialog 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__':
if len(sys.argv) > 1:
SimpleEditor(file=sys.argv[1]).mainloop() # filename on command line
else:
SimpleEditor().mainloop() # or not: start empty
This, too, was written with one eye toward reuse—theSimpleEditor
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 9-19
(shown editing itself and running on
Windows); index positions are printed onstdout
after each successful find
operation—
here, for two “def” finds, with
prior selection removal logic commented-out in the script (uncomment
this line in the script to get single-selection behavior for
finds):
C:\...\PP4E\Gui\Tour>python simpleedit.py simpleedit.py
PP4E scrolledtext
14.4
25.4
Figure 9-19. 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 9-20
shows this dialog in
action on Windows 7. Find operations also pop up a standard dialog box
to input a search string (
Figure 9-21
); in a
full-blown editor, you might want to save this string away to repeat the
find again (we will, in
Chapter 11
’s more
full-featured PyEdit example). Quit operations reuse the verifying Quit
button component we coded in
Chapter 8
yet again; code reuse means
never having to say you’re quitting without warning…
Figure 9-20. Save pop-up dialog on Windows
Figure 9-21. Find pop-up dialog
BesidesText
widget
operations,
Example 9-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 temporarily—deleted 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 aSimpleEditor
window,
and text cut inSimpleEditor
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 theText
widget—it can also be used to cut and
paste graphical objects in theCanvas
widget (discussed next).
As used in the script of
Example 9-11
, 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. TheCLIPBOARD
selection used by this script is
available on all platforms (aPRIMARY
selection is also available, but it
is only generally useful on X Windows, so we’ll ignore it here).
Notice that the clipboardselection_get
call throws aTclError
exception if it fails; this script
simply ignores it and abandons a paste request, but we’ll do better
later.
As coded,SimpleEditor
uses inheritance to extendScrolledText
with extra buttons and callback methods. As we’ve seen,
it’s also reasonable to attach (embed) GUI objects coded as
components, such asScrolledText
.
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
attachScrolledText
toSimpleEditor
with changed lines in bold font
(see the file
simpleedit2.py
in the book’s
examples distribution for a complete composition implementation). It’s
mostly a matter of passing in the right parents and adding an extrast
attribute name anytime you need
to get to theText
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...
This code doesn’t need to subclassFrame
necessarily (it could add widgets to
the passed-in parent directly), but being a frame allows the full
package here to be embedded and configured as well. 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.
Finally, before you change your system registry to
makeSimpleEditor
your
default text file viewer, I should mention that although it shows the
basics, it’s something of a stripped-down version (really, a
prototype) of the PyEdit example we’ll meet in
Chapter 11
. In fact, you may wish to 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, external files search, and more. Because theText
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 thatSimpleEditor
not only is limited in
function, but also is just plain careless—many boundary cases go
unchecked and trigger uncaught exceptions that don’t kill the GUI, but
are not handled or reported well. 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 inSimpleEditor
.