Files
help_lab/theme.py
T

746 lines
24 KiB
Python

# theme.py
"""
Настройки темы и цветовой схемы приложения
Централизованное управление стилями для единообразного внешнего вида
"""
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFontDatabase, QFont
# ========== ОСНОВНЫЕ ЦВЕТА ==========
class Colors:
"""Цветовая палитра приложения"""
# Основные цвета (Primary)
PRIMARY = "#3498db" # Синий - основной акцент
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
# Цвет для заголовка
TITLE_COLOR = "#2c3e50"
# Прозрачность
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_XXLARGE)
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 TitleStyles:
@staticmethod
def main_title():
return f"""
color: {Colors.TITLE_COLOR};
padding: {Spacing.XL}px;
font-size: {Fonts.SIZE_XLARGE}px;
font-weight: {Fonts.WEIGHT_BOLD};
"""
# ========== СТИЛИ КНОПОК ==========
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: 10 {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: 10 {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"""
/* СТИЛИ ДЛЯ ВСЕХ КНОПОК ПО УМОЛЧАНИЮ */
QPushButton {{
background-color: {Colors.PRIMARY};
color: {Colors.TEXT_ON_PRIMARY};
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;
}}
QPushButton:hover {{
background-color: {Colors.PRIMARY_LIGHT};
}}
QPushButton:pressed {{
background-color: {Colors.PRIMARY_DARK};
}}
/* Глобальные стили */
QLabel#mainTitle {{
{TitleStyles.main_title()}
}}
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