Wednesday, July 25, 2012

Tkinter Image Viewer Demo

This code is based on the Tcl image2.tcl. I've made a few minor changes to the original; used PIL for the image handling and added a filetype filter to limit the types of files that will appear in the Files listbox.


# File: imageviewer.py
# References:
#    http://www.tcl.tk/man/tcl8.5/TkCmd/ttk_labelframe.htm
#    http://www.tcl.tk/man/tcl8.5/TkCmd/listbox.htm
#    http://www.tcl.tk/man/tcl8.5/TkCmd/ttk_label.htm
#    http://www.tcl.tk/man/tcl8.5/TkCmd/bind.htm 
#    http://infohost.nmt.edu/tcc/help/pubs/pil/index.html
#    http://www.pythonware.com/library/pil/handbook/introduction.htm

import os
from os.path import basename
from glob import *
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
from PIL import Image, ImageTk
from demopanels import MsgPanel, SeeDismissPanel

# file formats that can be 'read' by PIL
FILETYPES = ['.bmp', '.dib', '.dcx', '.gif', '.im', '.jpg',
             '.jpe', '.jpeg', '.pcd', '.pcx', '.png', '.pbm',
             '.pgm', '.ppm', '.psd', '.tif', '.tiff', '.xbm', 
             '.xpm']

class ImageViewerDemo(ttk.Frame):
    
    def __init__(self, isapp=True, name='imageviewerdemo'):
        ttk.Frame.__init__(self, name=name)
        self.pack(expand=Y, fill=BOTH)
        self.master.title('Image Viewer Demo')
        self.isapp = isapp
        self._create_widgets()
        
    def _create_widgets(self):
        if self.isapp:
            MsgPanel(self, 
                     ["This demonstration allows you to view images using the Python Imaging Library (PIL).\n\n",
                      "To view an image, double-click on its name in the 'Files' list.\n\n",
                      "To view images in another directory, click on the ",
                      "'Directory...' button and select a new directory via a 'File Dialog'."])
            
            SeeDismissPanel(self)
        
        self._create_demo_panel()
        
    def _create_demo_panel(self):
        demoPanel = Frame(self)
        demoPanel.pack(side=TOP, fill=BOTH, expand=Y)
        
        dirPanel = self._create_dir_panel(demoPanel)
        filePanel = self._create_file_panel(demoPanel)
        imagePanel = self._create_image_panel(demoPanel)
        
        # position panels        
        dirPanel.grid(in_=demoPanel, sticky='ew', padx='1m', pady='1m', columnspan=2)
        filePanel.grid(in_=demoPanel, sticky='nw', padx='1m', pady='1m', row=1, column=0)
        imagePanel.grid(in_=demoPanel, sticky='nw', padx='1m', pady='1m', row=1, column=1)
        demoPanel.columnconfigure(1, weight=1)
        
        self.entry.focus_set() 

    def _create_dir_panel(self, parent):
        lf = ttk.Labelframe(parent, text='Directory')
        lf.pack(side=TOP, fill=BOTH, expand=Y)
        
        self.curpath = os.path.join(os.getcwd(),'images')  # current path
        self.dirName = StringVar()  # entry control variable
        self.dirName.set(self.curpath)
        
        self.entry = ttk.Entry(lf, width=30, textvariable=self.dirName)
        self.entry.bind('<Return>', self._load_dir)
        
        btn = ttk.Button(lf, text='Directory...',
                         command=self._select_dir)
        
        self.entry.pack(side=LEFT, fill=BOTH, padx='2m', pady='2m', expand=Y)
        btn.pack(side=LEFT, fill=Y, padx=(0, '2m'), pady='2m')

        return lf
    
    def _create_file_panel(self, parent):
        lf = ttk.Labelframe(parent, text='Files')
        lf.pack(side=TOP, fill=BOTH, expand=Y)       
        
        # there is no ttk.Listbox; however, ttk.Scrollbar
        # works fine with the tkinter Listbox
        self.files = Listbox(lf, width=20, height=10)
        sbar = ttk.Scrollbar(lf, command=self.files.yview, orient=VERTICAL)
        self.files.configure(yscrollcommand=sbar.set)
        self._load_dir()
        
        self.files.pack(side=LEFT, fill=Y, expand=Y)
        sbar.pack(side=LEFT, fill=Y, expand=Y)
        
        self.files.bind('<Double-1>', self._load_image)
        
        return lf
    
    def _create_image_panel(self, parent):
        lf = ttk.Labelframe(parent, text='Image')
        
        # image handle to prevent garbage collection
        # of current display image
        self.imh = None    

        self.labelImage = ttk.Label(lf, image=self.imh, relief=SUNKEN, border=2)
        self.labelImage.pack(side=TOP, padx='.5m', pady='.5m')
        
        return lf
    
    def _select_dir(self):
        dir = filedialog.askdirectory(initialdir=self.dirName.get(),
                                      parent=self,
                                      title='Select a new files image directory',
                                      mustexist=True) # only existing dirs
        if dir:
            self.dirName.set(dir)
            self._load_dir()
    
    def _load_dir(self, *args):
        dn = os.path.join(self.dirName.get(), '*')

        self.files.delete(0, END)
        
        # populate files list with filenames from
        # selected directory
        for f in iglob(dn):
            if os.path.splitext(f)[1] in FILETYPES:
                self.files.insert(END, basename(f))
        
        # prevent <Return> event from propagating to
        # windows higher up in the hierarchy
        return 'break'
    
    def _load_image(self, event):
        item = self.files.curselection()
        fn = self.files.get(item)
        
        imagePath = os.path.join(self.dirName.get(), fn)    
            
        try:
            im = Image.open(imagePath)

            if im.mode == '1':
                self.imh = ImageTk.BitmapImage(im)
            else:
                self.imh = ImageTk.PhotoImage(im)
            self.labelImage.configure(image=self.imh)
        
        except Exception:
            # mark non-image selections
            self.files.itemconfig(item, background='red', 
                                  selectbackground='red')
            
        
if __name__ == '__main__':
    ImageViewerDemo().mainloop()