# theme.py """ Настройки темы и цветовой схемы приложения Централизованное управление стилями для единообразного внешнего вида """ from PyQt5.QtCore import Qt from PyQt5.QtGui import QFontDatabase, QFont # ========== ОСНОВНЫЕ ЦВЕТА ========== class Colors: """Цветовая палитра приложения""" # Основные цвета (Primary) PRIMARY = "#999999" # Синий - основной акцент PRIMARY_DARK = "#777777" # Тёмно-синий (наведение) PRIMARY_LIGHT = "#5dade2" # Светло-синий PRIMARY_BG = "#ebf5fb" # Фоновый для primary элементов # Успех/Позитив (Success) SUCCESS = "#27ae60" # Зелёный SUCCESS_DARK = "#219a52" # Тёмно-зелёный SUCCESS_LIGHT = "#2ecc71" # Светло-зелёный SUCCESS_BG = "#d5f5e3" # Фоновый для успеха # Опасность/Ошибка (Danger) DANGER = "#e74c3c" # Красный DANGER_DARK = "#c0392b" # Тёмно-красный DANGER_LIGHT = "#ec7063" # Светло-красный DANGER_BG = "#fadbd8" # Фоновый для ошибок # Предупреждение (Warning) WARNING = "#f39c12" # Оранжевый WARNING_DARK = "#e67e22" # Тёмно-оранжевый WARNING_LIGHT = "#f5b041" # Светло-оранжевый WARNING_BG = "#fef9e7" # Фоновый для предупреждений # Информация (Info) INFO = "#34495e" # Тёмно-синий/серый INFO_LIGHT = "#5d6d7e" # Светлый вариант # Нейтральные цвета (Neutral) WHITE = "#ffffff" BLACK = "#000000" GRAY_100 = "#f8f9fa" GRAY_200 = "#ecf0f1" GRAY_300 = "#dee2e6" GRAY_400 = "#ced4da" GRAY_500 = "#adb5bd" GRAY_600 = "#6c757d" GRAY_700 = "#495057" GRAY_800 = "#343a40" GRAY_900 = "#212529" # Цвета для таблиц TABLE_ALTERNATE_ROW = "#f9f9f9" TABLE_CENTER_POINT = "#ffffc8" # Жёлтый для центральных точек TABLE_HIGHLIGHT = "#d4efdf" # Светло-зелёный для подсветки # Цвета для текста TEXT_PRIMARY = GRAY_900 TEXT_SECONDARY = GRAY_600 TEXT_MUTED = GRAY_500 TEXT_ON_PRIMARY = WHITE TEXT_ON_DARK = WHITE # Цвета для границ BORDER_LIGHT = GRAY_300 BORDER_DEFAULT = GRAY_400 BORDER_DARK = GRAY_600 # Прозрачность TRANSPARENT = "transparent" OVERLAY = "rgba(0, 0, 0, 0.5)" OVERLAY_LIGHT = "rgba(0, 0, 0, 0.1)" # ========== НАСТРОЙКИ ШРИФТОВ ========== class Fonts: """Настройки шрифтов""" FAMILY_PRIMARY = "Segoe UI, Arial, sans-serif" FAMILY_MONO = "Consolas, Monaco, monospace" # Размеры SIZE_TINY = 10 SIZE_SMALL = 11 SIZE_NORMAL = 12 SIZE_MEDIUM = 14 SIZE_LARGE = 16 SIZE_XLARGE = 18 SIZE_XXLARGE = 24 # Вес (жирность) WEIGHT_NORMAL = 400 WEIGHT_MEDIUM = 500 WEIGHT_SEMIBOLD = 600 WEIGHT_BOLD = 700 @classmethod def get_title_font(cls): """Возвращает шрифт для заголовков""" font = QFont(cls.FAMILY_PRIMARY.split(',')[0]) font.setPointSize(cls.SIZE_XLARGE) font.setBold(True) return font @classmethod def get_heading_font(cls, size=SIZE_LARGE): """Возвращает шрифт для подзаголовков""" font = QFont(cls.FAMILY_PRIMARY.split(',')[0]) font.setPointSize(size) font.setBold(True) return font @classmethod def get_normal_font(cls): """Возвращает обычный шрифт""" font = QFont(cls.FAMILY_PRIMARY.split(',')[0]) font.setPointSize(cls.SIZE_NORMAL) return font @classmethod def get_emoji_font(cls): """Возвращает шрифт с поддержкой эмодзи""" # Получаем список доступных шрифтов available_fonts = QFontDatabase().families() # Приоритетный список шрифтов с поддержкой эмодзи emoji_fonts = [ "Segoe UI Emoji", # Windows 10/11 "Apple Color Emoji", # macOS "Noto Color Emoji", # Linux "EmojiOne Color", # Альтернативный "Twemoji Mozilla", # Firefox "Symbola", # Основные эмодзи "Segoe UI Symbol", # Windows (старые версии) ] # Ищем первый доступный шрифт for font_name in emoji_fonts: if font_name in available_fonts: font = QFont(font_name) font.setPointSize(cls.SIZE_NORMAL) return font # Если шрифты с эмодзи не найдены, возвращаем обычный шрифт # и добавляем fallback для эмодзи через семейство шрифтов fallback_font = cls.get_normal_font() fallback_font.setFamily(f"{cls.FAMILY_PRIMARY}, Segoe UI Emoji, Apple Color Emoji") return fallback_font def setup_emoji_support(app: QApplication): """ Настраивает поддержку эмодзи во всём приложении Параметры: app: экземпляр QApplication """ # Устанавливаем шрифт по умолчанию с поддержкой эмодзи font = Fonts.get_emoji_font() app.setFont(font) # Дополнительно настраиваем атрибуты шрифта для лучшего отображения font.setStyleStrategy(QFont.PreferAntialias) # Для Windows: включаем поддержку DirectWrite для лучшего отображения try: app.setAttribute(Qt.AA_UseHighDpiPixmaps, True) app.setAttribute(Qt.AA_EnableHighDpiScaling, True) except: pass # Не все версии PyQt5 поддерживают эти атрибуты # ========== ОТСТУПЫ И РАЗМЕРЫ ========== class Spacing: """Отступы и размеры элементов""" XS = 4 # Очень маленький SM = 8 # Маленький MD = 12 # Средний LG = 16 # Большой XL = 24 # Очень большой XXL = 32 # Максимальный # Размеры элементов BUTTON_HEIGHT = 32 INPUT_HEIGHT = 30 TABLE_ROW_HEIGHT = 25 # Скругления BORDER_RADIUS_SM = 4 BORDER_RADIUS_MD = 6 BORDER_RADIUS_LG = 8 BORDER_RADIUS_XL = 12 # ========== СТИЛИ КНОПОК ========== class ButtonStyles: """Стили для разных типов кнопок""" @staticmethod def _base_style(): return f""" border: none; border-radius: {Spacing.BORDER_RADIUS_MD}px; padding: {Spacing.SM}px {Spacing.LG}px; font-weight: {Fonts.WEIGHT_SEMIBOLD}; font-size: {Fonts.SIZE_NORMAL}px; """ @staticmethod def primary(): """Основная кнопка (синяя)""" return f""" QPushButton {{ background-color: {Colors.PRIMARY}; color: {Colors.TEXT_ON_PRIMARY}; {ButtonStyles._base_style()} }} QPushButton:hover {{ background-color: {Colors.PRIMARY_LIGHT}; }} QPushButton:pressed {{ background-color: {Colors.PRIMARY_DARK}; }} QPushButton:disabled {{ background-color: {Colors.GRAY_400}; color: {Colors.GRAY_600}; }} """ @staticmethod def success(): """Кнопка успеха (зелёная)""" return f""" QPushButton {{ background-color: {Colors.SUCCESS}; color: {Colors.TEXT_ON_PRIMARY}; {ButtonStyles._base_style()} }} QPushButton:hover {{ background-color: {Colors.SUCCESS_DARK}; }} QPushButton:pressed {{ background-color: {Colors.SUCCESS_LIGHT}; }} QPushButton:disabled {{ background-color: {Colors.GRAY_400}; color: {Colors.GRAY_600}; }} """ @staticmethod def danger(): """Кнопка опасности/удаления (красная)""" return f""" QPushButton {{ background-color: {Colors.DANGER}; color: {Colors.TEXT_ON_PRIMARY}; {ButtonStyles._base_style()} }} QPushButton:hover {{ background-color: {Colors.DANGER_DARK}; }} QPushButton:pressed {{ background-color: {Colors.DANGER_LIGHT}; }} """ @staticmethod def warning(): """Кнопка предупреждения (оранжевая)""" return f""" QPushButton {{ background-color: {Colors.WARNING}; color: {Colors.TEXT_ON_PRIMARY}; {ButtonStyles._base_style()} }} QPushButton:hover {{ background-color: {Colors.WARNING_DARK}; }} QPushButton:pressed {{ background-color: {Colors.WARNING_LIGHT}; }} """ @staticmethod def outline(): """Контурная кнопка""" return f""" QPushButton {{ background-color: {Colors.TRANSPARENT}; color: {Colors.PRIMARY}; border: 1px solid {Colors.PRIMARY}; border-radius: {Spacing.BORDER_RADIUS_MD}px; padding: {Spacing.SM}px {Spacing.LG}px; font-weight: {Fonts.WEIGHT_SEMIBOLD}; }} QPushButton:hover {{ background-color: {Colors.PRIMARY_BG}; }} QPushButton:pressed {{ background-color: {Colors.PRIMARY_LIGHT}; color: {Colors.TEXT_ON_PRIMARY}; }} """ @staticmethod def ghost(): """Прозрачная кнопка (только текст)""" return f""" QPushButton {{ background-color: {Colors.TRANSPARENT}; color: {Colors.PRIMARY}; border: none; padding: {Spacing.SM}px {Spacing.LG}px; }} QPushButton:hover {{ background-color: {Colors.PRIMARY_BG}; border-radius: {Spacing.BORDER_RADIUS_MD}px; }} """ # ========== СТИЛИ ПОЛЕЙ ВВОДА ========== class InputStyles: """Стили для полей ввода""" @staticmethod def default(): return f""" QComboBox {{ border: 1px solid {Colors.BORDER_DEFAULT}; border-radius: {Spacing.BORDER_RADIUS_SM}px; padding: 1px; background-color: {Colors.WHITE}; min-height: {Spacing.INPUT_HEIGHT - 16}px; }} QLineEdit, QDoubleSpinBox, QSpinBox {{ border: 1px solid {Colors.BORDER_DEFAULT}; border-radius: {Spacing.BORDER_RADIUS_SM}px; padding: {Spacing.SM}px; background-color: {Colors.WHITE}; min-height: {Spacing.INPUT_HEIGHT - 16}px; }} QLineEdit:focus, QDoubleSpinBox:focus, QSpinBox:focus, QComboBox:focus {{ border: 1px solid {Colors.PRIMARY}; outline: none; }} QLineEdit:disabled, QDoubleSpinBox:disabled, QSpinBox:disabled {{ background-color: {Colors.GRAY_200}; color: {Colors.GRAY_600}; }} """ @staticmethod def error(): return f""" QLineEdit, QDoubleSpinBox, QSpinBox {{ border: 1px solid {Colors.DANGER}; border-radius: {Spacing.BORDER_RADIUS_SM}px; padding: {Spacing.SM}px; background-color: {Colors.DANGER_BG}; }} """ @staticmethod def success_border(): return f""" QLineEdit, QDoubleSpinBox, QSpinBox {{ border: 1px solid {Colors.SUCCESS}; border-radius: {Spacing.BORDER_RADIUS_SM}px; padding: {Spacing.SM}px; background-color: {Colors.SUCCESS_BG}; }} """ # ========== СТИЛИ ТАБЛИЦ ========== class TableStyles: """Стили для таблиц""" @staticmethod def default(): return f""" QTableWidget {{ gridline-color: {Colors.BORDER_LIGHT}; background-color: {Colors.WHITE}; alternate-background-color: {Colors.TABLE_ALTERNATE_ROW}; selection-background-color: {Colors.PRIMARY_BG}; selection-color: {Colors.TEXT_PRIMARY}; }} QHeaderView::section {{ background-color: {Colors.INFO}; color: {Colors.TEXT_ON_DARK}; padding: {Spacing.SM}px; font-weight: {Fonts.WEIGHT_BOLD}; border: none; }} QTableWidget::item {{ padding: {Spacing.XS}px; }} QTableWidget::item:selected {{ background-color: {Colors.PRIMARY_BG}; color: {Colors.TEXT_PRIMARY}; }} """ @staticmethod def compact(): """Компактный стиль таблицы""" return f""" QTableWidget {{ gridline-color: {Colors.BORDER_LIGHT}; background-color: {Colors.WHITE}; }} QHeaderView::section {{ background-color: {Colors.INFO}; color: {Colors.TEXT_ON_DARK}; padding: {Spacing.XS}px; font-weight: {Fonts.WEIGHT_BOLD}; }} QTableWidget::item {{ padding: {Spacing.XS}px; }} """ # ========== СТИЛИ ВКЛАДОК ========== class TabStyles: """Стили для вкладок (табов)""" @staticmethod def default(): return f""" QTabWidget::pane {{ border: 1px solid {Colors.BORDER_DEFAULT}; border-radius: {Spacing.BORDER_RADIUS_MD}px; background-color: {Colors.WHITE}; }} QTabBar::tab {{ background-color: {Colors.GRAY_200}; padding: {Spacing.SM}px {Spacing.XL}px; margin-right: {Spacing.XS}px; border-top-left-radius: {Spacing.BORDER_RADIUS_SM}px; border-top-right-radius: {Spacing.BORDER_RADIUS_SM}px; }} QTabBar::tab:selected {{ background-color: {Colors.PRIMARY}; color: {Colors.TEXT_ON_PRIMARY}; }} QTabBar::tab:hover:!selected {{ background-color: {Colors.PRIMARY_BG}; }} """ # ========== СТИЛИ ГРУПП ========== class GroupBoxStyles: """Стили для групп (GroupBox)""" @staticmethod def default(): return f""" QGroupBox {{ font-weight: {Fonts.WEIGHT_BOLD}; border: 2px solid {Colors.BORDER_DEFAULT}; border-radius: {Spacing.BORDER_RADIUS_MD}px; margin-top: {Spacing.LG}px; padding-top: {Spacing.LG}px; font-size: {Fonts.SIZE_MEDIUM}px; }} QGroupBox::title {{ subcontrol-origin: margin; left: {Spacing.LG}px; padding: 0 {Spacing.MD}px; color: {Colors.INFO}; }} """ @staticmethod def card(): """Стиль карточки""" return f""" QGroupBox {{ background-color: {Colors.WHITE}; border: 1px solid {Colors.BORDER_DEFAULT}; border-radius: {Spacing.BORDER_RADIUS_LG}px; margin-top: {Spacing.LG}px; padding-top: {Spacing.LG}px; }} QGroupBox::title {{ subcontrol-origin: margin; left: {Spacing.LG}px; padding: 0 {Spacing.MD}px; color: {Colors.PRIMARY}; }} """ # ========== СТИЛИ ПРОГРЕССА ========== class ProgressStyles: """Стили для индикаторов прогресса""" @staticmethod def default(): return f""" QProgressBar {{ border: 1px solid {Colors.BORDER_DEFAULT}; border-radius: {Spacing.BORDER_RADIUS_SM}px; text-align: center; background-color: {Colors.GRAY_200}; }} QProgressBar::chunk {{ background-color: {Colors.PRIMARY}; border-radius: {Spacing.BORDER_RADIUS_SM}px; }} """ @staticmethod def success(): return f""" QProgressBar::chunk {{ background-color: {Colors.SUCCESS}; }} """ # ========== СТИЛИ СТАТУСОВ ========== class StatusStyles: """Стили для статусных сообщений""" @staticmethod def info(): return f""" background-color: {Colors.PRIMARY_BG}; color: {Colors.PRIMARY_DARK}; padding: {Spacing.MD}px; border-radius: {Spacing.BORDER_RADIUS_MD}px; border-left: 4px solid {Colors.PRIMARY}; """ @staticmethod def success(): return f""" background-color: {Colors.SUCCESS_BG}; color: {Colors.SUCCESS_DARK}; padding: {Spacing.MD}px; border-radius: {Spacing.BORDER_RADIUS_MD}px; border-left: 4px solid {Colors.SUCCESS}; """ @staticmethod def warning(): return f""" background-color: {Colors.WARNING_BG}; color: {Colors.WARNING_DARK}; padding: {Spacing.MD}px; border-radius: {Spacing.BORDER_RADIUS_MD}px; border-left: 4px solid {Colors.WARNING}; """ @staticmethod def error(): return f""" background-color: {Colors.DANGER_BG}; color: {Colors.DANGER_DARK}; padding: {Spacing.MD}px; border-radius: {Spacing.BORDER_RADIUS_MD}px; border-left: 4px solid {Colors.DANGER}; """ # ========== ПОЛНАЯ ТЕМА ========== def get_full_stylesheet(): """ Возвращает полную таблицу стилей для приложения """ return f""" /* Глобальные стили */ QMainWindow {{ background-color: {Colors.GRAY_200}; }} QWidget {{ font-family: {Fonts.FAMILY_PRIMARY}; font-size: {Fonts.SIZE_NORMAL}px; color: {Colors.TEXT_PRIMARY}; }} QLabel {{ color: {Colors.TEXT_PRIMARY}; }} /* Кнопки с идентификаторами */ QPushButton#primary {{ {ButtonStyles.primary()} }} QPushButton#success {{ {ButtonStyles.success()} }} QPushButton#danger {{ {ButtonStyles.danger()} }} QPushButton#warning {{ {ButtonStyles.warning()} }} QPushButton#outline {{ {ButtonStyles.outline()} }} /* Группы */ {GroupBoxStyles.default()} /* Таблицы */ {TableStyles.default()} /* Вкладки */ {TabStyles.default()} /* Поля ввода */ {InputStyles.default()} /* ScrollArea */ QScrollArea {{ border: none; background-color: {Colors.TRANSPARENT}; }} QScrollBar:vertical {{ border: none; background-color: {Colors.GRAY_200}; width: {Spacing.LG}px; border-radius: {Spacing.BORDER_RADIUS_SM}px; }} QScrollBar::handle:vertical {{ background-color: {Colors.GRAY_500}; min-height: {Spacing.XL}px; border-radius: {Spacing.BORDER_RADIUS_SM}px; }} QScrollBar::handle:vertical:hover {{ background-color: {Colors.GRAY_600}; }} QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ border: none; background: none; }} /* ToolBar */ QToolBar {{ background-color: {Colors.WHITE}; border-bottom: 1px solid {Colors.BORDER_DEFAULT}; padding: {Spacing.SM}px; spacing: {Spacing.SM}px; }} QToolBar QToolButton {{ background-color: {Colors.TRANSPARENT}; padding: {Spacing.SM}px {Spacing.LG}px; border-radius: {Spacing.BORDER_RADIUS_MD}px; }} QToolBar QToolButton:hover {{ background-color: {Colors.GRAY_200}; }} /* Message Box */ QMessageBox {{ background-color: {Colors.WHITE}; }} QMessageBox QPushButton {{ min-width: 80px; padding: {Spacing.SM}px; }} """ # ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ========== def apply_theme(widget): """ Применяет тему к виджету и всем его дочерним элементам """ from PyQt5.QtWidgets import QApplication # Устанавливаем стиль приложения app = QApplication.instance() if app: app.setStyleSheet(get_full_stylesheet()) # Дополнительно устанавливаем виджету if widget: widget.setStyleSheet(get_full_stylesheet()) def get_primary_color(): """Возвращает основной цвет приложения""" return Colors.PRIMARY def get_success_color(): """Возвращает цвет успеха""" return Colors.SUCCESS def get_danger_color(): """Возвращает цвет опасности""" return Colors.DANGER def get_warning_color(): """Возвращает цвет предупреждения""" return Colors.WARNING