Skip to content

Commit 4dcfd02

Browse files
gh-68166: Add support of "vsapi" in ttk.Style.element_create() (GH-111393)
1 parent 45d6485 commit 4dcfd02

File tree

6 files changed

+204
-32
lines changed

6 files changed

+204
-32
lines changed

Doc/library/tkinter.ttk.rst

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1391,7 +1391,8 @@ option. If you don't know the class name of a widget, use the method
13911391
.. method:: element_create(elementname, etype, *args, **kw)
13921392

13931393
Create a new element in the current theme, of the given *etype* which is
1394-
expected to be either "image" or "from".
1394+
expected to be either "image", "from" or "vsapi".
1395+
The latter is only available in Tk 8.6 on Windows.
13951396

13961397
If "image" is used, *args* should contain the default image name followed
13971398
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
14391440
style = ttk.Style(root)
14401441
style.element_create('plain.background', 'from', 'default')
14411442

1443+
If "vsapi" is used as the value of *etype*, :meth:`element_create`
1444+
will create a new element in the current theme whose visual appearance
1445+
is drawn using the Microsoft Visual Styles API which is responsible
1446+
for the themed styles on Windows XP and Vista.
1447+
*args* is expected to contain the Visual Styles class and part as
1448+
given in the Microsoft documentation followed by an optional sequence
1449+
of tuples of ttk states and the corresponding Visual Styles API state
1450+
value.
1451+
*kw* may have the following options:
1452+
1453+
padding=padding
1454+
Specify the element's interior padding.
1455+
*padding* is a list of up to four integers specifying the left,
1456+
top, right and bottom padding quantities respectively.
1457+
If fewer than four elements are specified, bottom defaults to top,
1458+
right defaults to left, and top defaults to left.
1459+
In other words, a list of three numbers specify the left, vertical,
1460+
and right padding; a list of two numbers specify the horizontal
1461+
and the vertical padding; a single number specifies the same
1462+
padding all the way around the widget.
1463+
This option may not be mixed with any other options.
1464+
1465+
margins=padding
1466+
Specifies the elements exterior padding.
1467+
*padding* is a list of up to four integers specifying the left, top,
1468+
right and bottom padding quantities respectively.
1469+
This option may not be mixed with any other options.
1470+
1471+
width=width
1472+
Specifies the width for the element.
1473+
If this option is set then the Visual Styles API will not be queried
1474+
for the recommended size or the part.
1475+
If this option is set then *height* should also be set.
1476+
The *width* and *height* options cannot be mixed with the *padding*
1477+
or *margins* options.
1478+
1479+
height=height
1480+
Specifies the height of the element.
1481+
See the comments for *width*.
1482+
1483+
Example::
1484+
1485+
style = ttk.Style(root)
1486+
style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3, [
1487+
('pressed', '!selected', 3),
1488+
('active', '!selected', 2),
1489+
('pressed', 'selected', 6),
1490+
('active', 'selected', 5),
1491+
('selected', 4),
1492+
('', 1)])
1493+
style.layout('Explorer.Pin',
1494+
[('Explorer.Pin.pin', {'sticky': 'news'})])
1495+
pin = ttk.Checkbutton(style='Explorer.Pin')
1496+
pin.pack(expand=True, fill='both')
1497+
1498+
.. versionchanged:: 3.13
1499+
Added support of the "vsapi" element factory.
14421500

14431501
.. method:: element_names()
14441502

Doc/whatsnew/3.13.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ tkinter
301301
:meth:`!tk_busy_current`, and :meth:`!tk_busy_status`.
302302
(Contributed by Miguel, klappnase and Serhiy Storchaka in :gh:`72684`.)
303303

304+
* Add support of the "vsapi" element type in
305+
the :meth:`~tkinter.ttk.Style.element_create` method of
306+
:class:`tkinter.ttk.Style`.
307+
(Contributed by Serhiy Storchaka in :gh:`68166`.)
308+
304309
traceback
305310
---------
306311

Lib/test/test_ttk/test_style.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,55 @@ def test_element_create_image_errors(self):
258258
with self.assertRaisesRegex(TclError, 'bad option'):
259259
style.element_create('block2', 'image', image, spam=1)
260260

