def start(self):
TextEditor.start(self) # GuiMaker start call
for i in range(len(self.toolBar)): # delete quit in toolbar
if self.toolBar[i][0] == 'Quit': # delete file menu items,
del self.toolBar[i] # or just disable file
break
if self.deleteFile:
for i in range(len(self.menuBar)):
if self.menuBar[i][0] == 'File':
del self.menuBar[i]
break
else:
for (name, key, items) in self.menuBar:
if name == 'File':
items.append([1,2,3,4,6])
################################################################################
# standalone program run
################################################################################
def testPopup():
# see PyView and PyMail for component tests
root = Tk()
TextEditorMainPopup(root)
TextEditorMainPopup(root)
Button(root, text='More', command=TextEditorMainPopup).pack(fill=X)
Button(root, text='Quit', command=root.quit).pack(fill=X)
root.mainloop()
def main(): # may be typed or clicked
try: # or associated on Windows
fname = sys.argv[1] # arg = optional filename
except IndexError: # build in default Tk root
fname = None
TextEditorMain(loadFirst=fname).pack(expand=YES, fill=BOTH) # pack optional
mainloop()
if __name__ == '__main__': # when run as a script
#testPopup()
main() # run .pyw for no DOS box
[
39
]
Interestingly, Python’s own IDLE text editor in Python 3.1
suffers from two of the same bugs described here and resolved in
this edition’s PyEdit—in 3.1, IDLE positions at line 2 instead of
line 1 on file opens, and its external files search (similar to
PyEdit’s Grep) crashes on 3.X Unicode decoding errors when
scanning the Python standard library, causing IDLE to exit
altogether. Insert snarky comment about the shoemaker’s children
having no shoes here…
In
Chapter 9
, we
wrote a simple thumbnail image viewer that scrolled its
thumbnails in a canvas. That program in turn built on techniques and code
we developed at the end of
Chapter 8
to handle images. In both places, I promised that we’d eventually meet a
more full-featured extension of the ideas we deployed.
In this section, we finally wrap up the thumbnail images thread by
studying
PyPhoto—
an enhanced image
viewing and resizing program. PyPhoto’s basic operation is
straightforward
: given a directory of image
files, PyPhoto displays their thumbnails in a scrollable canvas. When a
thumbnail is selected, the corresponding image is displayed full size in a
pop-up window.
Unlike our prior viewers, though, PyPhoto is clever enough to scroll
(rather than crop) images too large for the physical display. Moreover,
PyPhoto introduces the notion of image resizing—it supports mouse and
keyboard events that resize the image to one of the display’s dimensions
and zoom the image in and out. Once images are opened, the resizing logic
allows images to be grown or shrunk arbitrarily, which is especially handy
for images produced by a digital camera that may be too large to view all
at once.
As added touches, PyPhoto also allows the image to be saved in a
file (possibly after being resized), and it allows image directories to be
selected and opened in the GUI itself, instead of just as command-line
arguments.
Put together, PyPhoto’s features make it an image-processing
program, albeit one with a currently small set of processing tools. I
encourage you to experiment with adding new features of your own; once you
get the hang of the Python Imaging Library (PIL) API, the object-oriented
nature of PyPhoto makes adding new tools remarkably simple.
In order to run
PyPhoto, you’ll need to fetch and install the PIL
extension package described in
Chapter 8
. PyPhoto inherits much of its
functionality from PIL—PIL is used to support extra image types beyond
those handled by standard tkinter (e.g., JPEG images) and to perform
image-processing operations such as resizes, thumbnail creation, and
saves. PIL is open source like Python, but it is not presently part of
the Python standard library. Search the Web for PIL’s location (
http://www.pythonware.com
is currently a safe bet). Also
check the Extensions directory of the examples distribution package for
a PIL self-installer.
The best way to get a feel for PyPhoto is to run it live on your
own machine to see how images are scrolled and resized. Here, we’ll
present a few screenshots to give the general flavor of the interaction.
You can start PyPhoto by clicking its icon, or you can start it from the
command line. When run directly, it opens the
images
subdirectory in its source directory, which
contains a handful of photos. When you run it from the command line, you
can pass in an initial image directory name as a command-line argument.
Figure 11-7
captures the
main thumbnail window when run directly.
Figure 11-7. PyPhoto main window, default directory
Internally, PyPhoto is loading or creating thumbnail images before
this window appears, using tools coded in
Chapter 8
. Startup may take a few seconds
the first time you open a directory, but it is quick thereafter—PyPhoto
caches thumbnails in a local subdirectory so that it can skip the
generation step the next time the directory is opened.
Technically, there are three different ways PyPhoto may start up:
viewing an explicit directory listed on the command line; viewing the
default
images
directory when no command-line
argument is given and when
images
is present where
the program is run; or displaying a simple one-button window that allows
you to select directories to open on demand, when no initial directory
is given or present (see the code’s__main__
logic).
PyPhoto also lets you open additional folders in new thumbnail
windows, by pressing the D key on your keyboard in either a thumbnail or
an image window.
Figure 11-8
, for instance,
captures the pop-up window produced in Windows 7 to select a new image
folder, and
Figure 11-9
shows the result when I select a directory copied from one of my digital
camera cards—this is a second PyPhoto thumbnail window on the display.
Figure 11-8
is also
opened by the one-button window if no initial directory is
available.
Figure 11-8. PyPhoto open directory dialog (the D key)
Figure 11-9. PyPhoto thumbnail window, other directory
Figure 11-10. PyPhoto image view window
Figure 11-11. PyPhoto Save As dialog (the S key; include an
extension)
When a thumbnail is selected, the image is displayed in a canvas,
in a new pop-up window. If it’s too large for the display, you can
scroll through its full size with the window’s scroll bars.
Figure 11-10
captures one image after its
thumbnail is clicked, and
Figure 11-11
shows the Save As
dialog issued when the S key is pressed in the image window; be sure to
type the desired filename extension (e.g.,.jpg
) in this Save As dialog, because PIL uses
it to know how to save the image to the file. In general, any number of
PyPhoto thumbnail and image windows can be open at once, and each image
can be saved independently.
Beyond the screenshots already shown, this system’s interaction is
difficult to capture in a static medium such as this book—you’re better
off test-driving the program live.
For example, clicking the left and right mouse buttons will resize
the image to the display’s height and width dimensions, respectively,
and pressing the I and O keys will zoom the image in and out in 10
percent increments. Both resizing schemes allow you to shrink an image
too large to see all at once, as well as expand small photos. They also
preserve the original aspect ratio of the photo, by changing its height
and width proportionally, while blindly resizing to the display’s
dimensions would not (height or width may be stretched).
Once resized, images may be saved in files at their current size.
PyPhoto is also smart enough to make windows full size on Windows, if an
image is larger than the
display.
Because PyPhoto
simply extends and reuses techniques and code we met
earlier in the book, we’ll omit a detailed discussion of its code here.
For background, see the discussion of image processing and PIL in
Chapter 8
and the coverage of the canvas
widget in
Chapter 9
.
In short, PyPhoto uses canvases in two ways: for thumbnail
collections and for opened images. For thumbnails, the same sort of
canvas layout code as the earlier thumbnails viewer in
Example 9-15
is employed. For
images, a canvas is used as well, but the canvas’s scrollable (full)
size is the image size, and the viewable area size is the minimum of the
physical screen size or the size of the image itself. The physical
screen size is available from themaxsize()
method ofToplevel
windows. The net effect is that
selected images may be scrolled now, too, which comes in handy if they
are too big for your display (a common case for pictures snapped with
newer digital cameras).
In addition, PyPhoto binds keyboard and mouse events to implement
resizing and zoom operations. With PIL, this is simple—we save the
original PIL image, run itsresize
method with the new image size, and redraw the image in the canvas.
PyPhoto also makes use of file open and save dialog objects, to remember
the last directory visited.
PIL supports additional operations, which we could add as new
events, but resizing is sufficient for a viewer. PyPhoto does not
currently use threads, to avoid becoming blocked for long-running tasks
(opening a large directory the first time, for instance). Such
enhancements are left as suggested exercises.
PyPhoto is implemented as the single file of
Example 11-5
, though it gets some
utility for free by reusing the thumbnail generation function of theviewer_thumbs
module that we
originally wrote near the end of
Chapter 8
in
Example 8-45
. To spare you from
having to flip back and forth too much, here’s a copy of the code of the
thumbs function imported and used here:
# imported from Chapter 8...
def makeThumbs(imgdir, size=(100, 100), subdir='thumbs'):
# returns a list of (image filename, thumb image object);
thumbdir = os.path.join(imgdir, subdir)
if not os.path.exists(thumbdir):
os.mkdir(thumbdir)
thumbs = []
for imgfile in os.listdir(imgdir):
thumbpath = os.path.join(thumbdir, imgfile)
if os.path.exists(thumbpath):
thumbobj = Image.open(thumbpath) # use already created
thumbs.append((imgfile, thumbobj))
else:
print('making', thumbpath)
imgpath = os.path.join(imgdir, imgfile)
try:
imgobj = Image.open(imgpath) # make new thumb
imgobj.thumbnail(size, Image.ANTIALIAS) # best downsize filter
imgobj.save(thumbpath) # type via ext or passed
thumbs.append((imgfile, imgobj))
except: # not always IOError
print("Skipping: ", imgpath)
return thumbs
Some of this example’s thumbnail selection window code is also
very similar to our earlier limited scrolled-thumbnails example in
Chapter 9
, but it is repeated in this
file instead of imported, to allow for future evolution (
Chapter 9
’s functional subset is now
officially demoted to prototype).
As you study this file, pay particular attention to the way it
factors
code into reused functions and methods, to
avoid redundancy; if we ever need to change the way zooming works, for
example, we have just one method to change, not two. Also notice itsScrolledCanvas
class—a reusable
component that handles the work of linking scroll bars and
canvases.
Example 11-5. PP4E\Gui\PIL\pyphoto1.py
"""
############################################################################
PyPhoto 1.1: thumbnail image viewer with resizing and saves.
Supports multiple image directory thumb windows - the initial img dir
is passed in as cmd arg, uses "images" default, or is selected via main
window button; later directories are opened by pressing "D" in image view
or thumbnail windows.
Viewer also scrolls popped-up images that are too large for the screen;
still to do: (1) rearrange thumbnails when window resized, based on current
window size; (2) [DONE] option to resize images to fit current window size?
(3) avoid scrolls if image size is less than window max size: use Label
if imgwide <= scrwide and imghigh <= scrhigh?
New in 1.1: updated to run in Python 3.1 and latest PIL;
New in 1.0: now does a form of (2) above: image is resized to one of the
display's dimensions if clicked, and zoomed in or out in 10% increments
on key presses; generalize me; caveat: seems to lose quality, pixels
after many resizes (this is probably a limitation of PIL)
The following scaler adapted from PIL's thumbnail code is similar to the
screen height scaler here, but only shrinks:
x, y = imgwide, imghigh
if x > scrwide: y = max(y * scrwide // x, 1); x = scrwide
if y > scrhigh: x = max(x * scrhigh // y, 1); y = scrhigh
############################################################################
"""
import sys, math, os
from tkinter import *
from tkinter.filedialog import SaveAs, Directory
from PIL import Image # PIL Image: also in tkinter
from PIL.ImageTk import PhotoImage # PIL photo widget replacement
from viewer_thumbs import makeThumbs # developed earlier in book
# remember last dirs across all windows
saveDialog = SaveAs(title='Save As (filename gives image type)')
openDialog = Directory(title='Select Image Directory To Open')
trace = print # or lambda *x: None
appname = 'PyPhoto 1.1: '
class ScrolledCanvas(Canvas):
"""
a canvas in a container that automatically makes
vertical and horizontal scroll bars for itself
"""
def __init__(self, container):
Canvas.__init__(self, container)
self.config(borderwidth=0)
vbar = Scrollbar(container)
hbar = Scrollbar(container, orient='horizontal')
vbar.pack(side=RIGHT, fill=Y) # pack canvas after bars
hbar.pack(side=BOTTOM, fill=X) # so clipped first
self.pack(side=TOP, fill=BOTH, expand=YES)
vbar.config(command=self.yview) # call on scroll move
hbar.config(command=self.xview)
self.config(yscrollcommand=vbar.set) # call on canvas move
self.config(xscrollcommand=hbar.set)
class ViewOne(Toplevel):
"""
open a single image in a pop-up window when created;
a class because photoimage obj must be saved, else
erased if reclaimed; scroll if too big for display;
on mouse clicks, resizes to window's height or width:
stretches or shrinks; on I/O keypress, zooms in/out;
both resizing schemes maintain original aspect ratio;
code is factored to avoid redundancy here as possible;
"""
def __init__(self, imgdir, imgfile, forcesize=()):
Toplevel.__init__(self)
helptxt = '(click L/R or press I/O to resize, S to save, D to open)'
self.title(appname + imgfile + ' ' + helptxt)
imgpath = os.path.join(imgdir, imgfile)
imgpil = Image.open(imgpath)
self.canvas = ScrolledCanvas(self)
self.drawImage(imgpil, forcesize)
self.canvas.bind('', self.onSizeToDisplayHeight)
self.canvas.bind('', self.onSizeToDisplayWidth)
self.bind('', self.onZoomIn)
self.bind('', self.onZoomOut)
self.bind('', self.onSaveImage)
self.bind('', onDirectoryOpen)
self.focus()
def drawImage(self, imgpil, forcesize=()):
imgtk = PhotoImage(image=imgpil) # not file=imgpath
scrwide, scrhigh = forcesize or self.maxsize() # wm screen size x,y
imgwide = imgtk.width() # size in pixels
imghigh = imgtk.height() # same as imgpil.size
fullsize = (0, 0, imgwide, imghigh) # scrollable
viewwide = min(imgwide, scrwide) # viewable
viewhigh = min(imghigh, scrhigh)
canvas = self.canvas
canvas.delete('all') # clear prior photo
canvas.config(height=viewhigh, width=viewwide) # viewable window size
canvas.config(scrollregion=fullsize) # scrollable area size
canvas.create_image(0, 0, image=imgtk, anchor=NW)
if imgwide <= scrwide and imghigh <= scrhigh: # too big for display?
self.state('normal') # no: win size per img
elif sys.platform[:3] == 'win': # do windows fullscreen
self.state('zoomed') # others use geometry()
self.saveimage = imgpil
self.savephoto = imgtk # keep reference on me
trace((scrwide, scrhigh), imgpil.size)
def sizeToDisplaySide(self, scaler):
# resize to fill one side of the display
imgpil = self.saveimage
scrwide, scrhigh = self.maxsize() # wm screen size x,y
imgwide, imghigh = imgpil.size # img size in pixels
newwide, newhigh = scaler(scrwide, scrhigh, imgwide, imghigh)
if (newwide * newhigh < imgwide * imghigh):
filter = Image.ANTIALIAS # shrink: antialias
else: # grow: bicub sharper
filter = Image.BICUBIC
imgnew = imgpil.resize((newwide, newhigh), filter)
self.drawImage(imgnew)
def onSizeToDisplayHeight(self, event):
def scaleHigh(scrwide, scrhigh, imgwide, imghigh):
newhigh = scrhigh
newwide = int(scrhigh * (imgwide / imghigh)) # 3.x true div
return (newwide, newhigh) # proportional
self.sizeToDisplaySide(scaleHigh)
def onSizeToDisplayWidth(self, event):
def scaleWide(scrwide, scrhigh, imgwide, imghigh):
newwide = scrwide
newhigh = int(scrwide * (imghigh / imgwide)) # 3.x true div
return (newwide, newhigh)
self.sizeToDisplaySide(scaleWide)
def zoom(self, factor):
# zoom in or out in increments
imgpil = self.saveimage
wide, high = imgpil.size
if factor < 1.0: # antialias best if shrink
filter = Image.ANTIALIAS # also nearest, bilinear
else:
filter = Image.BICUBIC
new = imgpil.resize((int(wide * factor), int(high * factor)), filter)
self.drawImage(new)
def onZoomIn(self, event, incr=.10):
self.zoom(1.0 + incr)
def onZoomOut(self, event, decr=.10):
self.zoom(1.0 - decr)
def onSaveImage(self, event):
# save current image state to file
filename = saveDialog.show()
if filename:
self.saveimage.save(filename)
def onDirectoryOpen(event):
"""
open a new image directory in new pop up
available in both thumb and img windows
"""
dirname = openDialog.show()
if dirname:
viewThumbs(dirname, kind=Toplevel)
def viewThumbs(imgdir, kind=Toplevel, numcols=None, height=400, width=500):
"""
make main or pop-up thumbnail buttons window;
uses fixed-size buttons, scrollable canvas;
sets scrollable (full) size, and places
thumbs at abs x,y coordinates in canvas;
no longer assumes all thumbs are same size:
gets max of all (x,y), some may be smaller;
"""
win = kind()
helptxt = '(press D to open other)'
win.title(appname + imgdir + ' ' + helptxt)
quit = Button(win, text='Quit', command=win.quit, bg='beige')
quit.pack(side=BOTTOM, fill=X)
canvas = ScrolledCanvas(win)
canvas.config(height=height, width=width) # init viewable window size
# changes if user resizes
thumbs = makeThumbs(imgdir) # [(imgfile, imgobj)]
numthumbs = len(thumbs)
if not numcols:
numcols = int(math.ceil(math.sqrt(numthumbs))) # fixed or N x N
numrows = int(math.ceil(numthumbs / numcols)) # 3.x true div
# max w|h: thumb=(name, obj), thumb.size=(width, height)
linksize = max(max(thumb[1].size) for thumb in thumbs)
trace(linksize)
fullsize = (0, 0, # upper left X,Y
(linksize * numcols), (linksize * numrows) ) # lower right X,Y
canvas.config(scrollregion=fullsize) # scrollable area size
rowpos = 0
savephotos = []
while thumbs:
thumbsrow, thumbs = thumbs[:numcols], thumbs[numcols:]
colpos = 0
for (imgfile, imgobj) in thumbsrow:
photo = PhotoImage(imgobj)
link = Button(canvas, image=photo)
def handler(savefile=imgfile):
ViewOne(imgdir, savefile)
link.config(command=handler, width=linksize, height=linksize)
link.pack(side=LEFT, expand=YES)
canvas.create_window(colpos, rowpos, anchor=NW,
window=link, width=linksize, height=linksize)
colpos += linksize
savephotos.append(photo)
rowpos += linksize
win.bind('', onDirectoryOpen)
win.savephotos = savephotos
return win
if __name__ == '__main__':
"""
open dir = default or cmdline arg
else show simple window to select
"""
imgdir = 'images'
if len(sys.argv) > 1: imgdir = sys.argv[1]
if os.path.exists(imgdir):
mainwin = viewThumbs(imgdir, kind=Tk)
else:
mainwin = Tk()
mainwin.title(appname + 'Open')
handler = lambda: onDirectoryOpen(None)
Button(mainwin, text='Open Image Directory', command=handler).pack()
mainwin.mainloop()