bpo-32585: Add tkinter.ttk.Spinbox. (#5221)

This commit is contained in:
Alan D Moore 2018-02-08 18:03:55 -06:00 committed by Serhiy Storchaka
parent 32921f9082
commit a48e78a0b7
6 changed files with 300 additions and 7 deletions

View File

@ -66,13 +66,13 @@ for improved styling effects.
Ttk Widgets
-----------
Ttk comes with 17 widgets, eleven of which already existed in tkinter:
Ttk comes with 18 widgets, twelve of which already existed in tkinter:
:class:`Button`, :class:`Checkbutton`, :class:`Entry`, :class:`Frame`,
:class:`Label`, :class:`LabelFrame`, :class:`Menubutton`, :class:`PanedWindow`,
:class:`Radiobutton`, :class:`Scale` and :class:`Scrollbar`. The other six are
new: :class:`Combobox`, :class:`Notebook`, :class:`Progressbar`,
:class:`Separator`, :class:`Sizegrip` and :class:`Treeview`. And all them are
subclasses of :class:`Widget`.
:class:`Radiobutton`, :class:`Scale`, :class:`Scrollbar`, and :class:`Spinbox`.
The other six are new: :class:`Combobox`, :class:`Notebook`,
:class:`Progressbar`, :class:`Separator`, :class:`Sizegrip` and
:class:`Treeview`. And all them are subclasses of :class:`Widget`.
Using the Ttk widgets gives the application an improved look and feel.
As discussed above, there are differences in how the styling is coded.
@ -381,6 +381,87 @@ ttk.Combobox
Sets the value of the combobox to *value*.
Spinbox
-------
The :class:`ttk.Spinbox` widget is a :class:`ttk.Entry` enhanced with increment
and decrement arrows. It can be used for numbers or lists of string values.
This widget is a subclass of :class:`Entry`.
Besides the methods inherited from :class:`Widget`: :meth:`Widget.cget`,
:meth:`Widget.configure`, :meth:`Widget.identify`, :meth:`Widget.instate`
and :meth:`Widget.state`, and the following inherited from :class:`Entry`:
:meth:`Entry.bbox`, :meth:`Entry.delete`, :meth:`Entry.icursor`,
:meth:`Entry.index`, :meth:`Entry.insert`, :meth:`Entry.xview`,
it has some other methods, described at :class:`ttk.Spinbox`.
Options
^^^^^^^
This widget accepts the following specific options:
.. tabularcolumns:: |l|L|
+----------------------+------------------------------------------------------+
| Option | Description |
+======================+======================================================+
| from | Float value. If set, this is the minimum value to |
| | which the decrement button will decrement. Must be |
| | spelled as ``from_`` when used as an argument, since |
| | ``from`` is a Python keyword. |
+----------------------+------------------------------------------------------+
| to | Float value. If set, this is the maximum value to |
| | which the increment button will increment. |
+----------------------+------------------------------------------------------+
| increment | Float value. Specifies the amount which the |
| | increment/decrement buttons change the |
| | value. Defaults to 1.0. |
+----------------------+------------------------------------------------------+
| values | Sequence of string or float values. If specified, |
| | the increment/decrement buttons will cycle through |
| | the items in this sequence rather than incrementing |
| | or decrementing numbers. |
| | |
+----------------------+------------------------------------------------------+
| wrap | Boolean value. If ``True``, increment and decrement |
| | buttons will cycle from the ``to`` value to the |
| | ``from`` value or the ``from`` value to the ``to`` |
| | value, respectively. |
+----------------------+------------------------------------------------------+
| format | String value. This specifies the format of numbers |
| | set by the increment/decrement buttons. It must be |
| | in the form "%W.Pf", where W is the padded width of |
| | the value, P is the precision, and '%' and 'f' are |
| | literal. |
+----------------------+------------------------------------------------------+
| command | Python callable. Will be called with no arguments |
| | whenever either of the increment or decrement buttons|
| | are pressed. |
| | |
+----------------------+------------------------------------------------------+
Virtual events
^^^^^^^^^^^^^^
The spinbox widget generates an **<<Increment>>** virtual event when the
user presses <Up>, and a **<<Decrement>>** virtual event when the user
presses <Down>.
ttk.Spinbox
^^^^^^^^^^^^
.. class:: Spinbox
.. method:: get()
Returns the current value of the spinbox.
.. method:: set(value)
Sets the value of the spinbox to *value*.
Notebook
--------

View File

@ -663,6 +663,12 @@ Added :attr:`sys.flags.dev_mode` flag for the new development mode.
Deprecated :func:`sys.set_coroutine_wrapper` and
:func:`sys.get_coroutine_wrapper`.
tkinter
-------
Added :class:`tkinter.ttk.Spinbox`.
time
----

View File

@ -1105,6 +1105,183 @@ class NotebookTest(AbstractWidgetTest, unittest.TestCase):
self.nb.event_generate('<Alt-a>')
self.assertEqual(self.nb.select(), str(self.child1))
@add_standard_options(IntegerSizeTests, StandardTtkOptionsTests)
class SpinboxTest(EntryTest, unittest.TestCase):
OPTIONS = (
'background', 'class', 'command', 'cursor', 'exportselection',
'font', 'foreground', 'format', 'from', 'increment',
'invalidcommand', 'justify', 'show', 'state', 'style',
'takefocus', 'textvariable', 'to', 'validate', 'validatecommand',
'values', 'width', 'wrap', 'xscrollcommand',
)
def setUp(self):
super().setUp()
self.spin = self.create()
self.spin.pack()
def create(self, **kwargs):
return ttk.Spinbox(self.root, **kwargs)
def _click_increment_arrow(self):
width = self.spin.winfo_width()
height = self.spin.winfo_height()
x = width - 5
y = height//2 - 5
self.spin.event_generate('<ButtonPress-1>', x=x, y=y)
self.spin.event_generate('<ButtonRelease-1>', x=x, y=y)
self.spin.update_idletasks()
def _click_decrement_arrow(self):
width = self.spin.winfo_width()
height = self.spin.winfo_height()
x = width - 5
y = height//2 + 4
self.spin.event_generate('<ButtonPress-1>', x=x, y=y)
self.spin.event_generate('<ButtonRelease-1>', x=x, y=y)
self.spin.update_idletasks()
def test_command(self):
success = []
self.spin['command'] = lambda: success.append(True)
self.spin.update()
self._click_increment_arrow()
self.spin.update()
self.assertTrue(success)
self._click_decrement_arrow()
self.assertEqual(len(success), 2)
# testing postcommand removal
self.spin['command'] = ''
self.spin.update_idletasks()
self._click_increment_arrow()
self._click_decrement_arrow()
self.spin.update()
self.assertEqual(len(success), 2)
def test_to(self):
self.spin['from'] = 0
self.spin['to'] = 5
self.spin.set(4)
self.spin.update()
self._click_increment_arrow() # 5
self.assertEqual(self.spin.get(), '5')
self._click_increment_arrow() # 5
self.assertEqual(self.spin.get(), '5')
def test_from(self):
self.spin['from'] = 1
self.spin['to'] = 10
self.spin.set(2)
self.spin.update()
self._click_decrement_arrow() # 1
self.assertEqual(self.spin.get(), '1')
self._click_decrement_arrow() # 1
self.assertEqual(self.spin.get(), '1')
def test_increment(self):
self.spin['from'] = 0
self.spin['to'] = 10
self.spin['increment'] = 4
self.spin.set(1)
self.spin.update()
self._click_increment_arrow() # 5
self.assertEqual(self.spin.get(), '5')
self.spin['increment'] = 2
self.spin.update()
self._click_decrement_arrow() # 3
self.assertEqual(self.spin.get(), '3')
def test_format(self):
self.spin.set(1)
self.spin['format'] = '%10.3f'
self.spin.update()
self._click_increment_arrow()
value = self.spin.get()
self.assertEqual(len(value), 10)
self.assertEqual(value.index('.'), 6)
self.spin['format'] = ''
self.spin.update()
self._click_increment_arrow()
value = self.spin.get()
self.assertTrue('.' not in value)
self.assertEqual(len(value), 1)
def test_wrap(self):
self.spin['to'] = 10
self.spin['from'] = 1
self.spin.set(1)
self.spin['wrap'] = True
self.spin.update()
self._click_decrement_arrow()
self.assertEqual(self.spin.get(), '10')
self._click_increment_arrow()
self.assertEqual(self.spin.get(), '1')
self.spin['wrap'] = False
self.spin.update()
self._click_decrement_arrow()
self.assertEqual(self.spin.get(), '1')
def test_values(self):
self.assertEqual(self.spin['values'],
() if tcl_version < (8, 5) else '')
self.checkParam(self.spin, 'values', 'mon tue wed thur',
expected=('mon', 'tue', 'wed', 'thur'))
self.checkParam(self.spin, 'values', ('mon', 'tue', 'wed', 'thur'))
self.checkParam(self.spin, 'values', (42, 3.14, '', 'any string'))
self.checkParam(
self.spin,
'values',
'',
expected='' if get_tk_patchlevel() < (8, 5, 10) else ()
)
self.spin['values'] = ['a', 1, 'c']
# test incrementing / decrementing values
self.spin.set('a')
self.spin.update()
self._click_increment_arrow()
self.assertEqual(self.spin.get(), '1')
self._click_decrement_arrow()
self.assertEqual(self.spin.get(), 'a')
# testing values with empty string set through configure
self.spin.configure(values=[1, '', 2])
self.assertEqual(self.spin['values'],
('1', '', '2') if self.wantobjects else
'1 {} 2')
# testing values with spaces
self.spin['values'] = ['a b', 'a\tb', 'a\nb']
self.assertEqual(self.spin['values'],
('a b', 'a\tb', 'a\nb') if self.wantobjects else
'{a b} {a\tb} {a\nb}')
# testing values with special characters
self.spin['values'] = [r'a\tb', '"a"', '} {']
self.assertEqual(self.spin['values'],
(r'a\tb', '"a"', '} {') if self.wantobjects else
r'a\\tb {"a"} \}\ \{')
# testing creating spinbox with empty string in values
spin2 = ttk.Spinbox(self.root, values=[1, 2, ''])
self.assertEqual(spin2['values'],
('1', '2', '') if self.wantobjects else '1 2 {}')
spin2.destroy()
@add_standard_options(StandardTtkOptionsTests)
class TreeviewTest(AbstractWidgetTest, unittest.TestCase):
@ -1679,7 +1856,7 @@ tests_gui = (
FrameTest, LabelFrameTest, LabelTest, MenubuttonTest,
NotebookTest, PanedWindowTest, ProgressbarTest,
RadiobuttonTest, ScaleTest, ScrollbarTest, SeparatorTest,
SizegripTest, TreeviewTest, WidgetTest,
SizegripTest, SpinboxTest, TreeviewTest, WidgetTest,
)
if __name__ == "__main__":

View File

@ -19,7 +19,7 @@ __author__ = "Guilherme Polo <ggpolo@gmail.com>"
__all__ = ["Button", "Checkbutton", "Combobox", "Entry", "Frame", "Label",
"Labelframe", "LabelFrame", "Menubutton", "Notebook", "Panedwindow",
"PanedWindow", "Progressbar", "Radiobutton", "Scale", "Scrollbar",
"Separator", "Sizegrip", "Style", "Treeview",
"Separator", "Sizegrip", "Spinbox", "Style", "Treeview",
# Extensions
"LabeledScale", "OptionMenu",
# functions
@ -1149,6 +1149,33 @@ class Sizegrip(Widget):
Widget.__init__(self, master, "ttk::sizegrip", kw)
class Spinbox(Entry):
"""Ttk Spinbox is an Entry with increment and decrement arrows
It is commonly used for number entry or to select from a list of
string values.
"""
def __init__(self, master=None, **kw):
"""Construct a Ttk Spinbox widget with the parent master.
STANDARD OPTIONS
class, cursor, style, takefocus, validate,
validatecommand, xscrollcommand, invalidcommand
WIDGET-SPECIFIC OPTIONS
to, from_, increment, values, wrap, format, command
"""
Entry.__init__(self, master, "ttk::spinbox", **kw)
def set(self, value):
"""Sets the value of the Spinbox to value."""
self.tk.call(self._w, "set", value)
class Treeview(Widget, tkinter.XView, tkinter.YView):
"""Ttk Treeview widget displays a hierarchical collection of items.

View File

@ -1073,6 +1073,7 @@ The Dragon De Monsyne
Bastien Montagne
Skip Montanaro
Peter Moody
Alan D. Moore
Paul Moore
Ross Moore
Ben Morgan

View File

@ -0,0 +1 @@
Add Ttk spinbox widget to to tkinter.ttk. Patch by Alan D Moore.