'''A set of Tkinter-based tools for GUIs.''' import matplotlib from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg from matplotlib.figure import Figure import time, re from Tkinter import * import tkMessageBox def isInt(str): '''Return True if the string is an integer, else False.''' try: int(str) except ValueError: return False else: return True def isFloat(str): '''Return True if the string is a float, else False.''' try: float(str) except ValueError: return False else: return True class MultiListbox(Frame): '''Listbox with multiple values per row. Modified from "Python Cookbook", recipe 9.4. ''' def __init__(self, master, lists, font=None): Frame.__init__(self, master,relief=SUNKEN,borderwidth=2) self.lists = [] self.labels = {} self.descriptions = {} idx = 0 for column in lists: l = column[0] w = column[1] frame = Frame(self); frame.pack(side=LEFT, expand=YES, fill=BOTH) self.labels[l] = Button(frame,text=l,borderwidth=1,relief=RAISED, padx=0,pady=0) self.labels[l].pack(fill=X) if len(column) >= 3: description = column[2] else: description = 'No description available' self.labels[l].bind('', lambda e, d=description: \ tkMessageBox.showinfo('Column Description',d)) self.labels[l].bind('', lambda e, col=idx, s=self: s.sort(col)) self.labels[l].bind('', lambda e, s=self: s.reverse()) lb = Listbox(frame, width=w, borderwidth=0, selectborderwidth=0, relief=FLAT, exportselection=FALSE, font=font) lb.pack(expand=YES, fill=BOTH) self.lists.append(lb) lb.bind('', lambda e, s=self: s._select(e.y)) lb.bind('', lambda e, s=self: s._select(e.y)) lb.bind('', lambda e: 'break') lb.bind('', lambda e, s=self: s._b2motion(e.x, e.y)) lb.bind('', lambda e, s=self: s._button2(e.x, e.y)) idx += 1 frame = Frame(self); frame.pack(side=LEFT, fill=Y) Button(frame, borderwidth=1, relief=RAISED, padx=0,pady=0).pack(fill=X) sb = Scrollbar(frame, orient=VERTICAL, command=self._scroll) sb.pack(expand=YES, fill=Y) self.lists[0]['yscrollcommand']=sb.set def sort(self,column): '''Sort the entries by the specified column. ARGS: columns: index of the column to sort on (0-indexed) ''' # Save the data and the current selections indices = [int(idx) for idx in self.curselection()] data = self.get(0,END) keys = [d[column] for d in data] intKeys = [int(key) for key in keys if isInt(key)] if len(intKeys) == len(keys): keys = intKeys else: floatKeys = [float(key) for key in keys if isFloat(key)] if len(floatKeys) == len(keys): keys = floatKeys decorated = zip(keys, xrange(0,len(keys))) decorated.sort() self.delete(0,END) selected = None cnt = 0 for key, idx in decorated: self.insert(END,data[idx]) if idx in indices: self.select_set(END,END) selected = cnt cnt += 1 if selected != None: self.see(selected) def reverse(self): '''Reverse the order of entries.''' # Save the data and the current selections data = self.get(0,END) nEntries = len(data) indices = [nEntries-int(idx)-1 for idx in self.curselection()] data.reverse() self.delete(0,END) selected = None idx = 0 for d in data: self.insert(END,d) if idx in indices: self.select_set(END,END) selected = idx idx += 1 if selected != None: self.see(selected) def bind(self,event,command): for list in self.lists: list.bind(event,command) def labelBind(self,label,event,command): self.labels[label].bind(event,command) def _select(self, y): row = self.lists[0].nearest(y) self.select_clear(0, END) self.select_set(row) return 'break' def _button2(self, x, y): for l in self.lists: l.scan_mark(x, y) return 'break' def _b2motion(self, x, y): for l in self.lists: l.scan_dragto(x, y) return 'break' def _scroll(self, *args): for l in self.lists: apply(l.yview, args) def curselection(self): return self.lists[0].curselection() def delete(self, first, last=None): for l in self.lists: l.delete(first, last) def get(self, first, last=None): result = [] for l in self.lists: result.append(l.get(first,last)) if last: return apply(map, [None] + result) return result def index(self, index): self.lists[0].index(index) def insert(self, index, *elements): for e in elements: i = 0 for l in self.lists: l.insert(index, e[i]) i = i + 1 def size(self): return self.lists[0].size() def see(self, index): for l in self.lists: l.see(index) def select_anchor(self, index): for l in self.lists: l.select_anchor(index) def select_clear(self, first, last=None): for l in self.lists: l.select_clear(first, last) def select_includes(self, index): return self.lists[0].select_includes(index) def select_set(self, first, last=None): for l in self.lists: l.select_set(first, last) class PickList(Menubutton): '''A multi-tiered picklist.''' def __init__(self,frame,width,init,entries,postcommand=None): '''Create the pick list. ARGS: frame (widget): container frame width (int): button width init (string): initial value entries: A list of entries, where each item is the list is a dictionary with the following keywords: "label" entry label "value" entry value (defaults to "label") "command" command (defaults to None) "state" state (defaults to NORMAL) "submenu" cascading submenu (defaults to None). This is a tuple of the same form as the entries tuple, which itself can hold additional levels of submenus. If an entry is not a dictionary, then its value its taken as both its label and value, and all options are set to the default values. postcommand: Command to execute when displaying the menu ''' self.variable = StringVar() self.variable.set(init) Menubutton.__init__(self,frame,width=width,textvariable=self.variable, relief=RAISED) self.menu = Menu(self,tearoff=0) if postcommand != None: self.menu.config(postcommand=postcommand) self.menuAdd(self.menu,entries) self['menu'] = self.menu def menuSet(self,entries): ''' Set the menu for this pick list. ARGS: entries: A list of entries, where each item is the list is a dictionary with the following keywords: "label" entry label "value" entry value (defaults to "label") "command" command (defaults to None) "state" state (defaults to NORMAL) "submenu" cascading submenu (defaults to None). This is a tuple of the same form as the entries tuple, which itself can hold additional levels of submenus. If an entry is not a dictionary, then its value its taken as both its label and value, and all options are set to the default values. ''' self.menu.delete(0,END) self.menuAdd(self.menu,entries) def menuAdd(self,menu,entries): ''' Add entries to the specified menu. This supports submenus, and is not meant to be called publically. ARGS: menu: Menu to add entries to. entries: A list of entries, where each item is the list is a dictionary with the following keywords: "label" entry label "value" entry value (defaults to "label") "command" command (defaults to None) "state" state (defaults to NORMAL) "submenu" cascading submenu (defaults to None). This is a tuple of the same form as the entries tuple, which itself can hold additional levels of submenus. ''' for entry in entries: if isinstance(entry,dict): label = entry['label'] value = entry.get('value',label) state = entry.get('state',NORMAL) command = entry.get('command',None) submenu = entry.get('submenu',None) if submenu == None: menu.add_radiobutton(variable=self.variable,label=label, value=value,command=command, state=state) else: sub = Menu(menu,tearoff=0) self.menuAdd(sub,submenu) menu.add_cascade(menu=sub,label=label,state=state, command=command) else: menu.add_radiobutton(variable=self.variable,label=entry, value=entry) def get(self): '''Return the selected value.''' return self.variable.get() def set(self,value): '''Set the selected value.''' return self.variable.set(value) class DateEntry(Frame): '''A set of date entry pick lists.''' def __init__(self,master,ut=False,command=None): '''Create the date entry form. The values are initialized to todays date. ARGS: ut: True if initialized to current UT date, False if local date. command: Execute this command when date is fiddled with. ''' Frame.__init__(self,master) # Conversion dictionary from abbreviated month name to month number self.monthName = {1:'Jan',2:'Feb',3:'Mar',4:'Apr',5:'May',6:'Jun', 7:'Jul',8:'Aug',9:'Sep',10:'Oct',11:'Nov',12:'Dec'} self.monthNumber = {} for i in self.monthName.keys(): self.monthNumber[self.monthName[i]] = i # Fetch the current date (either UT of local time) if ut: date = time.gmtime() else: date = time.localtime() # Create the picklists, initializing them to the current date Label(self,text='Year').pack(side=LEFT) self.yearBox = PickList(self,4,str(date[0]), [{'label':str(date[0]-10+i), 'value':str(date[0]-10+i), 'command':command} for i in xrange(0,21)]) self.yearBox.pack(side=LEFT) Label(self,text=' Month').pack(side=LEFT) self.monthBox = PickList(self,3,self.monthName[date[1]], [{'label':self.monthName[i], 'value':self.monthName[i], 'command':command} for i in xrange(1,13)]) self.monthBox.pack(side=LEFT) Label(self,text=' Day').pack(side=LEFT) days = [] for i in xrange(1,32): days.append(str(i)) self.dayBox = PickList(self,2,str(date[2]), [{'label':i, 'value':i, 'command':command} for i in xrange(1,32)]) self.dayBox.pack(side=LEFT) def year(self): '''Return the integer year.''' return int(self.yearBox.variable.get()) def month(self): '''Return the integer month.''' return self.monthNumber[self.monthBox.variable.get()] def day(self): '''Return the integer day.''' return int(self.dayBox.variable.get()) def get(self): '''Get the date as a YYYY-MM-DD string.''' return '%4d-%02d-%02d' % (self.year(), self.month(), self.day()) def set(self,dateStr): '''Set the date from a YYYY-MM-DD string. ARGS: dateStr: Date to set to, in YYYY-MM-DD format. ''' expr = re.compile('(2[0-9][0-9][0-9])-([0-1][0-9])-([0-3][0-9])') result = expr.match(dateStr) if result == None: return self.yearBox.variable.set(result.group(1)) month = result.group(2) if month[0] == '0': month = month[1] month = int(month) if not 1 <= month <= 12: return self.monthBox.variable.set(self.monthName[month]) day = result.group(3) if day[0] == '0': day = day[1] self.dayBox.variable.set(day) class EmbeddedFigure(Frame): '''A MATPLOTLIB figure embedded in a Tkinter Frame, so that it may be part of a GUI..''' # Figure counter. Incremented for every new instance, so that each # instance has a unique figure number. figureCounter = 1 def __init__(self,master,figsize=None): '''Create the date entry form. The values are initialized to todays date. ARGS: master: Containing frame. figsize: Size of figure, (w,h), in inches. ''' Frame.__init__(self,master) self.figureNumber = EmbeddedFigure.figureCounter if figsize == None: self.figure = matplotlib.pylab.figure(self.figureNumber) else: self.figure = matplotlib.pylab.figure(self.figureNumber, figsize=figsize) EmbeddedFigure.figureCounter += 1 self.canvas = FigureCanvasTkAgg(self.figure,master=self) self.canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1) self.canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=1) toolbar = NavigationToolbar2TkAgg(self.canvas,self) toolbar.update() def show(self): '''Show the figure.''' self.canvas.show() matplotlib.interactive(False)