[Python-checkins] bpo-32585: Add tkinter.ttk.Spinbox. (GH-5221) (GH-5592)

Serhiy Storchaka webhook-mailer at python.org
Fri Feb 9 06:40:17 EST 2018


https://github.com/python/cpython/commit/105fcbfd6a91abea0ecd71e53651f39ad6f6039b
commit: 105fcbfd6a91abea0ecd71e53651f39ad6f6039b
branch: 3.7
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: Serhiy Storchaka <storchaka at gmail.com>
date: 2018-02-09T13:40:14+02:00
summary:

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

(cherry picked from commit a48e78a0b7761dd74f1d03fc69e0f6caa6f02fe6)

Co-authored-by: Alan D Moore <me at alandmoore.com>

files:
A Misc/NEWS.d/next/Library/2018-01-18-13-09-00.bpo-32585.qpeijr.rst
M Doc/library/tkinter.ttk.rst
M Doc/whatsnew/3.7.rst
M Lib/tkinter/test/test_ttk/test_widgets.py
M Lib/tkinter/ttk.py
M Misc/ACKS

diff --git a/Doc/library/tkinter.ttk.rst b/Doc/library/tkinter.ttk.rst
index debbc81ce1a3..76ecfcce4977 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 **<<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
 --------
 
diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst
index 7b17c62e2562..fcd5188e7be2 100644
--- a/Doc/whatsnew/3.7.rst
+++ b/Doc/whatsnew/3.7.rst
@@ -655,6 +655,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 ab0db2878e13..5325e36b5212 100644
--- a/Lib/tkinter/test/test_ttk/test_widgets.py
+++ b/Lib/tkinter/test/test_ttk/test_widgets.py
@@ -1105,6 +1105,183 @@ def test_traversal(self):
             self.nb.event_generate('<Alt-a>')
         self.assertEqual(self.nb.select(), str(self.child1))
 
+ at 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):
@@ -1700,7 +1877,7 @@ def create(self, **kwargs):
         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 e6c90cef7bb1..c1651159b703 100644
--- a/Lib/tkinter/ttk.py
+++ b/Lib/tkinter/ttk.py
@@ -19,7 +19,7 @@
 __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
@@ -1151,6 +1151,33 @@ def __init__(self, master=None, **kw):
         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 c5eadc5ba017..4c8b86ac45d4 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 000000000000..c504e8b1e538
--- /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.



More information about the Python-checkins mailing list