Character Map Example

The example displays an array of characters which the user can click on to enter text in a line edit. The contents of the line edit can then be copied into the clipboard, and pasted into other applications. The purpose behind this sort of tool is to allow users to enter characters that may be unavailable or difficult to locate on their keyboards.

Download this example

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

import sys

from PySide6.QtWidgets import QApplication

from mainwindow import MainWindow

"""PySide6 port of the widgets/widgets/ charactermap example from Qt6"""


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from textwrap import dedent

from PySide6.QtCore import QSize, Qt, Slot, Signal
from PySide6.QtGui import (QBrush, QFont, QFontDatabase, QFontMetrics,
                           QPainter, QPen)
from PySide6.QtWidgets import QToolTip, QWidget

COLUMNS = 16


class CharacterWidget(QWidget):

    character_selected = Signal(str)

    def __init__(self, parent=None):
        super().__init__(parent)

        self._display_font = QFont()
        self._last_key = -1
        self._square_size = int(0)

        self.calculate_square_size()
        self.setMouseTracking(True)

    @Slot(QFont)
    def update_font(self, font):
        self._display_font.setFamily(font.family())
        self.calculate_square_size()
        self.adjustSize()
        self.update()

    @Slot(str)
    def update_size(self, fontSize):
        self._display_font.setPointSize(int(fontSize))
        self.calculate_square_size()
        self.adjustSize()
        self.update()

    @Slot(str)
    def update_style(self, fontStyle):
        old_strategy = self._display_font.styleStrategy()
        self._display_font = QFontDatabase.font(self._display_font.family(),
                                                fontStyle,
                                                self._display_font.pointSize())
        self._display_font.setStyleStrategy(old_strategy)
        self.calculate_square_size()
        self.adjustSize()
        self.update()

    @Slot(bool)
    def update_font_merging(self, enable):
        if enable:
            self._display_font.setStyleStrategy(QFont.PreferDefault)
        else:
            self._display_font.setStyleStrategy(QFont.NoFontMerging)
        self.adjustSize()
        self.update()

    def calculate_square_size(self):
        h = QFontMetrics(self._display_font, self).height()
        self._square_size = max(16, 4 + h)

    def sizeHint(self):
        return QSize(COLUMNS * self._square_size,
                     (65536 / COLUMNS) * self._square_size)

    def _unicode_from_pos(self, point):
        row = int(point.y() / self._square_size)
        return row * COLUMNS + int(point.x() / self._square_size)

    def mouseMoveEvent(self, event):
        widget_position = self.mapFromGlobal(event.globalPosition().toPoint())
        key = self._unicode_from_pos(widget_position)
        c = chr(key)
        family = self._display_font.family()
        text = dedent(f'''
                       <p>Character: <span style="font-size: 24pt; font-family: {family}">
                       {c}</span><p>Value: 0x{key:x}
                       ''')
        QToolTip.showText(event.globalPosition().toPoint(), text, self)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._last_key = self._unicode_from_pos(event.position().toPoint())
            if self._last_key != -1:
                c = chr(self._last_key)
                self.character_selected.emit(f"{c}")
            self.update()
        else:
            super().mousePressEvent(event)

    def paintEvent(self, event):
        with QPainter(self) as painter:
            self.render(event, painter)

    def render(self, event, painter):
        painter = QPainter(self)
        painter.fillRect(event.rect(), QBrush(Qt.white))
        painter.setFont(self._display_font)
        redraw_rect = event.rect()
        begin_row = int(redraw_rect.top() / self._square_size)
        end_row = int(redraw_rect.bottom() / self._square_size)
        begin_column = int(redraw_rect.left() / self._square_size)
        end_column = int(redraw_rect.right() / self._square_size)
        painter.setPen(QPen(Qt.gray))
        for row in range(begin_row, end_row + 1):
            for column in range(begin_column, end_column + 1):
                x = int(column * self._square_size)
                y = int(row * self._square_size)
                painter.drawRect(x, y, self._square_size, self._square_size)

        font_metrics = QFontMetrics(self._display_font)
        painter.setPen(QPen(Qt.black))
        for row in range(begin_row, end_row + 1):
            for column in range(begin_column, end_column + 1):
                key = int(row * COLUMNS + column)
                painter.setClipRect(column * self._square_size,
                                    row * self._square_size,
                                    self._square_size, self._square_size)

                if key == self._last_key:
                    painter.fillRect(column * self._square_size + 1,
                                     row * self._square_size + 1,
                                     self._square_size, self._square_size, QBrush(Qt.red))

                text = chr(key)
                painter.drawText(column * self._square_size + (self._square_size / 2)
                                 - font_metrics.horizontalAdvance(text) / 2,
                                 row * self._square_size + 4 + font_metrics.ascent(),
                                 text)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import Qt, qVersion, qFuzzyCompare
