summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2023-11-27 18:57:33 (GMT)
committerGitHub <noreply@github.com>2023-11-27 18:57:33 (GMT)
commit4dcfd02bed0d7958703ef44baa79a4a98475be2e (patch)
tree3fb7e4b6b595115e1818f2d26d45b9063a3ab9bd
parent45d648597b1146431bf3d91041e60d7f040e70bf (diff)
downloadcpython-4dcfd02bed0d7958703ef44baa79a4a98475be2e.zip
cpython-4dcfd02bed0d7958703ef44baa79a4a98475be2e.tar.gz
cpython-4dcfd02bed0d7958703ef44baa79a4a98475be2e.tar.bz2
gh-68166: Add support of "vsapi" in ttk.Style.element_create() (GH-111393)
-rw-r--r--Doc/library/tkinter.ttk.rst60
-rw-r--r--Doc/whatsnew/3.13.rst5
-rw-r--r--Lib/test/test_ttk/test_style.py82
-rw-r--r--Lib/test/test_ttk_textonly.py32
-rw-r--r--Lib/tkinter/ttk.py55
-rw-r--r--Misc/NEWS.d/next/Library/2023-11-08-18-53-07.gh-issue-68166.1iTh4Y.rst2
6 files changed, 204 insertions, 32 deletions
diff --git a/Doc/library/tkinter.ttk.rst b/Doc/library/tkinter.ttk.rst
index 5fab145..6e01ec7 100644
--- a/Doc/library/tkinter.ttk.rst
+++ b/Doc/library/tkinter.ttk.rst
@@ -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()
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index ec09dfe..dad49f4 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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
---------
diff --git a/Lib/test/test_ttk/test_style.py b/Lib/test/test_ttk/test_style.py
index 52c4b7a0..9a04a95 100644
--- a/Lib/test/test_ttk/test_style.py
+++ b/Lib/test/test_ttk/test_style.py
@@ -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()
diff --git a/Lib/test/test_ttk_textonly.py b/Lib/test/test_ttk_textonly.py
index 96dc179..e6525c4 100644
--- a/Lib/test/test_ttk_textonly.py
+++ b/Lib/test/test_ttk_textonly.py
@@ -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(
diff --git a/Lib/tkinter/ttk.py b/Lib/tkinter/ttk.py
index efeabb7..5ca938a 100644
--- a/Lib/tkinter/ttk.py
+++ b/Lib/tkinter/ttk.py
@@ -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):
diff --git a/Misc/NEWS.d/next/Library/2023-11-08-18-53-07.gh-issue-68166.1iTh4Y.rst b/Misc/NEWS.d/next/Library/2023-11-08-18-53-07.gh-issue-68166.1iTh4Y.rst
new file mode 100644
index 0000000..30379b8
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-11-08-18-53-07.gh-issue-68166.1iTh4Y.rst
@@ -0,0 +1,2 @@
+Add support of the "vsapi" element type in
+:meth:`tkinter.ttk.Style.element_create`.