261+
def test_element_create_vsapi_1(self):
262+
style = self.style
263+
if 'xpnative' not in style.theme_names():
264+
self.skipTest("requires 'xpnative' theme")
265+
style.element_create('smallclose', 'vsapi', 'WINDOW', 19, [
266+
('disabled', 4),
267+
('pressed', 3),
268+
('active', 2),
269+
('', 1)])
270+
style.layout('CloseButton',
271+
[('CloseButton.smallclose', {'sticky': 'news'})])
272+
b = ttk.Button(self.root, style='CloseButton')
273+
b.pack(expand=True, fill='both')
274+
self.assertEqual(b.winfo_reqwidth(), 13)
275+
self.assertEqual(b.winfo_reqheight(), 13)
276+
277+
def test_element_create_vsapi_2(self):
278+
style = self.style
279+
if 'xpnative' not in style.theme_names():
280+
self.skipTest("requires 'xpnative' theme")
281+
style.element_create('pin', 'vsapi', 'EXPLORERBAR', 3, [
282+
('pressed', '!selected', 3),
283+
('active', '!selected', 2),
284+
('pressed', 'selected', 6),
285+
('active', 'selected', 5),
286+
('selected', 4),
287+
('', 1)])
288+
style.layout('Explorer.Pin',
289+
[('Explorer.Pin.pin', {'sticky': 'news'})])
290+
pin = ttk.Checkbutton(self.root, style='Explorer.Pin')
291+
pin.pack(expand=True, fill='both')
292+
self.assertEqual(pin.winfo_reqwidth(), 16)
293+
self.assertEqual(pin.winfo_reqheight(), 16)
294+
295+
def test_element_create_vsapi_3(self):
296+
style = self.style
297+
if 'xpnative' not in style.theme_names():
298+
self.skipTest("requires 'xpnative' theme")
299+
style.element_create('headerclose', 'vsapi', 'EXPLORERBAR', 2, [
300+
('pressed', 3),
301+
('active', 2),
302+
('', 1)])
303+
style.layout('Explorer.CloseButton',
304+
[('Explorer.CloseButton.headerclose', {'sticky': 'news'})])
305+
b = ttk.Button(self.root, style='Explorer.CloseButton')
306+
b.pack(expand=True, fill='both')
307+
self.assertEqual(b.winfo_reqwidth(), 16)
308+
self.assertEqual(b.winfo_reqheight(), 16)
309+
261310
def test_theme_create(self):
262311
style = self.style
263312
curr_theme = style.theme_use()
@@ -358,6 +407,39 @@ def test_theme_create_image(self):
358407

359408
style.theme_use(curr_theme)
360409

410+
def test_theme_create_vsapi(self):
411+
style = self.style
412+
if 'xpnative' not in style.theme_names():
413+
self.skipTest("requires 'xpnative' theme")
414+
curr_theme = style.theme_use()
415+
new_theme = 'testtheme5'
416+
style.theme_create(new_theme, settings={
417+
'pin' : {
418+
'element create': ['vsapi', 'EXPLORERBAR', 3, [
419+
('pressed', '!selected', 3),
420+
('active', '!selected', 2),
421+
('pressed', 'selected', 6),
422+
('active', 'selected', 5),
423+
('selected', 4),
424+
('', 1)]],
425+
},
426+
'Explorer.Pin' : {
427+
'layout': [('Explorer.Pin.pin', {'sticky': 'news'})],
428+
},
429+
})
430+
431+
style.theme_use(new_theme)
432+
self.assertIn('pin', style.element_names())
433+
self.assertEqual(style.layout('Explorer.Pin'),
434+
[('Explorer.Pin.pin', {'sticky': 'nswe'})])
435+
436+
pin = ttk.Checkbutton(self.root, style='Explorer.Pin')
437+
pin.pack(expand=True, fill='both')
438+
self.assertEqual(pin.winfo_reqwidth(), 16)
439+
self.assertEqual(pin.winfo_reqheight(), 16)
440+
441+
style.theme_use(curr_theme)
442+
361443

362444
if __name__ == "__main__":
363445
unittest.main()

Lib/test/test_ttk_textonly.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def test_format_elemcreate(self):
179179
# don't format returned values as a tcl script
180180
# minimum acceptable for image type
181181
self.assertEqual(ttk._format_elemcreate('image', False, 'test'),
182-
("test ", ()))
182+
("test", ()))
183183
# specifying a state spec
184184
self.assertEqual(ttk._format_elemcreate('image', False, 'test',
185185
('', 'a')), ("test {} a", ()))
@@ -203,17 +203,19 @@ def test_format_elemcreate(self):
203203
# don't format returned values as a tcl script
204204
# minimum acceptable for vsapi
205205
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b'),
206-
("a b ", ()))
206+
('a', 'b', ('', 1), ()))
207207
# now with a state spec with multiple states
208208
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b',
209-
('a', 'b', 'c')), ("a b {a b} c", ()))
209+
[('a', 'b', 'c')]), ('a', 'b', ('a b', 'c'), ()))
210210
# state spec and option
211211
self.assertEqual(ttk._format_elemcreate('vsapi', False, 'a', 'b',
212-
('a', 'b'), opt='x'), ("a b a b", ("-opt", "x")))
212+
[('a', 'b')], opt='x'), ('a', 'b', ('a', 'b'), ("-opt", "x")))
213213
# format returned values as a tcl script
214214
# state spec with a multivalue and an option
215215
self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b',
216-
('a', 'b', [1, 2]), opt='x'), ("{a b {a b} {1 2}}", "-opt x"))
216+
opt='x'), ("a b {{} 1}", "-opt x"))
217+
self.assertEqual(ttk._format_elemcreate('vsapi', True, 'a', 'b',
218+
[('a', 'b', [1, 2])], opt='x'), ("a b {{a b} {1 2}}", "-opt x"))
217219

