Skip to content

Commit

Permalink
pythongh-68166: Fix support of "vsapi" in ttk.Style.element_create()
Browse files Browse the repository at this point in the history
* Fix and document support of "vsapi" element type in
  tkinter.ttk.Style.element_create().
* Add tests for element_create() and other ttk.Style methods.
* Add examples for element_create() in the documentation.
  • Loading branch information
serhiy-storchaka committed Oct 27, 2023
1 parent 74f0772 commit 3432df2
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 32 deletions.
71 changes: 70 additions & 1 deletion Doc/library/tkinter.ttk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1392,7 +1392,7 @@ option. If you don't know the class name of a widget, use the method

Create a new element in the current theme, of the given *etype* which is
expected to be either "image", "from" or "vsapi". The latter is only
available in Tk 8.6a for Windows XP and Vista and is not described here.
available in Tk 8.6a for Windows XP and Vista.

If "image" is used, *args* should contain the default image name followed
by statespec/value pairs (this is the imagespec), and *kw* may have the
Expand All @@ -1418,13 +1418,82 @@ option. If you don't know the class name of a widget, use the method
Specifies a minimum width for the element. If less than zero, the
base image's width is used as a default.

Example::

img1 = tkinter.PhotoImage(master=root, file='button.png')
img1 = tkinter.PhotoImage(master=root, file='button-pressed.png')
img1 = tkinter.PhotoImage(master=root, file='button-active.png')
style = ttk.Style()
style.element_create('Button.button', 'image',
img1, ('pressed', img2), ('active', img3),
border=(2, 4), sticky='we')

If "from" is used as the value of *etype*,
:meth:`element_create` will clone an existing
element. *args* is expected to contain a themename, from which
the element will be cloned, and optionally an element to clone from.
If this element to clone from is not specified, an empty element will
be used. *kw* is discarded.

Example::

style = ttk.Style()
style.element_create('plain.background', 'from', 'default')

If "vsapi" is used as the value of *etype*, :meth:`element_create`
will create a new element in the current theme whose visual appearance
is drawn using the Microsoft Visual Styles API which is responsible
for the themed styles on Windows XP and Vista.
*args* is expected to contain the Visual Styles class and part as
given in the Microsoft documentation followed by tuples of ttk states
and the corresponding Visual Styles API state value.
*kw* may have the following options:

padding=padding
Specify the element's interior padding.
*padding* is a list of up to four integers specifying the left,
top, right and bottom padding quantities respectively.
If fewer than four elements are specified, bottom defaults to top,
right defaults to left, and top defaults to left.
In other words, a list of three numbers specify the left, vertical,
and right padding; a list of two numbers specify the horizontal
and the vertical padding; a single number specifies the same
padding all the way around the widget.
This option may not be mixed with any other options.

margins=padding
Specifies the elements exterior padding.
*padding* is a list of up to four integers specifying the left, top,
right and bottom padding quantities respectively.
This option may not be mixed with any other options.

width=width
Specifies the width for the element.
If this option is set then the Visual Styles API will not be queried
for the recommended size or the part.
If this option is set then *height* should also be set.
The *width* and *height* options cannot be mixed with the *padding*
or *margins* options.

height=height
Specifies the height of the element.
See the comments for *width*.

Example::

style = ttk.Style()
style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3,
('pressed', '!selected', 3),
('active', '!selected', 2),
('pressed', 'selected', 6),
('active', 'selected', 5),
('selected', 4),
('', 1))
style.layout('Explorer.Pin',
[('Explorer.Pin.pin', {'sticky': 'news'})])
pin = ttk.Checkbutton(style='Explorer.Pin')
pin.pack(expand=True, fill='both')


.. method:: element_names()

Expand Down
120 changes: 120 additions & 0 deletions Lib/test/test_ttk/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
import tkinter
from tkinter import ttk
from tkinter import TclError
from test import support
from test.support import requires
from test.test_tkinter.support import AbstractTkTest, get_tk_patchlevel
Expand Down Expand Up @@ -176,6 +177,125 @@ def test_map_custom_copy(self):
for key, value in default.items():
self.assertEqual(style.map(newname, key), value)

def test_element_options(self):
style = self.style
element_names = style.element_names()
self.assertNotIsInstance(element_names, str)
for name in element_names:
self.assertIsInstance(name, str)
element_options = style.element_options(name)
self.assertNotIsInstance(element_options, str)
for optname in element_options:
self.assertIsInstance(optname, str)

def test_element_create_errors(self):
style = self.style
with self.assertRaises(TypeError):
style.element_create('plain.newelem')
with self.assertRaisesRegex(TclError, 'No such element type spam'):
style.element_create('plain.newelem', 'spam')

