gh-68166: Add support of "vsapi" in ttk.Style.element_create() (GH-111393)

This commit is contained in:
Serhiy Storchaka 2023-11-27 20:57:33 +02:00 committed by GitHub
parent 45d648597b
commit 4dcfd02bed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 204 additions and 32 deletions

View File

@ -1391,7 +1391,8 @@ option. If you don't know the class name of a widget, use the method
.. method:: element_create(elementname, etype, *args, **kw)
Create a new element in the current theme, of the given *etype* which is
expected to be either "image" or "from".
expected to be either "image", "from" or "vsapi".
The latter is only available in Tk 8.6 on Windows.
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
@ -1439,6 +1440,63 @@ option. If you don't know the class name of a widget, use the method
style = ttk.Style(root)
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 an optional sequence
of 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(root)
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')
.. versionchanged:: 3.13
Added support of the "vsapi" element factory.
.. method:: element_names()

View File

@ -301,6 +301,11 @@ tkinter
:meth:`!tk_busy_current`, and :meth:`!tk_busy_status`.
(Contributed by Miguel, klappnase and Serhiy Storchaka in :gh:`72684`.)
* Add support of the "vsapi" element type in
the :meth:`~tkinter.ttk.Style.element_create` method of
:class:`tkinter.ttk.Style`.
(Contributed by Serhiy Storchaka in :gh:`68166`.)
traceback
---------

View File

@ -258,6 +258,55 @@ class StyleTest(AbstractTkTest, unittest.TestCase):
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(self.root, 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(self.root, 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(self.root, style='Explorer.CloseButton')
b.pack(expand=True, fill='both')
self.assertEqual(b.winfo_reqwidth(), 16)
self.assertEqual(b.winfo_reqheight(), 16)
def test_theme_create(self):
style = self.style
curr_theme = style.theme_use()
@ -358,6 +407,39 @@ class StyleTest(AbstractTkTest, unittest.TestCase):
style.theme_use(curr_theme)
def test_theme_create_vsapi(self):
style = self.style
if 'xpnative' not in style.theme_names():
self.skipTest("requires 'xpnative' theme")
curr_theme = style.theme_use()
new_theme = 'testtheme5'
style.theme_create(new_theme, settings={
'pin' : {
'element create': ['vsapi', 'EXPLORERBAR', 3, [
('pressed', '!selected', 3),
('active', '!selected', 2),
('pressed', 'selected', 6),
('active', 'selected', 5),
('selected', 4),
('', 1)]],
},
'Explorer.Pin' : {
'layout': [('Explorer.Pin.pin', {'sticky': 'news'})],
},
})
style.theme_use(new_theme)
self.assertIn('pin', style.element_names())
self.assertEqual(style.layout('Explorer.Pin'),
[('Explorer.Pin.pin', {'sticky': 'nswe'})])
pin = ttk.Checkbutton(self.root, style='Explorer.Pin')
pin.pack(expand=True, fill='both')
self.assertEqual(pin.winfo_reqwidth(), 16)
self.assertEqual(pin.winfo_reqheight(), 16)
style.theme_use(curr_theme)
if __name__ == "__main__":
unittest.main()

View File

@ -179,7 +179,7 @@ class InternalFunctionsTest(unittest.TestCase):
# 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", ()))
@ -203,17 +203,19 @@ class InternalFunctionsTest(unittest.TestCase):
# 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', ('', 1), ()))
# 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"))
opt='x'), ("a b {{} 1}", "-opt x"))
self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b',
[('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
@ -222,9 +224,9 @@ class InternalFunctionsTest(unittest.TestCase):
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):
@ -326,6 +328,22 @@ class InternalFunctionsTest(unittest.TestCase):
"ttk::style element create thing image {name {state1 state2} val} "
"-opt {3 2m}")
vsapi = {'pin': {'element create':
['vsapi', 'EXPLORERBAR', 3, [
('pressed', '!selected', 3),
('active', '!selected', 2),
('pressed', 'selected', 6),
('active', 'selected', 5),
('selected', 4),
('', 1)]]}}
self.assertEqual(ttk._script_from_settings(vsapi),
"ttk::style element create pin vsapi EXPLORERBAR 3 {"
"{pressed !selected} 3 "
"{active !selected} 2 "
"{pressed selected} 6 "
"{active selected} 5 "
"selected 4 "
"{} 1} ")
def test_tclobj_to_py(self):
self.assertEqual(

View File

@ -95,40 +95,47 @@ 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.
if len(args) < 3:
class_name, part_id = args
statemap = (((), 1),)
else:
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
@ -214,10 +221,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, eopts = _format_elemcreate(etype, True, *elemargs, **elemkw)
script.append("ttk::style element create %s %s %s %s" % (
name, etype, spec, opts))
name, etype, specs, eopts))
return '\n'.join(script)
@ -434,9 +441,9 @@ class Style(object):
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):

View File

@ -0,0 +1,2 @@
Add support of the "vsapi" element type in
:meth:`tkinter.ttk.Style.element_create`.