The FontChooser class wraps the "tk fontchooser" command.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tkinter.rst
tkinter.colorchooser.rst
tkinter.font.rst
+ tkinter.fontchooser.rst
dialog.rst
tkinter.messagebox.rst
tkinter.scrolledtext.rst
--- /dev/null
+:mod:`!tkinter.fontchooser` --- Font selection dialog
+=====================================================
+
+.. module:: tkinter.fontchooser
+ :synopsis: Font selection dialog
+
+.. versionadded:: next
+
+**Source code:** :source:`Lib/tkinter/fontchooser.py`
+
+--------------
+
+The :mod:`!tkinter.fontchooser` module provides the :class:`FontChooser` class
+as an interface to the native font selection dialog.
+
+The font dialog is application-global:
+there is a single font dialog per Tcl interpreter,
+and all :class:`FontChooser` instances configure the same dialog.
+
+Depending on the platform, the dialog may be modal or modeless,
+so :meth:`~FontChooser.show` may return immediately.
+The selected font is not returned:
+it is passed as a :class:`~tkinter.font.Font` object to the callback
+specified with the *command* option.
+
+The dialog also generates two virtual events on the parent window
+(see the *parent* option):
+
+``<<TkFontchooserVisibility>>``
+ Generated when the dialog is shown or hidden.
+ Query the *visible* option to tell which.
+
+``<<TkFontchooserFontChanged>>``
+ Generated when the selected font changes.
+
+.. note::
+
+ The *command* callback is the only reliable way to obtain the selected font.
+ On some platforms the *font* option is not updated to the user's choice.
+
+.. class:: FontChooser(master=None, **options)
+
+ The class implementing the font selection dialog.
+
+ *master* is the widget whose Tcl interpreter owns the dialog.
+ If omitted, it defaults to *parent* if that is given,
+ or to the default root window otherwise.
+
+ The supported configuration options are:
+
+ * *parent* --- the window to which the dialog and its virtual events are related.
+ It defaults to the main window;
+ on macOS the dialog is shown as a sheet attached to it,
+ rather than as a free-standing panel.
+ * *title* --- the title of the dialog.
+ * *font* --- the font that is currently selected in the dialog.
+ * *command* --- a callback that is called
+ with a :class:`~tkinter.font.Font` object wrapping the selected font
+ when the user selects a font.
+ * *visible* --- whether the dialog is currently displayed (read-only).
+
+ The *font* option accepts the forms supported by :class:`tkinter.font.Font`.
+
+ .. method:: configure(**options)
+ config(**options)
+
+ Query or modify the options of the font dialog.
+ With no arguments, return a dict of all option values.
+ With a string argument, return the value of that option.
+ Otherwise, set the given options.
+
+ .. method:: cget(option)
+
+ Return the value of the given option of the font dialog.
+
+ .. method:: show()
+
+ Display the font dialog.
+ Depending on the platform, this method may return immediately
+ or only once the dialog has been withdrawn.
+
+ .. method:: hide()
+
+ Hide the font dialog if it is displayed.
+
+
+.. seealso::
+
+ Module :mod:`tkinter.font`
+ Tkinter font-handling utilities
:mod:`tkinter.font`
Utilities to help work with fonts.
+:mod:`tkinter.fontchooser`
+ Dialog to let the user choose a font.
+
:mod:`tkinter.messagebox`
Access to standard Tk dialog boxes.
:meth:`~tkinter.font.Font.measure` and :meth:`~tkinter.font.Font.metrics`.
(Contributed by Serhiy Storchaka in :gh:`143990`.)
+* Added the :mod:`tkinter.fontchooser` module which provides the
+ :class:`~tkinter.fontchooser.FontChooser` class as an interface to the
+ native font selection dialog.
+ (Contributed by Serhiy Storchaka in :gh:`72880`.)
+
* Values of several Tcl object types returned by :mod:`tkinter` are now
converted to the corresponding Python type instead of being wrapped in a
:class:`!_tkinter.Tcl_Obj`: ``index``, ``window``, ``nsName`` and
--- /dev/null
+import time
+import unittest
+import tkinter
+from tkinter import font
+from tkinter.fontchooser import FontChooser
+from test import support
+from test.support import requires
+from test.test_tkinter.support import (AbstractTkTest,
+ AbstractDefaultRootTest,
+ setUpModule) # noqa: F401
+
+requires('gui')
+
+
+class FontChooserTest(AbstractTkTest, unittest.TestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.fc = FontChooser(self.root)
+ self.addCleanup(self._reset)
+
+ def _reset(self):
+ # The font dialog is global for the interpreter, so restore
+ # a clean state for the next test.
+ try:
+ self.fc.configure(parent=self.root, title='', command=None)
+ self.fc.hide()
+ except tkinter.TclError:
+ pass
+
+ def _visible(self):
+ return self.root.tk.getboolean(self.fc.cget('visible'))
+
+ def _wait_visibility(self, expected, timeout=None):
+ # Bounded wait until the dialog visibility matches expected.
+ if timeout is None:
+ timeout = support.LOOPBACK_TIMEOUT
+ deadline = time.monotonic() + timeout
+ while time.monotonic() < deadline:
+ self.root.update()
+ if self._visible() == expected:
+ return True
+ time.sleep(0.01)
+ return False
+
+ def test_configure_query(self):
+ options = self.fc.configure()
+ self.assertIsInstance(options, dict)
+ self.assertLessEqual({'parent', 'title', 'font', 'command', 'visible'},
+ options.keys())
+
+ def test_configure(self):
+ self.fc.configure(title='Pick a font')
+ if self.root._windowingsystem == 'x11':
+ self.assertEqual(self.fc.cget('title'), 'Pick a font')
+ self.fc.configure(font='Courier 10')
+ self.assertTrue(self.fc.cget('font'))
+ if self.root._windowingsystem == 'x11':
+ self.assertEqual(str(self.fc.cget('font')), 'Courier 10')
+
+ def test_parent(self):
+ top = tkinter.Toplevel(self.root)
+ self.addCleanup(top.destroy)
+ # The constructor does not force -parent to the master: it stays
+ # the main window even when the master is another widget.
+ FontChooser(top)
+ self.assertEqual(self.fc.cget('parent'), str(self.root))
+ # -parent can be set explicitly.
+ self.fc.configure(parent=top)
+ self.assertEqual(self.fc.cget('parent'), str(top))
+
+ def test_configure_font_instance(self):
+ # A Font instance can be passed as the font, both to the
+ # constructor and to configure(). The dialog may store it as
+ # the font name or as a resolved description depending on the
+ # platform, so compare the actual attributes.
+ def actual(spec):
+ return font.Font(self.root, spec, exists=True).actual()
+ f = font.Font(self.root, family='Courier', size=14, weight='bold')
+ fc = FontChooser(self.root, font=f)
+ self.assertEqual(actual(fc.cget('font')), f.actual())
+ f2 = font.Font(self.root, family='Times', size=11)
+ fc.configure(font=f2)
+ self.assertEqual(actual(fc.cget('font')), f2.actual())
+
+ def test_configure_visible_readonly(self):
+ with self.assertRaises(tkinter.TclError):
+ self.fc.configure(visible=True)
+
+ def test_cget_visible(self):
+ self.assertFalse(self._visible())
+
+ def test_command(self):
+ result = []
+ self.fc.configure(command=result.append)
+ name = self.fc._command_name
+ self.assertTrue(name)
+ # The Tcl command is named after the wrapped callback.
+ self.assertTrue(name.endswith('append'), name)
+ # The callback receives a Font wrapping the selected font.
+ self.root.tk.call(name, 'Courier 10')
+ self.assertEqual(len(result), 1)
+ selected = result[0]
+ self.assertIsInstance(selected, font.Font)
+ # The description is wrapped without creating a named font.
+ self.assertEqual(str(selected), 'Courier 10')
+ self.assertFalse(selected.delete_font)
+ self.assertEqual(int(selected.actual('size')), 10)
+ # Replacing the callback deletes the old Tcl command.
+ self.fc.configure(command=lambda font: None)
+ self.assertNotEqual(self.fc._command_name, name)
+ self.assertFalse(self.root.tk.call('info', 'commands', name))
+ # Removing the callback deletes the Tcl command.
+ name = self.fc._command_name
+ self.fc.configure(command=None)
+ self.assertIsNone(self.fc._command_name)
+ self.assertFalse(self.root.tk.call('info', 'commands', name))
+ self.assertEqual(self.fc.cget('command'), '')
+
+ def test_show_hide(self):
+ if self.root._windowingsystem != 'x11':
+ self.skipTest('cannot safely drive the native font dialog')
+ events = []
+ self.root.bind('<<TkFontchooserVisibility>>', events.append)
+ self.fc.show()
+ if not self._wait_visibility(True):
+ self.skipTest('the font dialog was not mapped')
+ self.fc.hide()
+ self.assertTrue(self._wait_visibility(False))
+ self.assertTrue(events)
+
+
+class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
+
+ def test_fontchooser(self):
+ root = tkinter.Tk()
+ fc = FontChooser()
+ self.assertIs(fc.master, root)
+ root.destroy()
+ tkinter.NoDefaultRoot()
+ self.assertRaises(RuntimeError, FontChooser)
+
+
+if __name__ == "__main__":
+ unittest.main()
--- /dev/null
+"""Interface to the native font selection dialog.
+
+The FontChooser class gives access to the "tk fontchooser" command.
+The dialog is application-global: all instances configure the single
+dialog of the Tcl interpreter.
+
+The dialog may be modeless, so show() may return immediately.
+"""
+
+import tkinter
+from tkinter.font import Font
+
+__all__ = ["FontChooser"]
+
+
+class FontChooser:
+ """The font selection dialog.
+
+ Supported configuration options are:
+
+ parent: the window to which the dialog and its virtual
+ events are related (the main window by default)
+ title: the title of the dialog
+ font: the font that is currently selected in the dialog
+ command: a callback that is called with a tkinter.font.Font
+ object wrapping the selected font when the user
+ selects a font
+ visible: whether the dialog is currently displayed
+ (read-only)
+
+ The "font" option accepts the forms supported by
+ tkinter.font.Font(font=...).
+
+ The "command" callback is the only reliable way to obtain the
+ selected font. On some platforms the "font" option is not updated
+ to the user's choice.
+ """
+
+ def __init__(self, master=None, **options):
+ if master is None:
+ master = options.get('parent')
+ if master is None:
+ master = tkinter._get_default_root('create a font chooser')
+ self.master = master
+ self._command_name = None
+ if options:
+ self.configure(options)
+
+ def configure(self, cnf=None, **kw):
+ """Query or modify the options of the font dialog.
+
+ With no arguments, return a dict of all option values. With a
+ string argument, return the value of that option. Otherwise,
+ set the given options. The "visible" option is read-only.
+ """
+ if kw:
+ cnf = tkinter._cnfmerge((cnf, kw))
+ elif cnf:
+ cnf = tkinter._cnfmerge(cnf)
+ master = self.master
+ if cnf is None:
+ items = master.tk.splitlist(master.tk.call(
+ 'tk', 'fontchooser', 'configure'))
+ return {items[i][1:]: items[i+1] for i in range(0, len(items), 2)}
+ if isinstance(cnf, str):
+ return master.tk.call('tk', 'fontchooser', 'configure', '-' + cnf)
+ cnf = dict(cnf)
+ new_name = None
+ if 'command' in cnf:
+ command = cnf['command']
+ if command is None:
+ cnf['command'] = ''
+ elif callable(command):
+ # Pass the selected font to the callback as a Font object.
+ def callback(description):
+ command(Font(self.master, font=description, exists=True))
+ # Name the Tcl command after the callback.
+ callback.__func__ = command
+ new_name = cnf['command'] = master._register(callback)
+ try:
+ master.tk.call('tk', 'fontchooser', 'configure',
+ *master._options(cnf))
+ except tkinter.TclError:
+ if new_name is not None:
+ master.deletecommand(new_name)
+ raise
+ if 'command' in cnf:
+ if self._command_name is not None:
+ master.deletecommand(self._command_name)
+ self._command_name = new_name
+ config = configure
+
+ def cget(self, option):
+ """Return the value of the given option of the font dialog."""
+ return self.master.tk.call('tk', 'fontchooser', 'configure',
+ '-' + option)
+
+ def show(self):
+ """Display the font dialog.
+
+ Depending on the platform, may return immediately or only
+ once the dialog has been withdrawn.
+ """
+ self.master.tk.call('tk', 'fontchooser', 'show')
+
+ def hide(self):
+ """Hide the font dialog if it is displayed."""
+ self.master.tk.call('tk', 'fontchooser', 'hide')
--- /dev/null
+Added the :mod:`tkinter.fontchooser` module which provides the
+:class:`~tkinter.fontchooser.FontChooser` class as an interface to the
+native font selection dialog.