.. 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)
Retrieve an attribute of the font.
+ .. note::
+
+ :meth:`!cget` and :meth:`configure` operate on a named font and raise
+ :exc:`~tkinter.TclError` for a wrapped font description.
+ Use :meth:`actual` to query the attributes of the latter.
+
.. method:: config(**options)
:no-typesetting:
them.
(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
---
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()
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()
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)
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__}" \
--- /dev/null
+: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.