Анализ Windows и очистка кешей, мусора и остатков разработки. Ядро: - каталог правил: 228 правил путей (496 шаблонов) + 43 правила артефактов проектов — Windows, браузеры, кеши сборки, IDE, приложения, пакетные менеджеры; - рекурсивный поиск node_modules, target, build, __pycache__, .venv и т.п. с проверкой маркеров проекта, чтобы не ловить ложные срабатывания; - поиск крупных файлов и дубликатов (хеш по началу/середине/концу и полная сверка), инвентаризация установленных программ через реестр; - ограничение области сканирования по дискам; - рекомендации и текстовый отчёт. Безопасность: - четыре уровня риска, автоматически отмечается только «Безопасно»; - список защищённых путей проверяется и при выборе, и перед удалением; - для каталогов вроде %TEMP% удаляется содержимое, а не сам каталог; - удаление в Корзину по умолчанию, пробный запуск, журнал очисток. Интерфейс на Dear PyGui: - JetBrains Mono Nerd Font: кириллица и иконки в одном шрифте; - тёмная и светлая темы, адаптивная раскладка; - заголовок окна ставится через SetWindowTextW — Dear PyGui отдаёт его в ANSI-функцию как UTF-8 и портит кириллицу; - запуск всегда с правами администратора: манифест UAC в exe и самоперезапуск через ShellExecuteW при работе из исходников. Сборка в один exe через PyInstaller со вшитым шрифтом и значком. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
216 lines
9.6 KiB
Python
216 lines
9.6 KiB
Python
"""Шрифты для 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 рядом с приложением, "
|
||
"иначе русский текст будет отображаться квадратами."
|
||
)
|