Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 48 additions & 22 deletions Doc/library/tkinter.font.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,53 @@

.. class:: Font(root=None, font=None, name=None, exists=False, **options)

The :class:`Font` class represents a named font. *Font* instances are given
unique names and can be specified by their family, size, and style
configuration. Named fonts are Tk's method of creating and identifying
fonts as a single object, rather than specifying a font by its attributes
with each occurrence.
The :class:`Font` class represents a font used by Tk widgets.
It either creates a new *named font* or refers to an existing font.
A named font is Tk's way of identifying a font as a single object that can
be referred to by name and reconfigured in place,
rather than respecifying its attributes at each use.

With *exists* false (the default), a new named font is created.
Its attributes are taken from the font description *font* if it is given,
overridden by any keyword *options*.
The new font is named *name*, or a generated unique name if *name* is
omitted.

With *exists* true, an existing font is referred to instead of being
created.
If *name* is given, it is the name of the font,
which is reconfigured by *font* and *options* if either is given.
If *name* is omitted, the font description *font* is wrapped as is,
without creating a named font,
so that it is used without loss of precision by :meth:`actual`,
:meth:`measure` and :meth:`metrics`.
In this case no keyword options are accepted,
and the :attr:`!name` attribute is the description itself rather than a
string.

The font description *font* is a tuple of the family name, the size and
zero or more styles,
or any other form accepted by Tk, such as the name of a named font.

The keyword *options* are:

| *family* - font family, for example, Courier, Times
| *size* - font size
| If *size* is positive it is interpreted as size in points.
| If *size* is a negative number its absolute value is treated
| as size in pixels.
| *weight* - font emphasis (NORMAL, BOLD)
| *slant* - ROMAN, ITALIC
| *underline* - font underlining (0 - none, 1 - underline)
| *overstrike* - font strikeout (0 - none, 1 - strikeout)

.. versionchanged:: 3.10
Two fonts now compare equal (``==``) only when both are :class:`Font`
instances with the same name belonging to the same Tcl interpreter.

arguments:

| *font* - font specifier tuple (family, size, options)
| *name* - unique font name
| *exists* - self points to existing named font if true

additional keyword options (ignored if *font* is specified):

| *family* - font family, for example, Courier, Times
| *size* - font size
| If *size* is positive it is interpreted as size in points.
| If *size* is a negative number its absolute value is treated
| as size in pixels.
| *weight* - font emphasis (NORMAL, BOLD)
| *slant* - ROMAN, ITALIC
| *underline* - font underlining (0 - none, 1 - underline)
| *overstrike* - font strikeout (0 - none, 1 - strikeout)
.. versionchanged:: next
A font description can now be wrapped without creating a new named font,
and keyword options now override the attributes of the specified *font*.

.. method:: actual(option=None, displayof=None)

Expand All @@ -59,6 +79,12 @@

Retrieve an attribute of the font.

.. note::

:meth:`!cget` and :meth:`configure` operate on a named font and raise

Check warning on line 84 in Doc/library/tkinter.font.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:exc reference target not found: TclError [ref.exc]
:exc:`TclError` for a wrapped font description.
Use :meth:`actual` to query the attributes of the latter.

.. method:: config(**options)
:no-typesetting:

Expand Down
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ tkinter
ttk version, and accepts mappings of button options as *buttons* entries.
(Contributed by Serhiy Storchaka in :gh:`59396`.)