218220
# Testing type = from
219221
# from type expects at least a type name
@@ -222,9 +224,9 @@ def test_format_elemcreate(self):
222224
self.assertEqual(ttk._format_elemcreate('from', False, 'a'),
223225
('a', ()))
224226
self.assertEqual(ttk._format_elemcreate('from', False, 'a', 'b'),
225-
('a', ('b', )))
227+
('a', ('b',)))
226228
self.assertEqual(ttk._format_elemcreate('from', True, 'a', 'b'),
227-
('{a}', 'b'))
229+
('a', 'b'))
228230

229231

230232
def test_format_layoutlist(self):
@@ -326,6 +328,22 @@ def test_script_from_settings(self):
326328
"ttk::style element create thing image {name {state1 state2} val} "
327329
"-opt {3 2m}")
328330

331+
vsapi = {'pin': {'element create':
332+
['vsapi', 'EXPLORERBAR', 3, [
333+
('pressed', '!selected', 3),
334+
('active', '!selected', 2),
335+
('pressed', 'selected', 6),
336+
('active', 'selected', 5),
337+
('selected', 4),
338+
('', 1)]]}}
339+
self.assertEqual(ttk._script_from_settings(vsapi),
340+
"ttk::style element create pin vsapi EXPLORERBAR 3 {"
341+
"{pressed !selected} 3 "
342+
"{active !selected} 2 "
343+
"{pressed selected} 6 "
344+
"{active selected} 5 "
345+
"selected 4 "
346+
"{} 1} ")
329347

330348
def test_tclobj_to_py(self):
331349
self.assertEqual(

Lib/tkinter/ttk.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -95,40 +95,47 @@ def _format_mapdict(mapdict, script=False):
9595

9696
def _format_elemcreate(etype, script=False, *args, **kw):
9797
"""Formats args and kw according to the given element factory etype."""
98-
spec = None
98+
specs = ()
9999
opts = ()
100-
if etype in ("image", "vsapi"):
101-
if etype == "image": # define an element based on an image
102-
# first arg should be the default image name
103-
iname = args[0]
104-
# next args, if any, are statespec/value pairs which is almost
105-
# a mapdict, but we just need the value
106-
imagespec = _join(_mapdict_values(args[1:]))
107-
spec = "%s %s" % (iname, imagespec)
108-
100+
if etype == "image": # define an element based on an image
101+
# first arg should be the default image name
102+
iname = args[0]
103+
# next args, if any, are statespec/value pairs which is almost
104+
# a mapdict, but we just need the value
105+
imagespec = (iname, *_mapdict_values(args[1:]))
106+
if script:
107+
specs = (imagespec,)
109108
else:
110-
# define an element whose visual appearance is drawn using the
111-
# Microsoft Visual Styles API which is responsible for the
112-
# themed styles on Windows XP and Vista.
113-
# Availability: Tk 8.6, Windows XP and Vista.
114-
class_name, part_id = args[:2]
115-
statemap = _join(_mapdict_values(args[2:]))
116-
spec = "%s %s %s" % (class_name, part_id, statemap)
109+
specs = (_join(imagespec),)
110+
opts = _format_optdict(kw, script)
117111

112+
if etype == "vsapi":
113+
# define an element whose visual appearance is drawn using the
114+
# Microsoft Visual Styles API which is responsible for the
115+
# themed styles on Windows XP and Vista.
116+
# Availability: Tk 8.6, Windows XP and Vista.
117+
if len(args) < 3:
118+
class_name, part_id = args
119+
statemap = (((), 1),)
120+
else:
121+
class_name, part_id, statemap = args
122+
specs = (class_name, part_id, tuple(_mapdict_values(statemap)))
118123
opts = _format_optdict(kw, script)
119124

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

127132
if script:
128-
spec = '{%s}' % spec
133+
specs = _join(specs)
129134
opts = ' '.join(opts)
135+
return specs, opts
136+
else:
137+
return *specs, opts
130138

131-
return spec, opts
132139

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

215222
elemargs = eopts[1:argc]
216223
elemkw = eopts[argc] if argc < len(eopts) and eopts[argc] else {}
217-
spec, opts = _format_elemcreate(etype, True, *elemargs, **elemkw)
224+
specs, eopts = _format_elemcreate(etype, True, *elemargs, **elemkw)
218225

219226
script.append("ttk::style element create %s %s %s %s" % (
220-
name, etype, spec, opts))
227+
name, etype, specs, eopts))
221228

222229
return '\n'.join(script)
223230

@@ -434,9 +441,9 @@ def layout(self, style, layoutspec=None):
434441

435442
def element_create(self, elementname, etype, *args, **kw):
436443
"""Create a new element in the current theme of given etype."""
437-
spec, opts = _format_elemcreate(etype, False, *args, **kw)
444+
*specs, opts = _format_elemcreate(etype, False, *args, **kw)
438445
self.tk.call(self._name, "element", "create", elementname, etype,
439-
spec, *opts)
446+
*specs, *opts)
440447

441448

442449
def element_names(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add support of the "vsapi" element type in
2+
:meth:`tkinter.ttk.Style.element_create`.

0 commit comments

Comments
 (0)