Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a2a46f3
Add ttk::spinbox
alandmoore Jan 17, 2018
505c77d
Added NEWS.d/next entry
alandmoore Jan 18, 2018
c5cbac7
Removed double colon that was causing a warning/error during build.
alandmoore Jan 18, 2018
7b8219e
Add more arguments to ttk.Spinbox docstring
alandmoore Jan 19, 2018
826f4e3
Make spinbox inherit Combobox rather than Entry
alandmoore Jan 19, 2018
3228370
Expand on usage information in Spinbox docstring
alandmoore Jan 19, 2018
af4e1a1
Revert Spinbox to Entry baseclass, add set/current methods
alandmoore Jan 20, 2018
16a5b98
Keep __all__ alphabetzied
alandmoore Jan 21, 2018
a56a970
Test class for ttk.Spinbox
alandmoore Jan 21, 2018
1a3b7c1
Documentation for ttk.Spinbox
alandmoore Jan 21, 2018
a933457
Remove nonexistent "current" method
alandmoore Jan 21, 2018
6d4f9b5
Update spinbox-related documentation
alandmoore Jan 21, 2018
aa37596
Remove some blank lines and reformat some long ones
alandmoore Jan 21, 2018
02cb17b
Add patch author's name to ACKS
alandmoore Jan 21, 2018
032b44f
Added tkinter.ttk.Spinbox addition.
alandmoore Jan 21, 2018
eecd3d4
Remove tabs inserted by accident
alandmoore Jan 21, 2018
61d2aa0
Remove tab introduced by bad editor setting
alandmoore Jan 21, 2018
95bae22
Resolved merge conflict on whatsnew file
alandmoore Jan 22, 2018
3a5eadb
Fix table cell indent for Spinbox docs
alandmoore Jan 22, 2018
b533b2e
Merge branch 'master' into fix-issue-32585
alandmoore Jan 24, 2018
2aa62c3
Fix role-less inline code
alandmoore Jan 24, 2018
9f91776
Merge remote-tracking branch 'origin/fix-issue-32585' into fix-issue-…
alandmoore Jan 24, 2018
2df569d
add SpinboxTest to test_gui tuple
alandmoore Feb 7, 2018
8387517
Fix tests on HiDPI displays.
serhiy-storchaka Feb 7, 2018
1ef7ca0
Add missing options, update_idletasks to Spinbox tests
alandmoore Feb 7, 2018
bf8bd5c
Merge branch 'fix-issue-32585' of https://github.com/alandmoore/cpyth…
alandmoore Feb 7, 2018
92ecb6c
add update() to spinbox tests to ensure callbacks are called
alandmoore Feb 7, 2018
53046dc
Use update() after setting a command in the test.
serhiy-storchaka Feb 8, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 86 additions & 5 deletions Doc/library/tkinter.ttk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
--------

Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,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
----

Expand Down
179 changes: 178 additions & 1 deletion Lib/tkinter/test/test_ttk/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,183 @@ def test_traversal(self):
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):
Expand Down Expand Up @@ -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__":
Expand Down
29 changes: 28 additions & 1 deletion Lib/tkinter/ttk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be Combobox.__init__ now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite right. Is there a particular reason why none of the classes in this library use super()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually going to revert the base class to Entry per @terryjreedy 's comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure super came years later than the original Tkinter module. In 2.x it was a bit harder to use. For single inheritance, I don't think that there is much advantage. (I believe that there are cases of multiple inheritance in tkinter.init, but have not rechecked.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this should have a base class of Entry, but with that, I also think current and set from Combobox should be included here. It would probably be overkill, but maybe Spinbox and Combobox should both inherit from an Entry subclass that has current and set?

The tests will be able to use current and set.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current and set methods correspond the current and set subcommands of Ttk widgets. The ttk::entry widget doesn't have these commands.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant that both Spinbox and Combobox have current and set. I didn't know if it was better to implement them both directly from Entry or to have another class in between, that is a subclass of Entry that implements current and set and is the superclass to Combobox and Spinbox.



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.

Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,7 @@ The Dragon De Monsyne
Bastien Montagne
Skip Montanaro
Peter Moody
Alan D. Moore
Paul Moore
Ross Moore
Ben Morgan
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Ttk spinbox widget to to tkinter.ttk. Patch by Alan D Moore.