def test_element_create_from(self):
style = self.style
style.element_create('plain.background', 'from', 'default')
self.assertIn('plain.background', style.element_names())
style.element_create('plain.arrow', 'from', 'default', 'rightarrow')
self.assertIn('plain.arrow', style.element_names())

def test_element_create_from_errors(self):
style = self.style
with self.assertRaises(IndexError):
style.element_create('plain.newelem', 'from')
with self.assertRaisesRegex(TclError, 'theme "spam" doesn\'t exist'):
style.element_create('plain.newelem', 'from', 'spam')

def test_element_create_image(self):
style = self.style
image = tkinter.PhotoImage(master=self.root, width=10, height=10)
style.element_create('block', 'image', image)
self.assertIn('block', style.element_names())

imgfile = support.findfile('python.xbm', subdir='tkinterdata')
img1 = tkinter.BitmapImage(master=self.root, file=imgfile,
foreground='yellow', background='blue')
img2 = tkinter.BitmapImage(master=self.root, file=imgfile,
foreground='blue', background='yellow')
img3 = tkinter.BitmapImage(master=self.root, file=imgfile,
foreground='white', background='black')
style.element_create('Button.button', 'image',
img1, ('pressed', img2), ('active', img3),
border=(2, 4), sticky='we')
self.assertIn('Button.button', style.element_names())

def test_element_create_image_errors(self):
style = self.style
image = tkinter.PhotoImage(master=self.root, width=10, height=10)
with self.assertRaises(IndexError):
style.element_create('block2', 'image')
with self.assertRaises(TypeError):
style.element_create('block2', 'image', image, 1)
with self.assertRaises(ValueError):
style.element_create('block2', 'image', image, ())
with self.assertRaisesRegex(TclError, 'Invalid state name'):
style.element_create('block2', 'image', image, ('spam', image))
with self.assertRaisesRegex(TclError, 'Invalid state name'):
style.element_create('block2', 'image', image, (1, image))
with self.assertRaises(TypeError):
style.element_create('block2', 'image', image, ('pressed', 1, image))
with self.assertRaises(TypeError):
style.element_create('block2', 'image', image, (1, 'selected', image))
with self.assertRaisesRegex(TclError, 'bad option'):
style.element_create('block2', 'image', image, spam=1)

def test_element_create_vsapi_1(self):
style = self.style
if 'xpnative' not in style.theme_names():
self.skipTest("requires 'xpnative' theme")
style.element_create('smallclose', 'vsapi', 'WINDOW', 19,
('disabled', 4),
('pressed', 3),
('active', 2),
('', 1))
style.layout('CloseButton',
[('CloseButton.smallclose', {'sticky': 'news'})])
b = ttk.Button(style='CloseButton')
b.pack(expand=True, fill='both')
self.assertEqual(b.winfo_reqwidth(), 13)
self.assertEqual(b.winfo_reqheight(), 13)

def test_element_create_vsapi_2(self):
style = self.style
if 'xpnative' not in style.theme_names():
self.skipTest("requires 'xpnative' theme")
style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3,
('pressed', '!selected', 3),
('active', '!selected', 2),
('pressed', 'selected', 6),
('active', 'selected', 5),
('selected', 4),
('', 1))
style.layout('Explorer.Pin',
[('Explorer.Pin.pin', {'sticky': 'news'})])
pin = ttk.Checkbutton(style='Explorer.Pin')
pin.pack(expand=True, fill='both')
self.assertEqual(pin.winfo_reqwidth(), 16)
self.assertEqual(pin.winfo_reqheight(), 16)

def test_element_create_vsapi_3(self):
style = self.style
if 'xpnative' not in style.theme_names():
self.skipTest("requires 'xpnative' theme")
style.element_create('headerclose', 'vsapi', 'EXPLORERBAR', 2,
('pressed', 3),
('active', 2),
('', 1))
style.layout('Explorer.CloseButton',
[('Explorer.CloseButton.headerclose', {'sticky': 'news'})])
b = ttk.Button(style='Explorer.CloseButton')
b.pack(expand=True, fill='both')
self.assertEqual(b.winfo_reqwidth(), 16)
self.assertEqual(b.winfo_reqheight(), 16)


if __name__ == "__main__":
unittest.main()
14 changes: 7 additions & 7 deletions Lib/test/test_ttk_textonly.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def test_format_elemcreate(self):
# don't format returned values as a tcl script
# minimum acceptable for image type
self.assertEqual(ttk._format_elemcreate('image', False, 'test'),
("test ", ()))
("test", ()))
# specifying a state spec
self.assertEqual(ttk._format_elemcreate('image', False, 'test',
('', 'a')), ("test {} a", ()))
Expand All @@ -203,17 +203,17 @@ def test_format_elemcreate(self):
# don't format returned values as a tcl script
# minimum acceptable for vsapi
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b'),
("a b ", ()))
('a', 'b', (), ()))
# now with a state spec with multiple states
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b',
('a', 'b', 'c')), ("a b {a b} c", ()))
('a', 'b', 'c')), ('a', 'b', ('a b', 'c'), ()))
# state spec and option
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b',
('a', 'b'), opt='x'), ("a b a b", ("-opt", "x")))
('a', 'b'), opt='x'), ('a', 'b', ('a', 'b'), ("-opt", "x")))
# format returned values as a tcl script
# state spec with a multivalue and an option
self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b',
('a', 'b', [1, 2]), opt='x'), ("{a b {a b} {1 2}}", "-opt x"))
('a', 'b', [1, 2]), opt='x'), ("a b {{a b} {1 2}}", "-opt x"))

