table.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184
  1. # Natural Language Toolkit: Table widget
  2. #
  3. # Copyright (C) 2001-2019 NLTK Project
  4. # Author: Edward Loper <edloper@gmail.com>
  5. # URL: <http://nltk.org/>
  6. # For license information, see LICENSE.TXT
  7. """
  8. Tkinter widgets for displaying multi-column listboxes and tables.
  9. """
  10. from __future__ import division
  11. import operator
  12. from six.moves.tkinter import Frame, Label, Listbox, Scrollbar, Tk
  13. ######################################################################
  14. # Multi-Column Listbox
  15. ######################################################################
  16. class MultiListbox(Frame):
  17. """
  18. A multi-column listbox, where the current selection applies to an
  19. entire row. Based on the MultiListbox Tkinter widget
  20. recipe from the Python Cookbook (http://code.activestate.com/recipes/52266/)
  21. For the most part, ``MultiListbox`` methods delegate to its
  22. contained listboxes. For any methods that do not have docstrings,
  23. see ``Tkinter.Listbox`` for a description of what that method does.
  24. """
  25. # /////////////////////////////////////////////////////////////////
  26. # Configuration
  27. # /////////////////////////////////////////////////////////////////
  28. #: Default configuration values for the frame.
  29. FRAME_CONFIG = dict(background='#888', takefocus=True, highlightthickness=1)
  30. #: Default configurations for the column labels.
  31. LABEL_CONFIG = dict(
  32. borderwidth=1,
  33. relief='raised',
  34. font='helvetica -16 bold',
  35. background='#444',
  36. foreground='white',
  37. )
  38. #: Default configuration for the column listboxes.
  39. LISTBOX_CONFIG = dict(
  40. borderwidth=1,
  41. selectborderwidth=0,
  42. highlightthickness=0,
  43. exportselection=False,
  44. selectbackground='#888',
  45. activestyle='none',
  46. takefocus=False,
  47. )
  48. # /////////////////////////////////////////////////////////////////
  49. # Constructor
  50. # /////////////////////////////////////////////////////////////////
  51. def __init__(self, master, columns, column_weights=None, cnf={}, **kw):
  52. """
  53. Construct a new multi-column listbox widget.
  54. :param master: The widget that should contain the new
  55. multi-column listbox.
  56. :param columns: Specifies what columns should be included in
  57. the new multi-column listbox. If ``columns`` is an integer,
  58. the it is the number of columns to include. If it is
  59. a list, then its length indicates the number of columns
  60. to include; and each element of the list will be used as
  61. a label for the corresponding column.
  62. :param cnf, kw: Configuration parameters for this widget.
  63. Use ``label_*`` to configure all labels; and ``listbox_*``
  64. to configure all listboxes. E.g.:
  65. >>> mlb = MultiListbox(master, 5, label_foreground='red')
  66. """
  67. # If columns was specified as an int, convert it to a list.
  68. if isinstance(columns, int):
  69. columns = list(range(columns))
  70. include_labels = False
  71. else:
  72. include_labels = True
  73. if len(columns) == 0:
  74. raise ValueError("Expected at least one column")
  75. # Instance variables
  76. self._column_names = tuple(columns)
  77. self._listboxes = []
  78. self._labels = []
  79. # Pick a default value for column_weights, if none was specified.
  80. if column_weights is None:
  81. column_weights = [1] * len(columns)
  82. elif len(column_weights) != len(columns):
  83. raise ValueError('Expected one column_weight for each column')
  84. self._column_weights = column_weights
  85. # Configure our widgets.
  86. Frame.__init__(self, master, **self.FRAME_CONFIG)
  87. self.grid_rowconfigure(1, weight=1)
  88. for i, label in enumerate(self._column_names):
  89. self.grid_columnconfigure(i, weight=column_weights[i])
  90. # Create a label for the column
  91. if include_labels:
  92. l = Label(self, text=label, **self.LABEL_CONFIG)
  93. self._labels.append(l)
  94. l.grid(column=i, row=0, sticky='news', padx=0, pady=0)
  95. l.column_index = i
  96. # Create a listbox for the column
  97. lb = Listbox(self, **self.LISTBOX_CONFIG)
  98. self._listboxes.append(lb)
  99. lb.grid(column=i, row=1, sticky='news', padx=0, pady=0)
  100. lb.column_index = i
  101. # Clicking or dragging selects:
  102. lb.bind('<Button-1>', self._select)
  103. lb.bind('<B1-Motion>', self._select)
  104. # Scroll whell scrolls:
  105. lb.bind('<Button-4>', lambda e: self._scroll(-1))
  106. lb.bind('<Button-5>', lambda e: self._scroll(+1))
  107. lb.bind('<MouseWheel>', lambda e: self._scroll(e.delta))
  108. # Button 2 can be used to scan:
  109. lb.bind('<Button-2>', lambda e: self.scan_mark(e.x, e.y))
  110. lb.bind('<B2-Motion>', lambda e: self.scan_dragto(e.x, e.y))
  111. # Dragging outside the window has no effect (diable
  112. # the default listbox behavior, which scrolls):
  113. lb.bind('<B1-Leave>', lambda e: 'break')
  114. # Columns can be resized by dragging them:
  115. l.bind('<Button-1>', self._resize_column)
  116. # Columns can be resized by dragging them. (This binding is
  117. # used if they click on the grid between columns:)
  118. self.bind('<Button-1>', self._resize_column)
  119. # Set up key bindings for the widget:
  120. self.bind('<Up>', lambda e: self.select(delta=-1))
  121. self.bind('<Down>', lambda e: self.select(delta=1))
  122. self.bind('<Prior>', lambda e: self.select(delta=-self._pagesize()))
  123. self.bind('<Next>', lambda e: self.select(delta=self._pagesize()))
  124. # Configuration customizations
  125. self.configure(cnf, **kw)
  126. # /////////////////////////////////////////////////////////////////
  127. # Column Resizing
  128. # /////////////////////////////////////////////////////////////////
  129. def _resize_column(self, event):
  130. """
  131. Callback used to resize a column of the table. Return ``True``
  132. if the column is actually getting resized (if the user clicked
  133. on the far left or far right 5 pixels of a label); and
  134. ``False`` otherwies.
  135. """
  136. # If we're already waiting for a button release, then ignore
  137. # the new button press.
  138. if event.widget.bind('<ButtonRelease>'):
  139. return False
  140. # Decide which column (if any) to resize.
  141. self._resize_column_index = None
  142. if event.widget is self:
  143. for i, lb in enumerate(self._listboxes):
  144. if abs(event.x - (lb.winfo_x() + lb.winfo_width())) < 10:
  145. self._resize_column_index = i
  146. elif event.x > (event.widget.winfo_width() - 5):
  147. self._resize_column_index = event.widget.column_index
  148. elif event.x < 5 and event.widget.column_index != 0:
  149. self._resize_column_index = event.widget.column_index - 1
  150. # Bind callbacks that are used to resize it.
  151. if self._resize_column_index is not None:
  152. event.widget.bind('<Motion>', self._resize_column_motion_cb)
  153. event.widget.bind(
  154. '<ButtonRelease-%d>' % event.num, self._resize_column_buttonrelease_cb
  155. )
  156. return True
  157. else:
  158. return False
  159. def _resize_column_motion_cb(self, event):
  160. lb = self._listboxes[self._resize_column_index]
  161. charwidth = lb.winfo_width() / lb['width']
  162. x1 = event.x + event.widget.winfo_x()
  163. x2 = lb.winfo_x() + lb.winfo_width()
  164. lb['width'] = max(3, lb['width'] + (x1 - x2) // charwidth)
  165. def _resize_column_buttonrelease_cb(self, event):
  166. event.widget.unbind('<ButtonRelease-%d>' % event.num)
  167. event.widget.unbind('<Motion>')
  168. # /////////////////////////////////////////////////////////////////
  169. # Properties
  170. # /////////////////////////////////////////////////////////////////
  171. @property
  172. def column_names(self):
  173. """
  174. A tuple containing the names of the columns used by this
  175. multi-column listbox.
  176. """
  177. return self._column_names
  178. @property
  179. def column_labels(self):
  180. """
  181. A tuple containing the ``Tkinter.Label`` widgets used to
  182. display the label of each column. If this multi-column
  183. listbox was created without labels, then this will be an empty
  184. tuple. These widgets will all be augmented with a
  185. ``column_index`` attribute, which can be used to determine
  186. which column they correspond to. This can be convenient,
  187. e.g., when defining callbacks for bound events.
  188. """
  189. return tuple(self._labels)
  190. @property
  191. def listboxes(self):
  192. """
  193. A tuple containing the ``Tkinter.Listbox`` widgets used to
  194. display individual columns. These widgets will all be
  195. augmented with a ``column_index`` attribute, which can be used
  196. to determine which column they correspond to. This can be
  197. convenient, e.g., when defining callbacks for bound events.
  198. """
  199. return tuple(self._listboxes)
  200. # /////////////////////////////////////////////////////////////////
  201. # Mouse & Keyboard Callback Functions
  202. # /////////////////////////////////////////////////////////////////
  203. def _select(self, e):
  204. i = e.widget.nearest(e.y)
  205. self.selection_clear(0, 'end')
  206. self.selection_set(i)
  207. self.activate(i)
  208. self.focus()
  209. def _scroll(self, delta):
  210. for lb in self._listboxes:
  211. lb.yview_scroll(delta, 'unit')
  212. return 'break'
  213. def _pagesize(self):
  214. """:return: The number of rows that makes up one page"""
  215. return int(self.index('@0,1000000')) - int(self.index('@0,0'))
  216. # /////////////////////////////////////////////////////////////////
  217. # Row selection
  218. # /////////////////////////////////////////////////////////////////
  219. def select(self, index=None, delta=None, see=True):
  220. """
  221. Set the selected row. If ``index`` is specified, then select
  222. row ``index``. Otherwise, if ``delta`` is specified, then move
  223. the current selection by ``delta`` (negative numbers for up,
  224. positive numbers for down). This will not move the selection
  225. past the top or the bottom of the list.
  226. :param see: If true, then call ``self.see()`` with the newly
  227. selected index, to ensure that it is visible.
  228. """
  229. if (index is not None) and (delta is not None):
  230. raise ValueError('specify index or delta, but not both')
  231. # If delta was given, then calculate index.
  232. if delta is not None:
  233. if len(self.curselection()) == 0:
  234. index = -1 + delta
  235. else:
  236. index = int(self.curselection()[0]) + delta
  237. # Clear all selected rows.
  238. self.selection_clear(0, 'end')
  239. # Select the specified index
  240. if index is not None:
  241. index = min(max(index, 0), self.size() - 1)
  242. # self.activate(index)
  243. self.selection_set(index)
  244. if see:
  245. self.see(index)
  246. # /////////////////////////////////////////////////////////////////
  247. # Configuration
  248. # /////////////////////////////////////////////////////////////////
  249. def configure(self, cnf={}, **kw):
  250. """
  251. Configure this widget. Use ``label_*`` to configure all
  252. labels; and ``listbox_*`` to configure all listboxes. E.g.:
  253. >>> mlb = MultiListbox(master, 5)
  254. >>> mlb.configure(label_foreground='red')
  255. >>> mlb.configure(listbox_foreground='red')
  256. """
  257. cnf = dict(list(cnf.items()) + list(kw.items()))
  258. for (key, val) in list(cnf.items()):
  259. if key.startswith('label_') or key.startswith('label-'):
  260. for label in self._labels:
  261. label.configure({key[6:]: val})
  262. elif key.startswith('listbox_') or key.startswith('listbox-'):
  263. for listbox in self._listboxes:
  264. listbox.configure({key[8:]: val})
  265. else:
  266. Frame.configure(self, {key: val})
  267. def __setitem__(self, key, val):
  268. """
  269. Configure this widget. This is equivalent to
  270. ``self.configure({key,val``)}. See ``configure()``.
  271. """
  272. self.configure({key: val})
  273. def rowconfigure(self, row_index, cnf={}, **kw):
  274. """
  275. Configure all table cells in the given row. Valid keyword
  276. arguments are: ``background``, ``bg``, ``foreground``, ``fg``,
  277. ``selectbackground``, ``selectforeground``.
  278. """
  279. for lb in self._listboxes:
  280. lb.itemconfigure(row_index, cnf, **kw)
  281. def columnconfigure(self, col_index, cnf={}, **kw):
  282. """
  283. Configure all table cells in the given column. Valid keyword
  284. arguments are: ``background``, ``bg``, ``foreground``, ``fg``,
  285. ``selectbackground``, ``selectforeground``.
  286. """
  287. lb = self._listboxes[col_index]
  288. cnf = dict(list(cnf.items()) + list(kw.items()))
  289. for (key, val) in list(cnf.items()):
  290. if key in (
  291. 'background',
  292. 'bg',
  293. 'foreground',
  294. 'fg',
  295. 'selectbackground',
  296. 'selectforeground',
  297. ):
  298. for i in range(lb.size()):
  299. lb.itemconfigure(i, {key: val})
  300. else:
  301. lb.configure({key: val})
  302. def itemconfigure(self, row_index, col_index, cnf=None, **kw):
  303. """
  304. Configure the table cell at the given row and column. Valid
  305. keyword arguments are: ``background``, ``bg``, ``foreground``,
  306. ``fg``, ``selectbackground``, ``selectforeground``.
  307. """
  308. lb = self._listboxes[col_index]
  309. return lb.itemconfigure(row_index, cnf, **kw)
  310. # /////////////////////////////////////////////////////////////////
  311. # Value Access
  312. # /////////////////////////////////////////////////////////////////
  313. def insert(self, index, *rows):
  314. """
  315. Insert the given row or rows into the table, at the given
  316. index. Each row value should be a tuple of cell values, one
  317. for each column in the row. Index may be an integer or any of
  318. the special strings (such as ``'end'``) accepted by
  319. ``Tkinter.Listbox``.
  320. """
  321. for elt in rows:
  322. if len(elt) != len(self._column_names):
  323. raise ValueError(
  324. 'rows should be tuples whose length '
  325. 'is equal to the number of columns'
  326. )
  327. for (lb, elts) in zip(self._listboxes, list(zip(*rows))):
  328. lb.insert(index, *elts)
  329. def get(self, first, last=None):
  330. """
  331. Return the value(s) of the specified row(s). If ``last`` is
  332. not specified, then return a single row value; otherwise,
  333. return a list of row values. Each row value is a tuple of
  334. cell values, one for each column in the row.
  335. """
  336. values = [lb.get(first, last) for lb in self._listboxes]
  337. if last:
  338. return [tuple(row) for row in zip(*values)]
  339. else:
  340. return tuple(values)
  341. def bbox(self, row, col):
  342. """
  343. Return the bounding box for the given table cell, relative to
  344. this widget's top-left corner. The bounding box is a tuple
  345. of integers ``(left, top, width, height)``.
  346. """
  347. dx, dy, _, _ = self.grid_bbox(row=0, column=col)
  348. x, y, w, h = self._listboxes[col].bbox(row)
  349. return int(x) + int(dx), int(y) + int(dy), int(w), int(h)
  350. # /////////////////////////////////////////////////////////////////
  351. # Hide/Show Columns
  352. # /////////////////////////////////////////////////////////////////
  353. def hide_column(self, col_index):
  354. """
  355. Hide the given column. The column's state is still
  356. maintained: its values will still be returned by ``get()``, and
  357. you must supply its values when calling ``insert()``. It is
  358. safe to call this on a column that is already hidden.
  359. :see: ``show_column()``
  360. """
  361. if self._labels:
  362. self._labels[col_index].grid_forget()
  363. self.listboxes[col_index].grid_forget()
  364. self.grid_columnconfigure(col_index, weight=0)
  365. def show_column(self, col_index):
  366. """
  367. Display a column that has been hidden using ``hide_column()``.
  368. It is safe to call this on a column that is not hidden.
  369. """
  370. weight = self._column_weights[col_index]
  371. if self._labels:
  372. self._labels[col_index].grid(
  373. column=col_index, row=0, sticky='news', padx=0, pady=0
  374. )
  375. self._listboxes[col_index].grid(
  376. column=col_index, row=1, sticky='news', padx=0, pady=0
  377. )
  378. self.grid_columnconfigure(col_index, weight=weight)
  379. # /////////////////////////////////////////////////////////////////
  380. # Binding Methods
  381. # /////////////////////////////////////////////////////////////////
  382. def bind_to_labels(self, sequence=None, func=None, add=None):
  383. """
  384. Add a binding to each ``Tkinter.Label`` widget in this
  385. mult-column listbox that will call ``func`` in response to the
  386. event sequence.
  387. :return: A list of the identifiers of replaced binding
  388. functions (if any), allowing for their deletion (to
  389. prevent a memory leak).
  390. """
  391. return [label.bind(sequence, func, add) for label in self.column_labels]
  392. def bind_to_listboxes(self, sequence=None, func=None, add=None):
  393. """
  394. Add a binding to each ``Tkinter.Listbox`` widget in this
  395. mult-column listbox that will call ``func`` in response to the
  396. event sequence.
  397. :return: A list of the identifiers of replaced binding
  398. functions (if any), allowing for their deletion (to
  399. prevent a memory leak).
  400. """
  401. for listbox in self.listboxes:
  402. listbox.bind(sequence, func, add)
  403. def bind_to_columns(self, sequence=None, func=None, add=None):
  404. """
  405. Add a binding to each ``Tkinter.Label`` and ``Tkinter.Listbox``
  406. widget in this mult-column listbox that will call ``func`` in
  407. response to the event sequence.
  408. :return: A list of the identifiers of replaced binding
  409. functions (if any), allowing for their deletion (to
  410. prevent a memory leak).
  411. """
  412. return self.bind_to_labels(sequence, func, add) + self.bind_to_listboxes(
  413. sequence, func, add
  414. )
  415. # /////////////////////////////////////////////////////////////////
  416. # Simple Delegation
  417. # /////////////////////////////////////////////////////////////////
  418. # These methods delegate to the first listbox:
  419. def curselection(self, *args, **kwargs):
  420. return self._listboxes[0].curselection(*args, **kwargs)
  421. def selection_includes(self, *args, **kwargs):
  422. return self._listboxes[0].selection_includes(*args, **kwargs)
  423. def itemcget(self, *args, **kwargs):
  424. return self._listboxes[0].itemcget(*args, **kwargs)
  425. def size(self, *args, **kwargs):
  426. return self._listboxes[0].size(*args, **kwargs)
  427. def index(self, *args, **kwargs):
  428. return self._listboxes[0].index(*args, **kwargs)
  429. def nearest(self, *args, **kwargs):
  430. return self._listboxes[0].nearest(*args, **kwargs)
  431. # These methods delegate to each listbox (and return None):
  432. def activate(self, *args, **kwargs):
  433. for lb in self._listboxes:
  434. lb.activate(*args, **kwargs)
  435. def delete(self, *args, **kwargs):
  436. for lb in self._listboxes:
  437. lb.delete(*args, **kwargs)
  438. def scan_mark(self, *args, **kwargs):
  439. for lb in self._listboxes:
  440. lb.scan_mark(*args, **kwargs)
  441. def scan_dragto(self, *args, **kwargs):
  442. for lb in self._listboxes:
  443. lb.scan_dragto(*args, **kwargs)
  444. def see(self, *args, **kwargs):
  445. for lb in self._listboxes:
  446. lb.see(*args, **kwargs)
  447. def selection_anchor(self, *args, **kwargs):
  448. for lb in self._listboxes:
  449. lb.selection_anchor(*args, **kwargs)
  450. def selection_clear(self, *args, **kwargs):
  451. for lb in self._listboxes:
  452. lb.selection_clear(*args, **kwargs)
  453. def selection_set(self, *args, **kwargs):
  454. for lb in self._listboxes:
  455. lb.selection_set(*args, **kwargs)
  456. def yview(self, *args, **kwargs):
  457. for lb in self._listboxes:
  458. v = lb.yview(*args, **kwargs)
  459. return v # if called with no arguments
  460. def yview_moveto(self, *args, **kwargs):
  461. for lb in self._listboxes:
  462. lb.yview_moveto(*args, **kwargs)
  463. def yview_scroll(self, *args, **kwargs):
  464. for lb in self._listboxes:
  465. lb.yview_scroll(*args, **kwargs)
  466. # /////////////////////////////////////////////////////////////////
  467. # Aliases
  468. # /////////////////////////////////////////////////////////////////
  469. itemconfig = itemconfigure
  470. rowconfig = rowconfigure
  471. columnconfig = columnconfigure
  472. select_anchor = selection_anchor
  473. select_clear = selection_clear
  474. select_includes = selection_includes
  475. select_set = selection_set
  476. # /////////////////////////////////////////////////////////////////
  477. # These listbox methods are not defined for multi-listbox
  478. # /////////////////////////////////////////////////////////////////
  479. # def xview(self, *what): pass
  480. # def xview_moveto(self, fraction): pass
  481. # def xview_scroll(self, number, what): pass
  482. ######################################################################
  483. # Table
  484. ######################################################################
  485. class Table(object):
  486. """
  487. A display widget for a table of values, based on a ``MultiListbox``
  488. widget. For many purposes, ``Table`` can be treated as a
  489. list-of-lists. E.g., table[i] is a list of the values for row i;
  490. and table.append(row) adds a new row with the given lits of
  491. values. Individual cells can be accessed using table[i,j], which
  492. refers to the j-th column of the i-th row. This can be used to
  493. both read and write values from the table. E.g.:
  494. >>> table[i,j] = 'hello'
  495. The column (j) can be given either as an index number, or as a
  496. column name. E.g., the following prints the value in the 3rd row
  497. for the 'First Name' column:
  498. >>> print(table[3, 'First Name'])
  499. John
  500. You can configure the colors for individual rows, columns, or
  501. cells using ``rowconfig()``, ``columnconfig()``, and ``itemconfig()``.
  502. The color configuration for each row will be preserved if the
  503. table is modified; however, when new rows are added, any color
  504. configurations that have been made for *columns* will not be
  505. applied to the new row.
  506. Note: Although ``Table`` acts like a widget in some ways (e.g., it
  507. defines ``grid()``, ``pack()``, and ``bind()``), it is not itself a
  508. widget; it just contains one. This is because widgets need to
  509. define ``__getitem__()``, ``__setitem__()``, and ``__nonzero__()`` in
  510. a way that's incompatible with the fact that ``Table`` behaves as a
  511. list-of-lists.
  512. :ivar _mlb: The multi-column listbox used to display this table's data.
  513. :ivar _rows: A list-of-lists used to hold the cell values of this
  514. table. Each element of _rows is a row value, i.e., a list of
  515. cell values, one for each column in the row.
  516. """
  517. def __init__(
  518. self,
  519. master,
  520. column_names,
  521. rows=None,
  522. column_weights=None,
  523. scrollbar=True,
  524. click_to_sort=True,
  525. reprfunc=None,
  526. cnf={},
  527. **kw
  528. ):
  529. """
  530. Construct a new Table widget.
  531. :type master: Tkinter.Widget
  532. :param master: The widget that should contain the new table.
  533. :type column_names: list(str)
  534. :param column_names: A list of names for the columns; these
  535. names will be used to create labels for each column;
  536. and can be used as an index when reading or writing
  537. cell values from the table.
  538. :type rows: list(list)
  539. :param rows: A list of row values used to initialze the table.
  540. Each row value should be a tuple of cell values, one for
  541. each column in the row.
  542. :type scrollbar: bool
  543. :param scrollbar: If true, then create a scrollbar for the
  544. new table widget.
  545. :type click_to_sort: bool
  546. :param click_to_sort: If true, then create bindings that will
  547. sort the table's rows by a given column's values if the
  548. user clicks on that colum's label.
  549. :type reprfunc: function
  550. :param reprfunc: If specified, then use this function to
  551. convert each table cell value to a string suitable for
  552. display. ``reprfunc`` has the following signature:
  553. reprfunc(row_index, col_index, cell_value) -> str
  554. (Note that the column is specified by index, not by name.)
  555. :param cnf, kw: Configuration parameters for this widget's
  556. contained ``MultiListbox``. See ``MultiListbox.__init__()``
  557. for details.
  558. """
  559. self._num_columns = len(column_names)
  560. self._reprfunc = reprfunc
  561. self._frame = Frame(master)
  562. self._column_name_to_index = dict((c, i) for (i, c) in enumerate(column_names))
  563. # Make a copy of the rows & check that it's valid.
  564. if rows is None:
  565. self._rows = []
  566. else:
  567. self._rows = [[v for v in row] for row in rows]
  568. for row in self._rows:
  569. self._checkrow(row)
  570. # Create our multi-list box.
  571. self._mlb = MultiListbox(self._frame, column_names, column_weights, cnf, **kw)
  572. self._mlb.pack(side='left', expand=True, fill='both')
  573. # Optional scrollbar
  574. if scrollbar:
  575. sb = Scrollbar(self._frame, orient='vertical', command=self._mlb.yview)
  576. self._mlb.listboxes[0]['yscrollcommand'] = sb.set
  577. # for listbox in self._mlb.listboxes:
  578. # listbox['yscrollcommand'] = sb.set
  579. sb.pack(side='right', fill='y')
  580. self._scrollbar = sb
  581. # Set up sorting
  582. self._sortkey = None
  583. if click_to_sort:
  584. for i, l in enumerate(self._mlb.column_labels):
  585. l.bind('<Button-1>', self._sort)
  586. # Fill in our multi-list box.
  587. self._fill_table()
  588. # /////////////////////////////////////////////////////////////////
  589. # { Widget-like Methods
  590. # /////////////////////////////////////////////////////////////////
  591. # These all just delegate to either our frame or our MLB.
  592. def pack(self, *args, **kwargs):
  593. """Position this table's main frame widget in its parent
  594. widget. See ``Tkinter.Frame.pack()`` for more info."""
  595. self._frame.pack(*args, **kwargs)
  596. def grid(self, *args, **kwargs):
  597. """Position this table's main frame widget in its parent
  598. widget. See ``Tkinter.Frame.grid()`` for more info."""
  599. self._frame.grid(*args, **kwargs)
  600. def focus(self):
  601. """Direct (keyboard) input foxus to this widget."""
  602. self._mlb.focus()
  603. def bind(self, sequence=None, func=None, add=None):
  604. """Add a binding to this table's main frame that will call
  605. ``func`` in response to the event sequence."""
  606. self._mlb.bind(sequence, func, add)
  607. def rowconfigure(self, row_index, cnf={}, **kw):
  608. """:see: ``MultiListbox.rowconfigure()``"""
  609. self._mlb.rowconfigure(row_index, cnf, **kw)
  610. def columnconfigure(self, col_index, cnf={}, **kw):
  611. """:see: ``MultiListbox.columnconfigure()``"""
  612. col_index = self.column_index(col_index)
  613. self._mlb.columnconfigure(col_index, cnf, **kw)
  614. def itemconfigure(self, row_index, col_index, cnf=None, **kw):
  615. """:see: ``MultiListbox.itemconfigure()``"""
  616. col_index = self.column_index(col_index)
  617. return self._mlb.itemconfigure(row_index, col_index, cnf, **kw)
  618. def bind_to_labels(self, sequence=None, func=None, add=None):
  619. """:see: ``MultiListbox.bind_to_labels()``"""
  620. return self._mlb.bind_to_labels(sequence, func, add)
  621. def bind_to_listboxes(self, sequence=None, func=None, add=None):
  622. """:see: ``MultiListbox.bind_to_listboxes()``"""
  623. return self._mlb.bind_to_listboxes(sequence, func, add)
  624. def bind_to_columns(self, sequence=None, func=None, add=None):
  625. """:see: ``MultiListbox.bind_to_columns()``"""
  626. return self._mlb.bind_to_columns(sequence, func, add)
  627. rowconfig = rowconfigure
  628. columnconfig = columnconfigure
  629. itemconfig = itemconfigure
  630. # /////////////////////////////////////////////////////////////////
  631. # { Table as list-of-lists
  632. # /////////////////////////////////////////////////////////////////
  633. def insert(self, row_index, rowvalue):
  634. """
  635. Insert a new row into the table, so that its row index will be
  636. ``row_index``. If the table contains any rows whose row index
  637. is greater than or equal to ``row_index``, then they will be
  638. shifted down.
  639. :param rowvalue: A tuple of cell values, one for each column
  640. in the new row.
  641. """
  642. self._checkrow(rowvalue)
  643. self._rows.insert(row_index, rowvalue)
  644. if self._reprfunc is not None:
  645. rowvalue = [
  646. self._reprfunc(row_index, j, v) for (j, v) in enumerate(rowvalue)
  647. ]
  648. self._mlb.insert(row_index, rowvalue)
  649. if self._DEBUG:
  650. self._check_table_vs_mlb()
  651. def extend(self, rowvalues):
  652. """
  653. Add new rows at the end of the table.
  654. :param rowvalues: A list of row values used to initialze the
  655. table. Each row value should be a tuple of cell values,
  656. one for each column in the row.
  657. """
  658. for rowvalue in rowvalues:
  659. self.append(rowvalue)
  660. if self._DEBUG:
  661. self._check_table_vs_mlb()
  662. def append(self, rowvalue):
  663. """
  664. Add a new row to the end of the table.
  665. :param rowvalue: A tuple of cell values, one for each column
  666. in the new row.
  667. """
  668. self.insert(len(self._rows), rowvalue)
  669. if self._DEBUG:
  670. self._check_table_vs_mlb()
  671. def clear(self):
  672. """
  673. Delete all rows in this table.
  674. """
  675. self._rows = []
  676. self._mlb.delete(0, 'end')
  677. if self._DEBUG:
  678. self._check_table_vs_mlb()
  679. def __getitem__(self, index):
  680. """
  681. Return the value of a row or a cell in this table. If
  682. ``index`` is an integer, then the row value for the ``index``th
  683. row. This row value consists of a tuple of cell values, one
  684. for each column in the row. If ``index`` is a tuple of two
  685. integers, ``(i,j)``, then return the value of the cell in the
  686. ``i``th row and the ``j``th column.
  687. """
  688. if isinstance(index, slice):
  689. raise ValueError('Slicing not supported')
  690. elif isinstance(index, tuple) and len(index) == 2:
  691. return self._rows[index[0]][self.column_index(index[1])]
  692. else:
  693. return tuple(self._rows[index])
  694. def __setitem__(self, index, val):
  695. """
  696. Replace the value of a row or a cell in this table with
  697. ``val``.
  698. If ``index`` is an integer, then ``val`` should be a row value
  699. (i.e., a tuple of cell values, one for each column). In this
  700. case, the values of the ``index``th row of the table will be
  701. replaced with the values in ``val``.
  702. If ``index`` is a tuple of integers, ``(i,j)``, then replace the
  703. value of the cell in the ``i``th row and ``j``th column with
  704. ``val``.
  705. """
  706. if isinstance(index, slice):
  707. raise ValueError('Slicing not supported')
  708. # table[i,j] = val
  709. elif isinstance(index, tuple) and len(index) == 2:
  710. i, j = index[0], self.column_index(index[1])
  711. config_cookie = self._save_config_info([i])
  712. self._rows[i][j] = val
  713. if self._reprfunc is not None:
  714. val = self._reprfunc(i, j, val)
  715. self._mlb.listboxes[j].insert(i, val)
  716. self._mlb.listboxes[j].delete(i + 1)
  717. self._restore_config_info(config_cookie)
  718. # table[i] = val
  719. else:
  720. config_cookie = self._save_config_info([index])
  721. self._checkrow(val)
  722. self._rows[index] = list(val)
  723. if self._reprfunc is not None:
  724. val = [self._reprfunc(index, j, v) for (j, v) in enumerate(val)]
  725. self._mlb.insert(index, val)
  726. self._mlb.delete(index + 1)
  727. self._restore_config_info(config_cookie)
  728. def __delitem__(self, row_index):
  729. """
  730. Delete the ``row_index``th row from this table.
  731. """
  732. if isinstance(row_index, slice):
  733. raise ValueError('Slicing not supported')
  734. if isinstance(row_index, tuple) and len(row_index) == 2:
  735. raise ValueError('Cannot delete a single cell!')
  736. del self._rows[row_index]
  737. self._mlb.delete(row_index)
  738. if self._DEBUG:
  739. self._check_table_vs_mlb()
  740. def __len__(self):
  741. """
  742. :return: the number of rows in this table.
  743. """
  744. return len(self._rows)
  745. def _checkrow(self, rowvalue):
  746. """
  747. Helper function: check that a given row value has the correct
  748. number of elements; and if not, raise an exception.
  749. """
  750. if len(rowvalue) != self._num_columns:
  751. raise ValueError(
  752. 'Row %r has %d columns; expected %d'
  753. % (rowvalue, len(rowvalue), self._num_columns)
  754. )
  755. # /////////////////////////////////////////////////////////////////
  756. # Columns
  757. # /////////////////////////////////////////////////////////////////
  758. @property
  759. def column_names(self):
  760. """A list of the names of the columns in this table."""
  761. return self._mlb.column_names
  762. def column_index(self, i):
  763. """
  764. If ``i`` is a valid column index integer, then return it as is.
  765. Otherwise, check if ``i`` is used as the name for any column;
  766. if so, return that column's index. Otherwise, raise a
  767. ``KeyError`` exception.
  768. """
  769. if isinstance(i, int) and 0 <= i < self._num_columns:
  770. return i
  771. else:
  772. # This raises a key error if the column is not found.
  773. return self._column_name_to_index[i]
  774. def hide_column(self, column_index):
  775. """:see: ``MultiListbox.hide_column()``"""
  776. self._mlb.hide_column(self.column_index(column_index))
  777. def show_column(self, column_index):
  778. """:see: ``MultiListbox.show_column()``"""
  779. self._mlb.show_column(self.column_index(column_index))
  780. # /////////////////////////////////////////////////////////////////
  781. # Selection
  782. # /////////////////////////////////////////////////////////////////
  783. def selected_row(self):
  784. """
  785. Return the index of the currently selected row, or None if
  786. no row is selected. To get the row value itself, use
  787. ``table[table.selected_row()]``.
  788. """
  789. sel = self._mlb.curselection()
  790. if sel:
  791. return int(sel[0])
  792. else:
  793. return None
  794. def select(self, index=None, delta=None, see=True):
  795. """:see: ``MultiListbox.select()``"""
  796. self._mlb.select(index, delta, see)
  797. # /////////////////////////////////////////////////////////////////
  798. # Sorting
  799. # /////////////////////////////////////////////////////////////////
  800. def sort_by(self, column_index, order='toggle'):
  801. """
  802. Sort the rows in this table, using the specified column's
  803. values as a sort key.
  804. :param column_index: Specifies which column to sort, using
  805. either a column index (int) or a column's label name
  806. (str).
  807. :param order: Specifies whether to sort the values in
  808. ascending or descending order:
  809. - ``'ascending'``: Sort from least to greatest.
  810. - ``'descending'``: Sort from greatest to least.
  811. - ``'toggle'``: If the most recent call to ``sort_by()``
  812. sorted the table by the same column (``column_index``),
  813. then reverse the rows; otherwise sort in ascending
  814. order.
  815. """
  816. if order not in ('ascending', 'descending', 'toggle'):
  817. raise ValueError(
  818. 'sort_by(): order should be "ascending", ' '"descending", or "toggle".'
  819. )
  820. column_index = self.column_index(column_index)
  821. config_cookie = self._save_config_info(index_by_id=True)
  822. # Sort the rows.
  823. if order == 'toggle' and column_index == self._sortkey:
  824. self._rows.reverse()
  825. else:
  826. self._rows.sort(
  827. key=operator.itemgetter(column_index), reverse=(order == 'descending')
  828. )
  829. self._sortkey = column_index
  830. # Redraw the table.
  831. self._fill_table()
  832. self._restore_config_info(config_cookie, index_by_id=True, see=True)
  833. if self._DEBUG:
  834. self._check_table_vs_mlb()
  835. def _sort(self, event):
  836. """Event handler for clicking on a column label -- sort by
  837. that column."""
  838. column_index = event.widget.column_index
  839. # If they click on the far-left of far-right of a column's
  840. # label, then resize rather than sorting.
  841. if self._mlb._resize_column(event):
  842. return 'continue'
  843. # Otherwise, sort.
  844. else:
  845. self.sort_by(column_index)
  846. return 'continue'
  847. # /////////////////////////////////////////////////////////////////
  848. # { Table Drawing Helpers
  849. # /////////////////////////////////////////////////////////////////
  850. def _fill_table(self, save_config=True):
  851. """
  852. Re-draw the table from scratch, by clearing out the table's
  853. multi-column listbox; and then filling it in with values from
  854. ``self._rows``. Note that any cell-, row-, or column-specific
  855. color configuration that has been done will be lost. The
  856. selection will also be lost -- i.e., no row will be selected
  857. after this call completes.
  858. """
  859. self._mlb.delete(0, 'end')
  860. for i, row in enumerate(self._rows):
  861. if self._reprfunc is not None:
  862. row = [self._reprfunc(i, j, v) for (j, v) in enumerate(row)]
  863. self._mlb.insert('end', row)
  864. def _get_itemconfig(self, r, c):
  865. return dict(
  866. (k, self._mlb.itemconfig(r, c, k)[-1])
  867. for k in (
  868. 'foreground',
  869. 'selectforeground',
  870. 'background',
  871. 'selectbackground',
  872. )
  873. )
  874. def _save_config_info(self, row_indices=None, index_by_id=False):
  875. """
  876. Return a 'cookie' containing information about which row is
  877. selected, and what color configurations have been applied.
  878. this information can the be re-applied to the table (after
  879. making modifications) using ``_restore_config_info()``. Color
  880. configuration information will be saved for any rows in
  881. ``row_indices``, or in the entire table, if
  882. ``row_indices=None``. If ``index_by_id=True``, the the cookie
  883. will associate rows with their configuration information based
  884. on the rows' python id. This is useful when performing
  885. operations that re-arrange the rows (e.g. ``sort``). If
  886. ``index_by_id=False``, then it is assumed that all rows will be
  887. in the same order when ``_restore_config_info()`` is called.
  888. """
  889. # Default value for row_indices is all rows.
  890. if row_indices is None:
  891. row_indices = list(range(len(self._rows)))
  892. # Look up our current selection.
  893. selection = self.selected_row()
  894. if index_by_id and selection is not None:
  895. selection = id(self._rows[selection])
  896. # Look up the color configuration info for each row.
  897. if index_by_id:
  898. config = dict(
  899. (
  900. id(self._rows[r]),
  901. [self._get_itemconfig(r, c) for c in range(self._num_columns)],
  902. )
  903. for r in row_indices
  904. )
  905. else:
  906. config = dict(
  907. (r, [self._get_itemconfig(r, c) for c in range(self._num_columns)])
  908. for r in row_indices
  909. )
  910. return selection, config
  911. def _restore_config_info(self, cookie, index_by_id=False, see=False):
  912. """
  913. Restore selection & color configuration information that was
  914. saved using ``_save_config_info``.
  915. """
  916. selection, config = cookie
  917. # Clear the selection.
  918. if selection is None:
  919. self._mlb.selection_clear(0, 'end')
  920. # Restore selection & color config
  921. if index_by_id:
  922. for r, row in enumerate(self._rows):
  923. if id(row) in config:
  924. for c in range(self._num_columns):
  925. self._mlb.itemconfigure(r, c, config[id(row)][c])
  926. if id(row) == selection:
  927. self._mlb.select(r, see=see)
  928. else:
  929. if selection is not None:
  930. self._mlb.select(selection, see=see)
  931. for r in config:
  932. for c in range(self._num_columns):
  933. self._mlb.itemconfigure(r, c, config[r][c])
  934. # /////////////////////////////////////////////////////////////////
  935. # Debugging (Invariant Checker)
  936. # /////////////////////////////////////////////////////////////////
  937. _DEBUG = False
  938. """If true, then run ``_check_table_vs_mlb()`` after any operation
  939. that modifies the table."""
  940. def _check_table_vs_mlb(self):
  941. """
  942. Verify that the contents of the table's ``_rows`` variable match
  943. the contents of its multi-listbox (``_mlb``). This is just
  944. included for debugging purposes, to make sure that the
  945. list-modifying operations are working correctly.
  946. """
  947. for col in self._mlb.listboxes:
  948. assert len(self) == col.size()
  949. for row in self:
  950. assert len(row) == self._num_columns
  951. assert self._num_columns == len(self._mlb.column_names)
  952. # assert self._column_names == self._mlb.column_names
  953. for i, row in enumerate(self):
  954. for j, cell in enumerate(row):
  955. if self._reprfunc is not None:
  956. cell = self._reprfunc(i, j, cell)
  957. assert self._mlb.get(i)[j] == cell
  958. ######################################################################
  959. # Demo/Test Function
  960. ######################################################################
  961. # update this to use new WordNet API
  962. def demo():
  963. root = Tk()
  964. root.bind('<Control-q>', lambda e: root.destroy())
  965. table = Table(
  966. root,
  967. 'Word Synset Hypernym Hyponym'.split(),
  968. column_weights=[0, 1, 1, 1],
  969. reprfunc=(lambda i, j, s: ' %s' % s),
  970. )
  971. table.pack(expand=True, fill='both')
  972. from nltk.corpus import wordnet
  973. from nltk.corpus import brown
  974. for word, pos in sorted(set(brown.tagged_words()[:500])):
  975. if pos[0] != 'N':
  976. continue
  977. word = word.lower()
  978. for synset in wordnet.synsets(word):
  979. try:
  980. hyper_def = synset.hypernyms()[0].definition()
  981. except:
  982. hyper_def = '*none*'
  983. try:
  984. hypo_def = synset.hypernyms()[0].definition()
  985. except:
  986. hypo_def = '*none*'
  987. table.append([word, synset.definition(), hyper_def, hypo_def])
  988. table.columnconfig('Word', background='#afa')
  989. table.columnconfig('Synset', background='#efe')
  990. table.columnconfig('Hypernym', background='#fee')
  991. table.columnconfig('Hyponym', background='#ffe')
  992. for row in range(len(table)):
  993. for column in ('Hypernym', 'Hyponym'):
  994. if table[row, column] == '*none*':
  995. table.itemconfig(
  996. row, column, foreground='#666', selectforeground='#666'
  997. )
  998. root.mainloop()
  999. if __name__ == '__main__':
  1000. demo()