table.py 44 KB

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