From 63b5f0a49f55bc3e35b64e6cd399e54662fa7ba0 Mon Sep 17 00:00:00 2001 From: Artemiy Date: Fri, 22 May 2026 14:23:38 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=84=D0=B0=D0=B9=D0=BB=20=D1=82=D0=B5=D0=BC=D1=8B.?= =?UTF-8?q?=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20?= =?UTF-8?q?=D1=81=D1=87=D1=91=D1=82=20=D0=B2=D0=B5=D1=81=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- calculations/medium.py | 24 +- gui.py | 25 +- main.py | 18 +- theme.py | 699 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 739 insertions(+), 27 deletions(-) create mode 100644 theme.py diff --git a/calculations/medium.py b/calculations/medium.py index b7268c1..03d008f 100644 --- a/calculations/medium.py +++ b/calculations/medium.py @@ -52,12 +52,13 @@ def convert_units(value: float, from_unit: str, to_unit: str = None) -> float: units_map = MASS_UNITS else: raise ValueError(f"Неизвестная единица измерения: {from_unit}") - + print("units_map = ",units_map) # Конвертируем в базовую единицу (мкл для объёма, мг для массы) value_in_base = value * units_map[from_unit] - + print ("units_map[$from_unit]",units_map[from_unit]) + print ("value_in_base = ",value_in_base) # Если нужна конвертация в другую единицу - if to_unit and to_unit in units_map: + if from_unit and to_unit in units_map: return value_in_base / units_map[to_unit] return value_in_base @@ -158,7 +159,13 @@ def calculate_medium_composition( # Извлекаем параметры с значениями по умолчанию percentage = reagent.get('percentage', 0) unit = reagent.get('unit', 'мг') -# print ("unit = ",unit) + if unit in VOLUME_UNITS: + base_unit = "мкл" + elif unit in MASS_UNITS: + base_unit = "мг" + else: + raise ValueError(f"Неизвестная единица измерения: {from_unit}") + print ("unit = ",unit) # conversion_factor = reagent.get('conversion_factor', 1.0) dilution_factor = reagent.get('dilution_factor', 1.0) @@ -167,20 +174,19 @@ def calculate_medium_composition( # 1. Объём реагента в среде (исходя из процента) amount_in_base = (percentage / 100) * total_base -# print ("amount_in_base = ",amount_in_base) + print ("amount_in_base = ",amount_in_base) # 2. Применяем коэффициент конверсии # adjusted_amount_base = amount_in_base * conversion_factor # 3. Конвертируем в нужную единицу (без учёта разбавления) # undiluted_amount = convert_units(adjusted_amount_base, volume_unit, unit) - undiluted_amount = convert_units(amount_in_base, 'мкл', unit) -# print ("volume_unit = ",volume_unit) - + undiluted_amount = convert_units(amount_in_base, base_unit, unit) + print ("undiluted_amount = ",undiluted_amount) # 4. Применяем разбавление if dilution_factor <= 0: dilution_factor = 1.0 diluted_amount = undiluted_amount * dilution_factor -# print ("diluted_amount = ", diluted_amount) + print ("diluted_amount = ", diluted_amount) # 5. Для объёмных реагентов учитываем в расчёте растворителя if is_volume: reagent_volume_base = convert_units(diluted_amount, unit) diff --git a/gui.py b/gui.py index cd10165..5f336b4 100644 --- a/gui.py +++ b/gui.py @@ -2,7 +2,7 @@ """ Единый графический интерфейс для калькулятора сред и DoE """ - +from theme import Colors, Fonts, Spacing, ButtonStyles, get_full_stylesheet, apply_theme import sys from typing import List, Dict, Optional @@ -49,6 +49,7 @@ class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Биохимический помощник") + self.setStyleSheet(get_full_stylesheet()) self.setGeometry(100, 100, 1300, 800) self.setStyleSheet(self._get_stylesheet()) @@ -60,22 +61,7 @@ class MainWindow(QMainWindow): self._add_file_toolbar() def _get_stylesheet(self): - return """ - QMainWindow { background-color: #f0f0f0; } - QGroupBox { font-weight: bold; border: 2px solid #ccc; border-radius: 8px; margin-top: 12px; padding-top: 12px; } - QGroupBox::title { subcontrol-origin: margin; left: 10px; padding: 0 5px; color: #2c3e50; } - QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 5px; font-weight: bold; } - QPushButton:hover { background-color: #2980b9; } - QPushButton#success { background-color: #27ae60; } - QPushButton#success:hover { background-color: #219a52; } - QPushButton#danger { background-color: #e74c3c; } - QPushButton#danger:hover { background-color: #c0392b; } - QTableWidget { gridline-color: #ddd; background-color: white; alternate-background-color: #f9f9f9; } - QHeaderView::section { background-color: #34495e; color: white; padding: 6px; font-weight: bold; } - QTabWidget::pane { border: 1px solid #ccc; border-radius: 5px; } - QTabBar::tab { background-color: #ecf0f1; padding: 8px 16px; margin-right: 2px; } - QTabBar::tab:selected { background-color: #3498db; color: white; } - """ + return get_full_stylesheet() def _add_file_toolbar(self): """Добавляет панель инструментов с кнопками сохранения/загрузки""" @@ -210,6 +196,11 @@ class MainWindow(QMainWindow): self.reagents_table.setItem(row, 1, QTableWidgetItem("0")) unit_combo = QComboBox() + unit_combo.setStyleSheet(""" + QComboBox { + padding: 1px; + } + """) unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"]) unit_combo.setCurrentText("мл") self.reagents_table.setCellWidget(row, 2, unit_combo) diff --git a/main.py b/main.py index 85189b3..cf4c6dc 100644 --- a/main.py +++ b/main.py @@ -1,14 +1,30 @@ +# main.py """ Биохимический помощник - точка входа в приложение """ import sys import os +from PyQt5.QtWidgets import QApplication +from gui import MainWindow +from theme import Fonts, setup_emoji_support # Добавляем текущую директорию в путь sys.path.insert(0, os.path.dirname(__file__)) -from gui import main + + + +def main(): + app = QApplication(sys.argv) + + # Настраиваем поддержку эмодзи + setup_emoji_support(app) + + window = MainWindow() + window.show() + sys.exit(app.exec_()) + if __name__ == "__main__": main() diff --git a/theme.py b/theme.py new file mode 100644 index 0000000..5a1426c --- /dev/null +++ b/theme.py @@ -0,0 +1,699 @@ +# theme.py +""" +Настройки темы и цветовой схемы приложения +Централизованное управление стилями для единообразного внешнего вида +""" + +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFontDatabase, QFont + +# ========== ОСНОВНЫЕ ЦВЕТА ========== +class Colors: + """Цветовая палитра приложения""" + + # Основные цвета (Primary) + PRIMARY = "#3498db" # Синий - основной акцент + PRIMARY_DARK = "#2980b9" # Тёмно-синий (наведение) + 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_DARK}; + }} + QPushButton:pressed {{ + background-color: {Colors.PRIMARY_LIGHT}; + }} + 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""" + QLineEdit, QDoubleSpinBox, QSpinBox, QComboBox {{ + 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