# Testing type = from
# from type expects at least a type name
Expand All @@ -222,9 +222,9 @@ def test_format_elemcreate(self):
self.assertEqual(ttk._format_elemcreate('from', False, 'a'),
('a', ()))
self.assertEqual(ttk._format_elemcreate('from', False, 'a', 'b'),
('a', ('b', )))
('a', ('b',)))
self.assertEqual(ttk._format_elemcreate('from', True, 'a', 'b'),
('{a}', 'b'))
('a', 'b'))


def test_format_layoutlist(self):
Expand Down
51 changes: 27 additions & 24 deletions Lib/tkinter/ttk.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,40 +95,43 @@ def _format_mapdict(mapdict, script=False):

def _format_elemcreate(etype, script=False, *args, **kw):
"""Formats args and kw according to the given element factory etype."""
spec = None
specs = ()
opts = ()
if etype in ("image", "vsapi"):
if etype == "image": # define an element based on an image
# first arg should be the default image name
iname = args[0]
# next args, if any, are statespec/value pairs which is almost
# a mapdict, but we just need the value
imagespec = _join(_mapdict_values(args[1:]))
spec = "%s %s" % (iname, imagespec)

if etype == "image": # define an element based on an image
# first arg should be the default image name
iname = args[0]
# next args, if any, are statespec/value pairs which is almost
# a mapdict, but we just need the value
imagespec = (iname, *_mapdict_values(args[1:]))
if script:
specs = (imagespec,)
else:
# define an element whose visual appearance is drawn using the
# Microsoft Visual Styles API which is responsible for the
# themed styles on Windows XP and Vista.
# Availability: Tk 8.6, Windows XP and Vista.
class_name, part_id = args[:2]
statemap = _join(_mapdict_values(args[2:]))
spec = "%s %s %s" % (class_name, part_id, statemap)
specs = (_join(imagespec),)
opts = _format_optdict(kw, script)

if etype == "vsapi":
# define an element whose visual appearance is drawn using the
# Microsoft Visual Styles API which is responsible for the
# themed styles on Windows XP and Vista.
# Availability: Tk 8.6, Windows XP and Vista.
class_name, part_id, *statemap = args
specs = (class_name, part_id, tuple(_mapdict_values(statemap)))
opts = _format_optdict(kw, script)

elif etype == "from": # clone an element
# it expects a themename and optionally an element to clone from,
# otherwise it will clone {} (empty element)
spec = args[0] # theme name
specs = (args[0],) # theme name
if len(args) > 1: # elementfrom specified
opts = (_format_optvalue(args[1], script),)

if script:
spec = '{%s}' % spec
specs = _join(specs)
opts = ' '.join(opts)
return specs, opts
else:
return *specs, opts

return spec, opts

def _format_layoutlist(layout, indent=0, indent_size=2):
"""Formats a layout list so we can pass the result to ttk::style
Expand Down Expand Up @@ -214,10 +217,10 @@ def _script_from_settings(settings):

elemargs = eopts[1:argc]
elemkw = eopts[argc] if argc < len(eopts) and eopts[argc] else {}
spec, opts = _format_elemcreate(etype, True, *elemargs, **elemkw)
specs, opts = _format_elemcreate(etype, True, *elemargs, **elemkw)

script.append("ttk::style element create %s %s %s %s" % (
name, etype, spec, opts))
name, etype, specs, opts))

return '\n'.join(script)

Expand Down Expand Up @@ -434,9 +437,9 @@ def layout(self, style, layoutspec=None):

def element_create(self, elementname, etype, *args, **kw):
"""Create a new element in the current theme of given etype."""
spec, opts = _format_elemcreate(etype, False, *args, **kw)
*specs, opts = _format_elemcreate(etype, False, *args, **kw)
self.tk.call(self._name, "element", "create", elementname, etype,
spec, *opts)
*specs, *opts)


def element_names(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix and document support of "vsapi" element type in
:meth:`tkinter.ttk.Style.element_create`. Add tests for ``element_create()``
and other ``ttk.Style`` methods. Add examples for ``element_create()`` in
the documentation.

0 comments on commit 3432df2

Please sign in to comment.