Retrieving the current font definition of a tkinter.ttk.Label widget

166 Views Asked by At

I wish to retrieve the font object currently assigned to an arbitrary tkinter.ttk.Label widget.

(To be more exact, I want to obtain the exact configuration attributes of the widget’s current tkinter.font.Font instance — family, size, weight, slant, underline, and overstrike — in order to programmatically define modified variants of the same base font.)

This is easy to do with a plain, unthemed tkinter.Label:

import tkinter.font

# Create root object
root = tkinter.Tk()

# Create label widget
label = tkinter.Label( root, text="Hello world!" )
label.pack()

# Get font name (key) from the widget
font_name = label['font']
print( " font_name:", repr(font_name) )

# Get Font instance and print attributes
font = tkinter.font.nametofont( font_name )
for key, value in font.config().items():
    print( f"{key:>10}: {repr(value)}" )

# Display the widget
root.mainloop() 

The above program prints out the font details as follows:

 font_name: 'TkDefaultFont'
    family: 'sans-serif'
      size: 10
    weight: 'normal'
     slant: 'roman'
 underline: 0
overstrike: 0

However, if I change it to use a tkinter.ttk.Label instead of a tkinter.Label...

import tkinter.font
import tkinter.ttk

[...]

# Create label widget
label = tkinter.ttk.Label( root, text="Hello world!" )
label.pack()

...it no longer works. This is because the expression label['font'] now returns an empty string instead of a valid Tkinter font name. The empty string gets assigned to font_name and then used in a nametofont function call, resulting to:

 font_name: ''
