E
Eric Brunel
Hi all,
I was creating a Tkinter widget in the style of the reversed tabs below Excel
worksheets and I stepped in a serious problem: the code I made makes python
crash with a seg fault, bus error or X11 BadGC error on both Solaris (2.6 and
2.7) and Linux (Mandrake 8.0); it doesn't crash on Windows. I tried to simplify
the script, but I couldn't reproduce the crash with a simpler code. So the code
below is somewhat long; sorry for that.
To make it crash, just run the script and click on a tab. It usually crashes at
the first click, but you may have to play a bit with the tabs. I tried to run
Python through gdb to see where the crash happens, but it seems to be quite
random. Maybe a memory corruption?
There's a simple workaround, but with drawbacks: at the beginning of the
__update method, if I do not destroy and re-create the Canvas, but simply empty
its contents, the script works. But it keeps the commands declared created at
the tcl level for the former bindings, so it silently eats up memory.
My setup is Python 2.1 with tcl/tk 8.3.4. I searched the bug database but this
bug doesn't seem to be known. But maybe it was corrected in a newer Python or tk
version? Can anyone confirm that?
Thanks a lot in advance.
Here is the code:
--TabRow.py----------------------------------
from Tkinter import *
## CLASS GENERIC.CALLBACK:
## =======================
# Instances are generic callbacks for buttons, bindings, etc...
class GenericCallback:
def __init__(self, callback, *commonArgs):
self.callback = callback
self.__commonArgs = commonArgs
def __call__(self, *args):
return apply(self.callback, self.__commonArgs + args)
## CLASS TAB.ROW:
## ==============
# Excel-style reverse tabs in a row
class TabRow(Frame):
## CLASS ATTRIBUTES:
## -----------------
defaultHeight = 24 # Default height for row
verticalMargin = 2 # Margin at the top and the bottom of the tabs
tabBorderWidth = 10 # Width for the descending and ascending lines at the
# border of tabs
## METHOD __INIT__:
## ----------------
# Constructor
# Parameters: the ones for the Frame class
# Recognized options: the ones for the Frame class +
# - tabs: list of tabs
# - currenttab: active element in tabs, or its index
# - tabchangecallback: function called when the user clicks on a tab
# (1 param = text; returns a boolean)
# - font: the font to use to display the texts in the tabs
# - activebackground: the background for the current tab
def __init__(self, *args, **options):
## INSTANCE ATTRIBUTES:
## --------------------
self.__tabs = ()
self.__tabChangeCallback = None
self.__tabsCanvas = None
self.__currentTabIndex = 0
self.__tabPositions = None
self.__canvasHeight = TabRow.defaultHeight
## Super-init
apply(Frame.__init__, (self,) + args)
## Configure/update
self.configure(options)
## METHOD CONFIGURE:
## -----------------
# Changes the options for the tab row
def configure(self, dictOptions={}, **options):
options.update(dictOptions)
## Get specific options
self.__tabs = options.get("tabs", self.__tabs)
self.__tabChangeCallback = options.get('tabchangecallback',
self.__tabChangeCallback)
## Get index for current tab
if options.has_key('currenttab'):
if type(options['currenttab']) == type(0):
self.__currentTabIndex = options['currenttab']
else:
indices = [i for i in range(len(self.__tabs))
if self.__tabs == options['currenttab']]
if indices: self.__currentTabIndex = indices[0]
## Remember forced height for canvas if any
self.__canvasHeight = options.get('height', self.__canvasHeight)
## Remove unwanted options
needUpdate = 0
for o in ('tabs', 'currenttab', 'tabchangecallback',
'font', 'activebackground', 'height'):
if not options.has_key(o): continue
del options[o]
needUpdate = 1
if options.has_key('bg') or options.has_key('background'): needUpdate = 1
## If needed, apply options on the frame
if options:
apply(Frame.configure, (self,), options)
## If needed, update display
if needUpdate: self.__update()
## METHOD __UPDATE:
## ----------------
# Updates the display
def __update(self):
## (Re)create canvas for tabs
if self.__tabsCanvas is not None:
self.__tabsCanvas.grid_forget()
self.__tabsCanvas.destroy()
self.__tabsCanvas = Canvas(self, bg=self.cget('background'),
height=self.__canvasHeight)
self.__tabsCanvas.grid(row=0, column=0, sticky='nswe')
## Build tabs
tabIndex, pos = 0, 0
self.__tabPositions = []
activeTabRight = 0
for text in self.__tabs:
## Standard tag + specific tag if tab is the current one
tabTag = 'TAB_%s' % tabIndex
tags = [tabTag]
if tabIndex == self.__currentTabIndex: tags.append("CURRENT_TAB")
tags = tuple(tags)
## Remember tab position
self.__tabPositions.append(pos)
## Draw text
textId = self.__tabsCanvas.create_text(pos + TabRow.tabBorderWidth,
self.__canvasHeight / 2,
text=text, anchor=W, tags=tags,
font=('helvetica', 10, 'bold'))
## Polygon for tab, including line from left side if current tab
textBBox = self.__tabsCanvas.bbox(textId)
x = textBBox[2]
coords = [
pos, TabRow.verticalMargin,
pos + TabRow.tabBorderWidth,
self.__canvasHeight - TabRow.verticalMargin,
x, self.__canvasHeight - TabRow.verticalMargin,
x + TabRow.tabBorderWidth, TabRow.verticalMargin ]
if tabIndex == self.__currentTabIndex:
coords = [0, TabRow.verticalMargin] + coords
activeTabRight = x + TabRow.tabBorderWidth
## Get polygon background
polygOpt = {'fill' : self.__tabsCanvas.cget('background'),
'outline':'', 'tags':tags}
if tabIndex == self.__currentTabIndex: polygOpt['fill'] = 'white'
## Draw polygon
polygId = apply(self.__tabsCanvas.create_polygon, coords, polygOpt)
lineId = apply(self.__tabsCanvas.create_line, coords,
{'fill':'black', 'tags':tags})
## Put it under text
self.__tabsCanvas.lower(lineId)
self.__tabsCanvas.lower(polygId)
## Binding for tab change
self.__tabsCanvas.tag_bind(tabTag, '<ButtonRelease-1>',
GenericCallback(self.__changeTab, tabIndex, text))
## Update position and tab index
pos = x + TabRow.tabBorderWidth / 2
tabIndex += 1
## End of display: draw line from active tab to right border
## and put active tab on top
self.__tabsCanvas.create_line(activeTabRight, TabRow.verticalMargin,
pos + TabRow.tabBorderWidth / 2,
TabRow.verticalMargin)
self.__tabsCanvas.tag_raise("CURRENT_TAB")
## METHOD __CHANGE.TAB:
## --------------------
# Called when the user clicks on a tab
def __changeTab(self, tabIndex, text, event=None):
if self.__tabChangeCallback is None: return
if not self.__tabChangeCallback(text): return
self.__currentTabIndex = tabIndex
self.__update()
if __name__ == '__main__':
root = Tk()
def ct(t):
print t
return 1
r = TabRow(root, tabs=('foo', 'bar', 'spam'), tabchangecallback=ct)
r.pack()
root.mainloop()
I was creating a Tkinter widget in the style of the reversed tabs below Excel
worksheets and I stepped in a serious problem: the code I made makes python
crash with a seg fault, bus error or X11 BadGC error on both Solaris (2.6 and
2.7) and Linux (Mandrake 8.0); it doesn't crash on Windows. I tried to simplify
the script, but I couldn't reproduce the crash with a simpler code. So the code
below is somewhat long; sorry for that.
To make it crash, just run the script and click on a tab. It usually crashes at
the first click, but you may have to play a bit with the tabs. I tried to run
Python through gdb to see where the crash happens, but it seems to be quite
random. Maybe a memory corruption?
There's a simple workaround, but with drawbacks: at the beginning of the
__update method, if I do not destroy and re-create the Canvas, but simply empty
its contents, the script works. But it keeps the commands declared created at
the tcl level for the former bindings, so it silently eats up memory.
My setup is Python 2.1 with tcl/tk 8.3.4. I searched the bug database but this
bug doesn't seem to be known. But maybe it was corrected in a newer Python or tk
version? Can anyone confirm that?
Thanks a lot in advance.
Here is the code:
--TabRow.py----------------------------------
from Tkinter import *
## CLASS GENERIC.CALLBACK:
## =======================
# Instances are generic callbacks for buttons, bindings, etc...
class GenericCallback:
def __init__(self, callback, *commonArgs):
self.callback = callback
self.__commonArgs = commonArgs
def __call__(self, *args):
return apply(self.callback, self.__commonArgs + args)
## CLASS TAB.ROW:
## ==============
# Excel-style reverse tabs in a row
class TabRow(Frame):
## CLASS ATTRIBUTES:
## -----------------
defaultHeight = 24 # Default height for row
verticalMargin = 2 # Margin at the top and the bottom of the tabs
tabBorderWidth = 10 # Width for the descending and ascending lines at the
# border of tabs
## METHOD __INIT__:
## ----------------
# Constructor
# Parameters: the ones for the Frame class
# Recognized options: the ones for the Frame class +
# - tabs: list of tabs
# - currenttab: active element in tabs, or its index
# - tabchangecallback: function called when the user clicks on a tab
# (1 param = text; returns a boolean)
# - font: the font to use to display the texts in the tabs
# - activebackground: the background for the current tab
def __init__(self, *args, **options):
## INSTANCE ATTRIBUTES:
## --------------------
self.__tabs = ()
self.__tabChangeCallback = None
self.__tabsCanvas = None
self.__currentTabIndex = 0
self.__tabPositions = None
self.__canvasHeight = TabRow.defaultHeight
## Super-init
apply(Frame.__init__, (self,) + args)
## Configure/update
self.configure(options)
## METHOD CONFIGURE:
## -----------------
# Changes the options for the tab row
def configure(self, dictOptions={}, **options):
options.update(dictOptions)
## Get specific options
self.__tabs = options.get("tabs", self.__tabs)
self.__tabChangeCallback = options.get('tabchangecallback',
self.__tabChangeCallback)
## Get index for current tab
if options.has_key('currenttab'):
if type(options['currenttab']) == type(0):
self.__currentTabIndex = options['currenttab']
else:
indices = [i for i in range(len(self.__tabs))
if self.__tabs == options['currenttab']]
if indices: self.__currentTabIndex = indices[0]
## Remember forced height for canvas if any
self.__canvasHeight = options.get('height', self.__canvasHeight)
## Remove unwanted options
needUpdate = 0
for o in ('tabs', 'currenttab', 'tabchangecallback',
'font', 'activebackground', 'height'):
if not options.has_key(o): continue
del options[o]
needUpdate = 1
if options.has_key('bg') or options.has_key('background'): needUpdate = 1
## If needed, apply options on the frame
if options:
apply(Frame.configure, (self,), options)
## If needed, update display
if needUpdate: self.__update()
## METHOD __UPDATE:
## ----------------
# Updates the display
def __update(self):
## (Re)create canvas for tabs
if self.__tabsCanvas is not None:
self.__tabsCanvas.grid_forget()
self.__tabsCanvas.destroy()
self.__tabsCanvas = Canvas(self, bg=self.cget('background'),
height=self.__canvasHeight)
self.__tabsCanvas.grid(row=0, column=0, sticky='nswe')
## Build tabs
tabIndex, pos = 0, 0
self.__tabPositions = []
activeTabRight = 0
for text in self.__tabs:
## Standard tag + specific tag if tab is the current one
tabTag = 'TAB_%s' % tabIndex
tags = [tabTag]
if tabIndex == self.__currentTabIndex: tags.append("CURRENT_TAB")
tags = tuple(tags)
## Remember tab position
self.__tabPositions.append(pos)
## Draw text
textId = self.__tabsCanvas.create_text(pos + TabRow.tabBorderWidth,
self.__canvasHeight / 2,
text=text, anchor=W, tags=tags,
font=('helvetica', 10, 'bold'))
## Polygon for tab, including line from left side if current tab
textBBox = self.__tabsCanvas.bbox(textId)
x = textBBox[2]
coords = [
pos, TabRow.verticalMargin,
pos + TabRow.tabBorderWidth,
self.__canvasHeight - TabRow.verticalMargin,
x, self.__canvasHeight - TabRow.verticalMargin,
x + TabRow.tabBorderWidth, TabRow.verticalMargin ]
if tabIndex == self.__currentTabIndex:
coords = [0, TabRow.verticalMargin] + coords
activeTabRight = x + TabRow.tabBorderWidth
## Get polygon background
polygOpt = {'fill' : self.__tabsCanvas.cget('background'),
'outline':'', 'tags':tags}
if tabIndex == self.__currentTabIndex: polygOpt['fill'] = 'white'
## Draw polygon
polygId = apply(self.__tabsCanvas.create_polygon, coords, polygOpt)
lineId = apply(self.__tabsCanvas.create_line, coords,
{'fill':'black', 'tags':tags})
## Put it under text
self.__tabsCanvas.lower(lineId)
self.__tabsCanvas.lower(polygId)
## Binding for tab change
self.__tabsCanvas.tag_bind(tabTag, '<ButtonRelease-1>',
GenericCallback(self.__changeTab, tabIndex, text))
## Update position and tab index
pos = x + TabRow.tabBorderWidth / 2
tabIndex += 1
## End of display: draw line from active tab to right border
## and put active tab on top
self.__tabsCanvas.create_line(activeTabRight, TabRow.verticalMargin,
pos + TabRow.tabBorderWidth / 2,
TabRow.verticalMargin)
self.__tabsCanvas.tag_raise("CURRENT_TAB")
## METHOD __CHANGE.TAB:
## --------------------
# Called when the user clicks on a tab
def __changeTab(self, tabIndex, text, event=None):
if self.__tabChangeCallback is None: return
if not self.__tabChangeCallback(text): return
self.__currentTabIndex = tabIndex
self.__update()
if __name__ == '__main__':
root = Tk()
def ct(t):
print t
return 1
r = TabRow(root, tabs=('foo', 'bar', 'spam'), tabchangecallback=ct)
r.pack()
root.mainloop()