from PySide6.QtGui import QGuiApplication, QFontDatabase
from PySide6.QtWidgets import (QDialog, QDialogButtonBox,
                               QPlainTextEdit, QVBoxLayout)


def _format_font(font):
    family = font.family()
    size = font.pointSizeF()
    return f"{family}, {size}pt"


class FontInfoDialog(QDialog):

    def __init__(self, parent):
        super().__init__(parent)
        self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
        main_layout = QVBoxLayout(self)
        text_edit = QPlainTextEdit(self.text(), self)
        text_edit.setReadOnly(True)
        text_edit.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))
        main_layout.addWidget(text_edit)
        button_box = QDialogButtonBox(QDialogButtonBox.Close, self)
        button_box.rejected.connect(self.reject)
        main_layout.addWidget(button_box)

    def text(self):
        default_font = QFontDatabase.systemFont(QFontDatabase.GeneralFont)
        fixed_font = QFontDatabase.systemFont(QFontDatabase.FixedFont)
        title_font = QFontDatabase.systemFont(QFontDatabase.TitleFont)
        smallest_readable_font = QFontDatabase.systemFont(QFontDatabase.SmallestReadableFont)

        v = qVersion()
        platform = QGuiApplication.platformName()
        dpi = self.logicalDpiX()
        dpr = self.devicePixelRatio()
        text = f"Qt {v} on {platform}, {dpi}DPI"
        if not qFuzzyCompare(dpr, float(1)):
            text += f", device pixel ratio: {dpr}"
        text += ("\n\nDefault font : " + _format_font(default_font)
                 + "\nFixed font : " + _format_font(fixed_font)
                 + "\nTitle font : " + _format_font(title_font)
                 + "\nSmallest font: " + _format_font(smallest_readable_font))
        return text
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import Qt, QSignalBlocker, Slot
from PySide6.QtGui import QGuiApplication, QClipboard, QFont, QFontDatabase
from PySide6.QtWidgets import (QCheckBox, QComboBox, QFontComboBox,
                               QHBoxLayout, QLabel, QLineEdit, QMainWindow,
                               QPushButton, QScrollArea,
                               QVBoxLayout, QWidget)

from characterwidget import CharacterWidget
from fontinfodialog import FontInfoDialog