Traceback (most recent call last):
  File "fonttest.py", line 14, in <module>
    font = tkinter.font.nametofont( font_name )
  File "/usr/lib/python3.10/tkinter/font.py", line 23, in nametofont
    return Font(name=name, exists=True, root=root)
  File "/usr/lib/python3.10/tkinter/font.py", line 87, in __init__
    raise tkinter._tkinter.TclError(
_tkinter.TclError: named font font1 does not already exist

If I change the program yet further and specify a named ttk theme by adding the following lines after the creation of the root object but before creating the ttk.Label widget...

# Set up a ttk theme
style = tkinter.ttk.Style()
style.theme_use( 'breeze' )

...I get a somewhat different result, but still no valid font name:

 font_name: <font object: 'Helvetica 10'>
Traceback (most recent call last):
  File "fonttest.py", line 20, in <module>
    font = tkinter.font.nametofont( font_name )
  File "/usr/lib/python3.10/tkinter/font.py", line 23, in nametofont
    return Font(name=name, exists=True, root=root)
  File "/usr/lib/python3.10/tkinter/font.py", line 87, in __init__
    raise tkinter._tkinter.TclError(
_tkinter.TclError: named font Helvetica 10 does not already exist

Note how label['font'] no longer returned a string — empty or otherwise — but a “font object”.


Adding some more debug prints...

print( " font_name:", repr(font_name) )
print( "font_name.__class__:", font_name.__class__ )
print( "font_name.__str__():", repr(font_name.__str__()) )
print( "dir( font_name ):", dir( font_name ) )
print( "tkinter.font.names():", tkinter.font.names() ) 

...gives us this output:

 font_name: <font object: 'Helvetica 10'>
font_name.__class__: <class '_tkinter.Tcl_Obj'>
font_name.__str__(): 'Helvetica 10'
dir( font_name ): ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'string', 'typename']
tkinter.font.names(): ('TkCaptionFont', 'TkSmallCaptionFont', 'TkTooltipFont', 'TkFixedFont', 'TkHeadingFont', 'TkMenuFont', 'TkIconFont', 'TkTextFont', 'TkDefaultFont')

It is not really a “font name”, and not an instance of tkinter.font.Font either, but an instance of the class _tkinter.Tcl_Obj. The dir output lists a member called typename so I added this line, too...

print( "font_name.typename:", repr(font_name.typename) )

...which yields:

font_name.typename: 'font'

To summarize: sometimes the themed ttk widgets return an empty string for their font, some other times they return a “font object”. The “font object”, if present, is not a tkinter.font.Font instance but a _tkinter.Tcl_Obj instance — apparently a Python wrapper for an internal TCL font object whose properties are inaccessible to Python code?

Since this curious font object is convertible to a string (Helvetica 10), it could technically be used as a Tkinter “font name” in a font lookup call. Alas, the string that the object holds or converts into is still not a registered “font name” (that tkinter.font.names() ortkinter.font.nametofont( font_name ) would recognize) so the attributes of the underlying font remain inaccessible.

So, my question is: what is the correct way to programmatically obtain the current tkinter.font.Font instance (or the equivalent attribute details — family, size, weight, slant, underline, overstrike) of the font assigned to a tkinter.ttk.Label widget both when a theme has been explicitly set and when it has not been set?

My test environment in the above was Python 3.10.12 that comes standard with Kubuntu (Ubuntu) 22.04 LTS. The packages python3-tk (version 3.10.8-1~22.04) and python3-ttkthemes (version 3.2.2+git20220101+07e6509cc6bf-1) were manually installed from the standard Ubuntu repositories.


Edit: I initially assumed thettkthemes package would just passively register more TTK themes for the tkinter.ttk “theme engine”.

Not so: the ttkthemes package actually provides custom ThemedTk and ThemedStyle classes (derivatives of Tk and Style), which its friendly manual instructs to use in place of the standard classes, for loading the bundled themes.

This means the explanation given above about loading the Breeze theme using tkinter.ttk.Style().theme_use('breeze') is not exactly correct, even though it technically works on Ubuntu; ttkthemes.ThemedStyle().theme_use('breeze') should be used instead. Thanks to pippo1980 for making me realize this. This does not invalidate the original question but may make fully answering it somewhat more specific to the ttkthemes package than I originally thought.


Nordine Lotfi’s answer almost resolves the question.

However, even while the described method works with all of the “bult-in” tkinter.ttk themes, some of the additional ttkthemes themes still escape it.

I made another small test program to illustrate this remaining issue. It tries to resolve a tkinter.ttk.Label widget “font name” in each available theme:

import tkinter.font
import tkinter.ttk

try:
    from ttkthemes import ThemedTk, ThemedStyle
    root = ThemedTk()
    style = ThemedStyle()
    print( "Using ttkthemes.ThemedTk and ttkthemes.ThemedStyle." )
except ImportError:
    print( "Using standard tkinter.Tk and tkinter.ttk.Style." )
    root = tkinter.Tk()
    style = tkinter.ttk.Style()

label = tkinter.ttk.Label( root, text="Hello world!" )
label.pack()
label_style = label.winfo_class()

def print_status( status, theme_name, font_name ):
    print( f"[{status:^6}] {repr(theme_name):>12}: {repr(font_name)}" )

for theme_name in style.theme_names():
    style.theme_use( theme_name )
    # font_name = label['font']
    font_name = style.lookup( label_style, 'font' )
    try:
        font = tkinter.font.nametofont( font_name )        
        print_status( "OK", theme_name, font_name )
    except tkinter._tkinter.TclError as e:
        print_status( "FAIL", theme_name, font_name )

#root.mainloop()

When I only have the modules tkinter and tkinter.ttk available in the Python environment, this program produces the following output on an Ubuntu system:

Using standard tkinter.Tk and tkinter.ttk.Style.
[  OK  ]       'clam': 'TkDefaultFont'
[  OK  ]        'alt': 'TkDefaultFont'
[  OK  ]    'default': 'TkDefaultFont'
[  OK  ]    'classic': 'TkDefaultFont'

If I install the ttkthemes package (pip install ttkthemes or apt install python3-ttkthemes) and run the program again, I can no longer resolve all the “font names” (FAIL indicates an exception happened when trying to look up the resolved font name with nametofont):

Using ttkthemes.ThemedTk and ttkthemes.ThemedStyle.
[  OK  ]     'ubuntu': 'TkDefaultFont'
[ FAIL ]     'breeze': 'Helvetica 10'
[  OK  ] 'scidpurple': 'TkDefaultFont'
[  OK  ]      'black': 'TkDefaultFont'
[ FAIL ]       'smog': ''
[  OK  ]   'aquativo': 'TkDefaultFont'
[  OK  ]        'arc': 'TkDefaultFont'
[  OK  ]   'scidgrey': 'TkDefaultFont'
[  OK  ] 'clearlooks': 'TkDefaultFont'
[  OK  ]   'scidblue': 'TkDefaultFont'
[  OK  ]       'yaru': 'TkDefaultFont'
[  OK  ]       'kroc': 'TkDefaultFont'
[  OK  ]   'scidmint': 'TkDefaultFont'
[ FAIL ]      'itft1': ''
[  OK  ]   'scidsand': 'TkDefaultFont'
[  OK  ]    'classic': 'TkDefaultFont'
[  OK  ]        'alt': 'TkDefaultFont'
[  OK  ]    'equilux': 'TkDefaultFont'
[  OK  ]   'radiance': 'TkDefaultFont'
[  OK  ]   'scidpink': 'TkDefaultFont'
[  OK  ]  'winxpblue': 'TkDefaultFont'
[ FAIL ]       'blue': ''
[  OK  ]    'default': 'TkDefaultFont'
[  OK  ]  'scidgreen': 'TkDefaultFont'
[  OK  ]       'clam': 'TkDefaultFont'
[  OK  ]    'plastik': 'TkDefaultFont'
[  OK  ]     'adapta': 'TkDefaultFont'
[  OK  ]   'elegance': 'TkDefaultFont'
[  OK  ]    'keramik': 'TkDefaultFont'

(Is there some silent fallback mechanism going on in the style engine which handles these invalid TTK font names? Are these “outlier” themes incorrectly defined?)

2

There are 2 best solutions below

1
On

not sure my code is how it should be done, but googling around SO I figured out that there are ways to solve your question using tkinter.font.Font toghether with tkinter.ttk.Style class (tkinter.ttk.Style) , see code below :

import tkinter.font

import tkinter.ttk

from tkinter.font import BOLD, Font 

# Create root object
root = tkinter.Tk()


# Set up a ttk theme
stylez = tkinter.ttk.Style()
# style.theme_use( 'breeze' )  ### not used see below


default_font_root = tkinter.font.nametofont(stylez.lookup(root , 'font')).actual()

print('default_font_root : ' , default_font_root)

print("default_font_root size : " , default_font_root['size'])

# Set up a ttk theme
print(stylez.theme_names())

stylez.theme_use(stylez.theme_names()[2])

#same of 
stylez.theme_use('default')

#create Font
fontz = Font(root , family = 'Ubuntu',  size = 64 , weight = BOLD , underline = 1)


stylez.configure('my.TLabel', font = fontz)


# Create label widget
label = tkinter.ttk.Label(root, text="Hello world!" , style = 'my.TLabel')

label.pack()


print(""""style.lookup("TLabel", "font" : """ , stylez.lookup("TLabel", "font"))                    
                       
print(""""style.lookup("my.TLabel", "font" : """ , stylez.lookup("my.TLabel", "font"))
                       


label_font = tkinter.font.nametofont(stylez.lookup(label['style'] , 'font')).actual()

label_font_name = tkinter.font.nametofont(stylez.lookup(label['style'] , 'font')).name

print('label_font : ' , label_font, label_font_name)


print("fontz['size'] : ", fontz['size'])

# Display the widget
root.mainloop() 


"""
this is used to get available Font families in your system :
    
    from tkinter import Tk, font


    root = Tk()

    for i in sorted(font.families()):
        
        
        print('\n',i)

"""

3
On

You can do that by using tkinter.ttk.Style():

import tkinter as tk
import tkinter.font
import tkinter.ttk

root = tk.Tk()
label = tkinter.ttk.Label(root, text="Hello world!")
label.pack()

style = tkinter.ttk.Style()
widget_style = label.winfo_class()
print("Widget Style:", widget_style)

font_name = style.lookup(widget_style, 'font')
print("Font Name:", font_name)

font = tkinter.font.nametofont(font_name)
for key, value in font.config().items():
    print(f"{key:>10}: {repr(value)}")

root.mainloop()

Which prints on my end:

Widget Style: TLabel
Font Name: TkDefaultFont
    family: 'Segoe UI'
      size: 9
    weight: 'normal'
     slant: 'roman'
 underline: 0
overstrike: 0