[Tkinter-discuss] Draw a table in Python 3.1

Michael O'Donnell michael.odonnell at uam.es
Mon Sep 7 10:24:13 CEST 2009


I use the following all-tkinter code to make a table. If it
lacks functionality, you can edit the code.

from Tkinter import *
from tkFont import Font
from math import floor

class Spreadsheet(Frame):

    def __init__(self, parent, font=None, **keywords):
        Frame.__init__(self, parent)
        self.columns=[]

        # Setup font information
        if font:
            self.txtFont=font
        else:
            self.txtFont=Font(family="Arial", size=14)
        self.defaultRowHeight=self.txtFont['size']+7

        self.headerFont=self.txtFont.copy()
        self.headerFont.configure(weight='bold')

        self.spreadsheet=Canvas(self, bg='white', bd=0,
highlightthickness=0, **keywords)
        self.header=Canvas(self, height=self.defaultRowHeight+2, bd=0,
highlightthickness=0, bg='white')
        self.header.pack(side=TOP, expand=FALSE, fill=X, pady=0)

        self.scrollY = Scrollbar(self, orient=VERTICAL,
command=self.spreadsheet.yview )
        self.scrollX = Scrollbar(self, orient=HORIZONTAL, command=self.xview )
        self.spreadsheet["xscrollcommand"] = self.scrollX.set
        self.spreadsheet["yscrollcommand"] = self.scrollY.set
        self.scrollY.pack(side="right", fill="y")
        self.scrollX.pack(side="bottom", fill="x")
        self.spreadsheet.pack(fill="both", expand=True, side="left")

        # Set up the mousewheel to scroll
        self.spreadsheet.focus_set()
        self.spreadsheet.bind("<MouseWheel>", self.mouseScroll)

        # Store current cursor (the one to restore to after a change)
        self.defaultCursor=self.cget("cursor")

        self.bind("<Configure>", self.catchResize)

    def catchResize(self, event):
        try:
            self.after_cancel(self.config_id)
        except: pass
        self.config_id = self.after(500, self.optimiseColumns)


    def xview(self, *args):
        self.header.xview(*args)
        self.spreadsheet.xview(*args)

    def initialise(self):

        self.spreadsheet.delete(ALL)

        # Any window items still bound, destroy
        for c in self.spreadsheet.children.values(): c.destroy()

        self.columns=[]
        self.rows=[]
        self.startCol=0
        self.startRow=0
        self.totalHeight=0


    def mouseScroll(self, event):
        if event.delta >0:
            self.spreadsheet.yview("scroll", "-1", "units")
        else:
            self.spreadsheet.yview("scroll", "1", "units")


    def setupColumns(self, columns):

        self.columns=[]

        for i in range(0, len(columns)):
            column=columns[i]
            name=column[0]
            width=column[1]
            if len(column)>2:
                align=column[2]
            else:
                align=CENTER
            self.columns.append([name,width,align])


    def addColumn(self, label, width=50, align=LEFT, bg='white', fg='black'):
        col=dict()
        col['label']=label
        col['width']=width
        col['align']=align
        col['bg']=bg
        col['fg']=fg
        self.columns.append(col)

    def addRow(self, pos, row):
        row=Row(row)
        row.height=self.getRowHeight(row)

        row.widgets=[]
        col=0
        for item in row:
            colDat=self.columns[col]
            if isinstance(item, Widget):
                row.widgets.append(item)
                item.internal=False
            else:
                e=Entry(self.spreadsheet, bg=colDat['bg'],
fg=colDat['fg'], font=self.txtFont, justify=colDat['align'])
                e.internal=True
                e.insert(END, item)
                if colDat['align']==RIGHT: e.xview(END)

                row.widgets.append(e)
            col += 1

        if pos==END:
            self.rows.append(row)
        else:
            self.rows.insert(pos, row)

    def getRowHeight(self, row):
        maxh=0
        for item in row:
            maxh=max(maxh, self.valHeight(item))
        return maxh


    def optimiseColumns(self, fixedWidth=True):
        if not self.columns: return

        # 1. Find the current total
        totWidth=0
        for column in self.columns:
            totWidth+=column['width']

        # Minimise columns which can be
        newWidth=0
        for col in range(0, len(self.columns)):
            maxwidth=self.neededWidth(col)
            colObj=self.columns[col]
            if maxwidth<colObj['width']:
                colObj['width']=maxwidth
            newWidth+=colObj['width']

        # Now, if some columns need more space, and it is available,
