From a48e78a0b7761dd74f1d03fc69e0f6caa6f02fe6 Mon Sep 17 00:00:00 2001 From: Alan D Moore Date: Thu, 8 Feb 2018 18:03:55 -0600 Subject: bpo-32585: Add tkinter.ttk.Spinbox. (#5221) --- Doc/library/tkinter.ttk.rst | 91 ++++++++++- Doc/whatsnew/3.7.rst | 6 + Lib/tkinter/test/test_ttk/test_widgets.py | 179 ++++++++++++++++++++- Lib/tkinter/ttk.py | 29 +++- Misc/ACKS | 1 + .../2018-01-18-13-09-00.bpo-32585.qpeijr.rst | 1 + 6 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-01-18-13-09-00.bpo-32585.qpeijr.rst diff --git a/Doc/library/tkinter.ttk.rst b/Doc/library/tkinter.ttk.rst index 9c0c4cd..5ba31fe 100644 --- a/Doc/library/tkinter.ttk.rst +++ b/Doc/library/tkinter.ttk.rst @@ -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 **<>** virtual event when the +user presses , and a **<>** virtual event when the user +presses . + +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 -------- diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 8d4772f..3b4ba6e 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -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 ---- diff --git a/Lib/tkinter/test/test_ttk/test_widgets.py b/Lib/tkinter/test/test_ttk/test_widgets.py index 08f5fc3..bbc508d 100644 --- a/Lib/tkinter/test/test_ttk/test_widgets.py +++ b/Lib/tkinter/test/test_ttk/test_widgets.py @@ -1105,6 +1105,183 @@ class NotebookTest(AbstractWidgetTest, unittest.TestCase): self.nb.event_generate('') 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('', x=x, y=y) + self.spin.event_generate('', 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('', x=x, y=y) + self.spin.event_generate('', 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__": diff --git a/Lib/tkinter/ttk.py b/Lib/tkinter/ttk.py index 2ab5b59..490b502 100644 --- a/Lib/tkinter/ttk.py +++ b/Lib/tkinter/ttk.py @@ -19,7 +19,7 @@ __author__ = "Guilherme Polo " __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. diff --git a/Misc/ACKS b/Misc/ACKS index c5eadc5..4c8b86a 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1073,6 +1073,7 @@ The Dragon De Monsyne Bastien Montagne Skip Montanaro Peter Moody +Alan D. Moore Paul Moore Ross Moore Ben Morgan diff --git a/Misc/NEWS.d/next/Library/2018-01-18-13-09-00.bpo-32585.qpeijr.rst b/Misc/NEWS.d/next/Library/2018-01-18-13-09-00.bpo-32585.qpeijr.rst new file mode 100644 index 0000000..c504e8b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-01-18-13-09-00.bpo-32585.qpeijr.rst @@ -0,0 +1 @@ +Add Ttk spinbox widget to to tkinter.ttk. Patch by Alan D Moore. -- cgit v0.12