class MainWindow(QMainWindow):

    def __init__(self, parent=None):
        super().__init__(parent)

        self._character_widget = CharacterWidget()
        self._filter_combo = QComboBox()
        self._style_combo = QComboBox()
        self._size_combo = QComboBox()
        self._font_combo = QFontComboBox()
        self._line_edit = QLineEdit()
        self._scroll_area = QScrollArea()
        self._font_merging = QCheckBox()

        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction("Quit", self.close)
        help_menu = self.menuBar().addMenu("Help")
        help_menu.addAction("Show Font Info", self.show_info)
        help_menu.addAction("About &Qt", qApp.aboutQt)  # noqa: F821

        central_widget = QWidget()

        self._filter_label = QLabel("Filter:")
        self._filter_combo = QComboBox()
        self._filter_combo.addItem("All", int(QFontComboBox.AllFonts.value))
        self._filter_combo.addItem("Scalable", int(QFontComboBox.ScalableFonts.value))
        self._filter_combo.addItem("Monospaced", int(QFontComboBox.MonospacedFonts.value))
        self._filter_combo.addItem("Proportional", int(QFontComboBox.ProportionalFonts.value))
        self._filter_combo.setCurrentIndex(0)
        self._filter_combo.currentIndexChanged.connect(self.filter_changed)

        self._font_label = QLabel("Font:")
        self._font_combo = QFontComboBox()
        self._size_label = QLabel("Size:")
        self._size_combo = QComboBox()
        self._style_label = QLabel("Style:")
        self._style_combo = QComboBox()
        self._font_merging_label = QLabel("Automatic Font Merging:")
        self._font_merging = QCheckBox()
        self._font_merging.setChecked(True)

        self._scroll_area = QScrollArea()
        self._character_widget = CharacterWidget()
        self._scroll_area.setWidget(self._character_widget)
        self.find_styles(self._font_combo.currentFont())
        self.find_sizes(self._font_combo.currentFont())

        self._line_edit = QLineEdit()
        self._line_edit.setClearButtonEnabled(True)
        self._clipboard_button = QPushButton("To clipboard")
        self._font_combo.currentFontChanged.connect(self.find_styles)
        self._font_combo.currentFontChanged.connect(self.find_sizes)
        self._font_combo.currentFontChanged.connect(self._character_widget.update_font)
        self._size_combo.currentTextChanged.connect(self._character_widget.update_size)
        self._style_combo.currentTextChanged.connect(self._character_widget.update_style)
        self._character_widget.character_selected.connect(self.insert_character)

        self._clipboard_button.clicked.connect(self.update_clipboard)
        self._font_merging.toggled.connect(self._character_widget.update_font_merging)

        controls_layout = QHBoxLayout()
        controls_layout.addWidget(self._filter_label)
        controls_layout.addWidget(self._filter_combo, 1)
        controls_layout.addWidget(self._font_label)
        controls_layout.addWidget(self._font_combo, 1)
        controls_layout.addWidget(self._size_label)
        controls_layout.addWidget(self._size_combo, 1)
        controls_layout.addWidget(self._style_label)
        controls_layout.addWidget(self._style_combo, 1)
        controls_layout.addWidget(self._font_merging_label)
        controls_layout.addWidget(self._font_merging, 1)
        controls_layout.addStretch(1)

        line_layout = QHBoxLayout()
        line_layout.addWidget(self._line_edit, 1)
        line_layout.addSpacing(12)
        line_layout.addWidget(self._clipboard_button)

        central_layout = QVBoxLayout(central_widget)
        central_layout.addLayout(controls_layout)
        central_layout.addWidget(self._scroll_area, 1)
        central_layout.addSpacing(4)
        central_layout.addLayout(line_layout)

        self.setCentralWidget(central_widget)
        self.setWindowTitle("Character Map")

    @Slot(QFont)
    def find_styles(self, font):
        current_item = self._style_combo.currentText()
        self._style_combo.clear()
        styles = QFontDatabase.styles(font.family())
        for style in styles:
            self._style_combo.addItem(style)

        style_index = self._style_combo.findText(current_item)

        if style_index == -1:
            self._style_combo.setCurrentIndex(0)
        else:
            self._style_combo.setCurrentIndex(style_index)

    @Slot(int)
    def filter_changed(self, f):
        filter = QFontComboBox.FontFilter(self._filter_combo.itemData(f))
        self._font_combo.setFontFilters(filter)
        count = self._font_combo.count()
        self.statusBar().showMessage(f"{count} font(s) found")

    @Slot(QFont)
    def find_sizes(self, font):
        current_size = self._size_combo.currentText()
        with QSignalBlocker(self._size_combo):
            # sizeCombo signals are now blocked until end of scope
            self._size_combo.clear()

            style = QFontDatabase.styleString(font)
            if QFontDatabase.isSmoothlyScalable(font.family(), style):
                sizes = QFontDatabase.standardSizes()
                for size in sizes:
                    self._size_combo.addItem(f"{size}")
                    self._size_combo.setEditable(True)
            else:
                sizes = QFontDatabase.smoothSizes(font.family(), style)
                for size in sizes:
                    self._size_combo.addItem(f"{size}")
                    self._size_combo.setEditable(False)

        size_index = self._size_combo.findText(current_size)

        if size_index == -1:
            self._size_combo.setCurrentIndex(max(0, self._size_combo.count() / 3))
        else:
            self._size_combo.setCurrentIndex(size_index)

    @Slot(str)
    def insert_character(self, character):
        self._line_edit.insert(character)

    @Slot()
    def update_clipboard(self):
        clipboard = QGuiApplication.clipboard()
        clipboard.setText(self._line_edit.text(), QClipboard.Clipboard)
        clipboard.setText(self._line_edit.text(), QClipboard.Selection)

    @Slot()
    def show_info(self):
        screen_geometry = self.screen().geometry()
        dialog = FontInfoDialog(self)
        dialog.setWindowTitle("Fonts")
        dialog.setAttribute(Qt.WA_DeleteOnClose)
        dialog.resize(screen_geometry.width() / 4, screen_geometry.height() / 4)
        dialog.show()