give it to them
        swidth=self.spreadsheet.winfo_width()
        if swidth<2:
            swidth=self.spreadsheet.winfo_reqwidth()

        if swidth>newWidth:
            # we have free space

            expand=[]
            for col in range(0, len(self.columns)):
                coldat=self.columns[col]
                reqwidth=self.neededWidth(col)
                if reqwidth>coldat['width']:
                    expand.append((coldat, reqwidth-coldat['width']))

            # Now, we assign each col an equal share of the free space,
            # up to their max requirement
            free=swidth-newWidth
            expand.sort(cmp=lambda a, b: cmp(a[1], b[1]))
            while expand:
                if free<1: break
                col,req=expand.pop()
                req=min(free, req)
                col['width']+=req
                free=free-req

        self.show()

    def neededWidth(self, col):
        maxwidth=self.headerFont.measure(self.columns[col]['label'])
        for row in self.rows:
            wdth=self.valWidth(row[col])
            maxwidth=max(wdth, maxwidth)
        return maxwidth+6


    def valWidth(self, val):
        if isinstance(val, basestring):
            return self.txtFont.measure(val)
        try:
            return val.winfo_reqwidth()
        except: pass


    def valHeight(self, val):
        if isinstance(val, basestring):
            return self.defaultRowHeight
        try:
            return val.winfo_reqheight()
        except: pass


    ##########################################
    # REDRAWING

    # REDRAW after change of screensize
    # Called after screen resize or the first time

    def show(self):
        self.spreadsheet.delete(ALL)
        self.redrawHeader()
        self.redrawSheet()

    def redrawHeader(self):
        self.header.delete(ALL)
        x=5
        height=self.defaultRowHeight+2
        self.header.create_line(x, 2, x, height)
        count=0
        for col in self.columns:
            width=col['width']
            self.header.create_rectangle(x+1, 3, x+width-1, height-1,
fill="#c1c2ef", outline="#c1c2ef")
            self.header.create_line(x, 2, x+width, 2)
            self.header.create_line(x, height, x+width, height)
            self.header.create_line(x+width, 2, x+width, height,
tags=('colend', str(count)))

            self.header.create_text(x+width/2, 1, text=col['label'],
anchor=N, font=self.headerFont, tags=('title', str(count)))
            # for the endline use a rect of width 3, but the border
same as background
            # Basically, this gives us a widget of 3 pix width for
detecting enter/leave events
            self.header.create_rectangle(x+width-1, 2, x+width+1,
height, fill='black', outline="#c1c2ef", tags=('colend', str(count)))
            x+=width
            count+=1

        self.header["scrollregion"]=(0,0,
x+self.scrollY.winfo_reqwidth(), height)

        # Make sure all controls are on top
        self.header.tag_raise('colend')
        self.header.tag_bind("colend", "<Enter>", self.enterColEnd)
        self.header.tag_bind("colend", "<Leave>", self.leaveColEnd)
        self.header.tag_bind("colend", "<Button-1>", self.startDrag)
        self.header.tag_bind("title", "<ButtonRelease>", self.orderByColumn)

    def redrawSheet(self):
        if not self.rows: return
        # now show the data
        y=0
        for row in self.rows:
            height=row.height
            x=5
            col=-1
            for value in row.widgets:
                col+=1
                width=self.columns[col]['width']
                self.drawCell(x, y, width, height, value, self.columns[col])
                x+=width
            y+=height

        self.spreadsheet["scrollregion"]=(0,0, x, y)

    def orderByColumn(self, event):
        wgt=self.header.find_withtag('current')
        colID=int(self.header.gettags(wgt)[1])
        self.sortOnColumn(colID)

    def enterColEnd(self, event):
        self.header.configure(cursor='sb_h_double_arrow')

    def leaveColEnd(self, event):
        self.header.configure(cursor=self.defaultCursor)

    def startDrag(self, event):
        self.wgt1=self.header.find_withtag('current')
        self.currCol=self.columns[int(self.header.gettags(self.wgt1)[1])]
        self.startX=self.header.bbox(self.wgt1)[0]
        self.header.bind('<B1-Motion>', self.moveBorder)
        self.header.bind('<ButtonRelease>', self.stopMoveBorder)

    def moveBorder(self, event):
        self.header.tag_raise(self.wgt1)
        wgt_x=self.header.bbox(self.wgt1)[0]
        diff=event.x-wgt_x
        self.header.move(self.wgt1, diff, 0)

    def stopMoveBorder(self, event):
        self.header.unbind('<B1-Motion>')
        self.header.unbind('<ButtonRelease>')
        self.grab_release()
        wgt_x=self.header.bbox(self.wgt1)[0]
        change=wgt_x-self.startX
        self.currCol['width']+=change
        self.show()

    def drawCell(self, x, y, width, height, value, col):
        if value.internal:
            self.spreadsheet.create_window(x, y,  window=value,
height=height, width=width, anchor=NW)
        else:
            wheight=min(height-2,value.winfo_reqheight())
            self.spreadsheet.create_window(x+width/2, y+height/2,
window=value, height=wheight, anchor=CENTER)

    def sortOnColumn(self, colID):
        self.rows.sort(cmp=lambda a, b, c=colID: cmp(a[c], b[c]))
        self.show()


class Row(list):

    def __init__(self, vals):
        if isinstance(vals, tuple): vals=list(vals)
        list.__init__(self, vals)
        self.height=0



if __name__ == '__main__':
    tk = Tk()
    ssw = Spreadsheet(tk, width=900)
    ssw.pack(side=TOP, expand=TRUE, fill=BOTH)
    ssw.initialise()
    ssw.addColumn('', 110)
    ssw.addColumn('Subject', 300, bg='light blue')
    ssw.addColumn('Sender', 200, align=CENTER)
    ssw.addColumn('Date', 200, align=RIGHT)

    # if embedded widgets do not have ssw.spreadsheet as parent, they
will overlap the border
    for i in range(0,20):
        but=Button(ssw.spreadsheet, text="View", font="Arial 8")
        ssw.addRow(END, (but, 'Important Message: %d' % i, 'John Doe',
'10/10/%04d' % (1900-i)))

    ssw.optimiseColumns()
    ssw.show()
    tk.mainloop()


More information about the Tkinter-discuss mailing list