"""Шрифты для Dear PyGui с поддержкой кириллицы. Почему без этого модуля интерфейс будет в квадратах --------------------------------------------------- Dear PyGui использует Dear ImGui, а тот по умолчанию берёт встроенный шрифт ProggyClean, в котором кириллицы нет вообще. Любой русский текст превращается в пустые прямоугольники. Лечится это регистрацией собственного TTF. Порядок вызовов важен: 1. ``dpg.create_context()``; 2. ``dpg.font_registry()`` → ``dpg.font(путь, размер)`` — регистрируем TTF, в котором кириллица есть; 3. ``dpg.bind_font(...)`` — внутри блока реестра, пока шрифт заведомо жив; 4. и только потом ``dpg.setup_dearpygui()`` — на этом шаге собирается атлас глифов, после него менять состав символов уже поздно. Про диапазоны символов ---------------------- В Dear PyGui **1.x** нужно было явно просить нужные блоки Unicode:: dpg.add_font_range_hint(dpg.mvFontRangeHint_Default) # латиница, цифры dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic) # кириллица Начиная с **2.0** эти вызовы объявлены устаревшими и ничего не делают: диапазоны определяются автоматически по содержимому шрифта («This call is a no-op because character ranges are now automatic»). Мы всё равно их вызываем — чтобы модуль работал и на 1.x, — но глушим предупреждения, чтобы не засорять вывод на 2.x. Проверено на Dear PyGui 2.3.1 + Segoe UI: кириллица, «кавычки», ·, ×, –, … отображаются корректно. А вот ⚠ и ✓ есть не во всех системных шрифтах (в Consolas их нет), поэтому в интерфейсе они не используются. """ from __future__ import annotations import os import warnings import dearpygui.dearpygui as dpg # Шрифты в порядке предпочтения. Первым идёт JetBrains Mono Nerd Font: в нём # есть и полная кириллица, и иконочные глифы Nerd Font, так что интерфейсу # не нужен отдельный иконочный шрифт. CANDIDATES = ( "JetBrainsMonoNerdFont-Regular.ttf", # иконки натуральной ширины "JetBrainsMonoNLNerdFont-Regular.ttf", # без лигатур "JetBrainsMonoNerdFontPropo-Regular.ttf", "JetBrainsMonoNerdFontMono-Regular.ttf", "JetBrainsMono-Regular.ttf", # обычный JetBrains Mono — без иконок "segoeui.ttf", # системные запасные варианты "calibri.ttf", "tahoma.ttf", "arial.ttf", ) BOLD_CANDIDATES = ( "JetBrainsMonoNerdFont-Bold.ttf", "JetBrainsMonoNLNerdFont-Bold.ttf", "JetBrainsMonoNerdFontPropo-Bold.ttf", "JetBrainsMonoNerdFontMono-Bold.ttf", "JetBrainsMono-Bold.ttf", "segoeuib.ttf", "calibrib.ttf", "arialbd.ttf", ) MONO_CANDIDATES = ( "JetBrainsMonoNerdFontMono-Regular.ttf", # иконки в одну ячейку "JetBrainsMonoNerdFont-Regular.ttf", "JetBrainsMono-Regular.ttf", "consola.ttf", "cour.ttf", ) def _font_dirs() -> list[str]: """Где искать TTF: вложенные в сборку, рядом с программой, системные.""" import sys here = os.path.dirname(os.path.abspath(__file__)) project = os.path.dirname(os.path.dirname(here)) windir = os.environ.get("WINDIR", r"C:\Windows") local = os.environ.get("LOCALAPPDATA", "") dirs = [] # шрифты, вшитые в exe: PyInstaller распаковывает их во временный _MEIPASS meipass = getattr(sys, "_MEIPASS", "") if meipass: dirs.append(os.path.join(meipass, "fonts")) # рядом с самим exe — можно подложить свой шрифт без пересборки dirs.append(os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "fonts")) dirs += [ os.path.join(project, "fonts"), os.path.join(os.getcwd(), "fonts"), os.path.join(windir, "Fonts"), ] if local: dirs.append(os.path.join(local, "Microsoft", "Windows", "Fonts")) seen, out = set(), [] for d in dirs: key = os.path.normcase(d) if key not in seen and os.path.isdir(d): seen.add(key) out.append(d) return out def find_font(candidates: tuple[str, ...] = CANDIDATES) -> str | None: """Первый существующий TTF из списка кандидатов.""" for directory in _font_dirs(): for name in candidates: path = os.path.join(directory, name) if os.path.isfile(path): return path # запасной вариант — любой .ttf из папки fonts, положенной рядом с программой for directory in _font_dirs(): if not directory.lower().endswith("fonts") or "windows" in directory.lower(): continue try: for entry in sorted(os.listdir(directory)): if entry.lower().endswith((".ttf", ".otf")): return os.path.join(directory, entry) except OSError: continue return None def _add_ranges() -> None: """Диапазоны символов. На Dear PyGui 2.x — no-op, нужны только для 1.x.""" with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) try: dpg.add_font_range_hint(dpg.mvFontRangeHint_Default) # латиница, цифры dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic) # кириллица except Exception: pass try: # знаки вне базовых блоков, которые встречаются в подписях dpg.add_font_chars([0x00AB, 0x00BB, 0x00B7, 0x00D7, 0x00A0, 0x2013, 0x2014, 0x2026, 0x2192, 0x2007]) except Exception: pass try: # зона частного использования — иконки Nerd Font. # Берём только BMP: Material Design Icons лежат выше U+FFFF, # а Dear ImGui собран с 16-битным ImWchar и их не отрисует. dpg.add_font_range(0xE000, 0xF8FF) except Exception: pass class Fonts: """Набор шрифтов приложения. Строить строго до ``setup_dearpygui()``.""" def __init__(self, base_size: int = 17) -> None: self.base_size = base_size self.path = find_font() self.bold_path = find_font(BOLD_CANDIDATES) or self.path self.mono_path = find_font(MONO_CANDIDATES) or self.path self.default = None self.small = None self.h1 = None self.h2 = None self.big = None self.mono = None @property def ok(self) -> bool: return self.path is not None @property def has_icons(self) -> bool: """Найден ли Nerd Font — от этого зависит, показывать ли иконки.""" return bool(self.path) and "nerdfont" in os.path.basename(self.path).lower() def build(self) -> None: """Зарегистрировать шрифты и применить основной ко всему интерфейсу.""" if not self.path: return # кириллицы не будет, но приложение запустится s = self.base_size with dpg.font_registry(): with dpg.font(self.path, s) as self.default: _add_ranges() with dpg.font(self.path, max(12, s - 3)) as self.small: _add_ranges() with dpg.font(self.bold_path, s + 10) as self.h1: _add_ranges() with dpg.font(self.bold_path, s + 2) as self.h2: _add_ranges() with dpg.font(self.bold_path, s + 16) as self.big: _add_ranges() with dpg.font(self.mono_path, max(12, s - 3)) as self.mono: _add_ranges() # bind_font — внутри блока реестра: шрифт уже создан и ещё «в области видимости» dpg.bind_font(self.default) @staticmethod def apply(item, font) -> None: """Назначить отдельному элементу другой шрифт (заголовок, крупная цифра).""" if font is None or item is None: return try: dpg.bind_item_font(item, font) except Exception: pass MISSING_FONT_MESSAGE = ( "Не найден TTF-шрифт с кириллицей. Положите любой такой шрифт " "(например, Roboto-Regular.ttf) в папку fonts рядом с приложением, " "иначе русский текст будет отображаться квадратами." )