* :class:`tkinter.font.Font` can now wrap a font description without creating a
new named font, by passing it as *font* with ``exists=True`` and no *name*.
This avoids a loss of precision in :meth:`~tkinter.font.Font.actual`,
:meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`.
(Contributed by Serhiy Storchaka in :gh:`143990`.)

xml
---

Expand Down
82 changes: 78 additions & 4 deletions Lib/test/test_tkinter/test_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,70 @@ def test_configure(self):
self.assertRaises(TypeError, self.font.cget)
self.assertRaises(TypeError, self.font.cget, 'size', 'weight')

def test_create(self):
sizetype = int if self.wantobjects else str

# A new named font is created from the font description...
f = font.Font(root=self.root, font=('Times', 20, 'bold'))
self.assertIn(f.name, font.names(self.root))
self.assertEqual(f.actual('weight'), 'bold')
self.assertEqual(f.cget('size'), sizetype(20))

# ... or from the keyword options.
f = font.Font(root=self.root, family='Times', size=20, weight='bold')
self.assertIn(f.name, font.names(self.root))
self.assertEqual(f.actual('weight'), 'bold')
self.assertEqual(f.cget('size'), sizetype(20))

# Explicit options override the corresponding settings of *font*.
f = font.Font(root=self.root, font=('Times', 20, 'bold'), weight='normal')
self.assertEqual(f.actual('weight'), 'normal')
self.assertEqual(f.cget('size'), sizetype(20))

# The new font can be given an explicit name.
f = font.Font(root=self.root, name='testfont', font=('Times', 20))
self.assertEqual(f.name, 'testfont')
self.assertIn('testfont', font.names(self.root))
self.assertEqual(f.cget('size'), sizetype(20))
# Reusing the name of an existing font fails.
self.assertRaises(tkinter.TclError, font.Font, root=self.root,
name='testfont', font=('Times', 10))

def test_existing(self):
sizetype = int if self.wantobjects else str

# With a name, refer to the existing named font.
named = font.Font(root=self.root, name='existingfont', family='Times', size=20)
f = font.Font(root=self.root, name='existingfont', exists=True)
self.assertEqual(f.name, 'existingfont')
self.assertEqual(f.cget('size'), sizetype(20))
# Referring to a non-existent named font fails.
self.assertRaises(tkinter.TclError, font.Font, root=self.root,
name='nosuchfont', exists=True)
# A name and options reconfigure the existing font.
font.Font(root=self.root, name='existingfont', exists=True, size=8)
self.assertEqual(f.cget('size'), sizetype(8))

# With a description and no name, the description is wrapped without
# creating a new named font (gh-143990), so that it is used without
# loss of precision by actual(), measure() and metrics().
f = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True)
self.assertEqual(f.name, ('Times', 20, 'bold'))
self.assertEqual(str(f), 'Times 20 bold')
self.assertNotIn(f.name, font.names(self.root))
self.assertEqual(f.actual('weight'), 'bold')
self.assertEqual(f.actual('size'), sizetype(20))
# It can be used as a widget option, with the same effect as the
# description itself (gh-143990).
self.assertEqual(tkinter.Label(self.root, font=f).cget('font'),
tkinter.Label(self.root, font=f.name).cget('font'))

# Options cannot be combined with a wrapped description.
self.assertRaises(TypeError, font.Font, root=self.root,
font=('Times', 20), exists=True, weight='bold')
# A name or a description is required.
self.assertRaises(TypeError, font.Font, root=self.root, exists=True)

def test_copy(self):
f = font.Font(root=self.root, family='Times', size=10, weight='bold')
copied = f.copy()
Expand All @@ -60,10 +124,7 @@ def test_copy(self):

def test_unicode_family(self):
family = 'MS \u30b4\u30b7\u30c3\u30af'
try:
f = font.Font(root=self.root, family=family, exists=True)
except tkinter.TclError:
f = font.Font(root=self.root, family=family, exists=False)
f = font.Font(root=self.root, family=family)
self.assertEqual(f.cget('family'), family)
del f
gc_collect()
Expand Down Expand Up @@ -96,6 +157,19 @@ def test_equality(self):
self.assertEqual(font1, font2)
self.assertNotEqual(font1, font1.copy())

# Wrapped descriptions (gh-143990) compare by the description.
w1 = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True)
w2 = font.Font(root=self.root, font=('Times', 20, 'bold'), exists=True)
self.assertIsNot(w1, w2)
self.assertEqual(w1, w2)
w3 = font.Font(root=self.root, font=('Times', 12), exists=True)
self.assertNotEqual(w1, w3)
# A wrapped description never equals a named font, even one whose name
# is the string form of the description.
named = font.Font(root=self.root, name=str(w1), family='Courier')
self.assertNotEqual(w1, named)
self.assertNotEqual(named, w1)

self.assertNotEqual(font1, 0)
self.assertEqual(font1, ALWAYS_EQ)

Expand Down
63 changes: 42 additions & 21 deletions Lib/tkinter/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,34 +70,55 @@ def __init__(self, root=None, font=None, name=None, exists=False,
if root is None:
root = tkinter._get_default_root('use font')
tk = getattr(root, 'tk', root)
if font:
# get actual settings corresponding to the given font
font = tk.splitlist(tk.call("font", "actual", font))
self.delete_font = False
if exists and not name:
if not font:
raise TypeError("font name or description is required if exists=True")
if options:
raise TypeError("cannot specify font options when wrapping an "
"existing font description")
# Wrap the description without creating a new named font, so that
# it is used as is by actual(), measure() and metrics(). 'name' is
# then the description rather than a string.
self.name = font
else:
font = self._set(options)
if not name:
name = "font" + str(next(self.counter))
self.name = name

if exists:
self.delete_font = False
# confirm font exists
if self.name not in tk.splitlist(tk.call("font", "names")):
raise tkinter._tkinter.TclError(
"named font %s does not already exist" % (self.name,))
# if font config info supplied, apply it
if font:
tk.call("font", "configure", self.name, *font)
else:
# create new font (raises TclError if the font exists)
tk.call("font", "create", self.name, *font)
self.delete_font = True
# start from the actual settings of the given font
font = tk.splitlist(tk.call("font", "actual", font))
if options:
# explicit options override the corresponding settings
settings = self._mkdict(font)
settings.update(options)
font = self._set(settings)
else:
font = self._set(options)
if exists:
self.name = name
# confirm font exists
if self.name not in tk.splitlist(tk.call("font", "names")):
raise tkinter._tkinter.TclError(
"named font %s does not already exist" % (self.name,))
# if font config info supplied, apply it
if font:
tk.call("font", "configure", self.name, *font)
else:
if name:
self.name = name
else:
self.name = "font" + str(next(self.counter))
# create new font (raises TclError if the font exists)
tk.call("font", "create", self.name, *font)
self.delete_font = True # set after creation
self._tk = tk
self._split = tk.splitlist
self._call = tk.call

def __str__(self):
return self.name
# A wrapped description is a list or tuple, not a string; format it as
# a Tcl word so it can be used as an option value (as ttk does).
if isinstance(self.name, str):
return self.name
return tkinter._join(self.name)

def __repr__(self):
return f"<{self.__class__.__module__}.{self.__class__.__qualname__}" \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:class:`tkinter.font.Font` can now wrap a font description without creating a
new named font, by passing it as *font* with ``exists=True`` and no *name*.
This avoids a loss of precision in :meth:`~tkinter.font.Font.actual`,
:meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`.
Keyword options now override the corresponding settings of the given *font*
instead of being ignored.
Loading