Добавлен файл темы. Исправлен счёт веса реагентов
This commit is contained in:
+15
-9
@@ -52,12 +52,13 @@ def convert_units(value: float, from_unit: str, to_unit: str = None) -> float:
|
|||||||
units_map = MASS_UNITS
|
units_map = MASS_UNITS
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Неизвестная единица измерения: {from_unit}")
|
raise ValueError(f"Неизвестная единица измерения: {from_unit}")
|
||||||
|
print("units_map = ",units_map)
|
||||||
# Конвертируем в базовую единицу (мкл для объёма, мг для массы)
|
# Конвертируем в базовую единицу (мкл для объёма, мг для массы)
|
||||||
value_in_base = value * units_map[from_unit]
|
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 / units_map[to_unit]
|
||||||
|
|
||||||
return value_in_base
|
return value_in_base
|
||||||
@@ -158,7 +159,13 @@ def calculate_medium_composition(
|
|||||||
# Извлекаем параметры с значениями по умолчанию
|
# Извлекаем параметры с значениями по умолчанию
|
||||||
percentage = reagent.get('percentage', 0)
|
percentage = reagent.get('percentage', 0)
|
||||||
unit = reagent.get('unit', 'мг')
|
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)
|
# conversion_factor = reagent.get('conversion_factor', 1.0)
|
||||||
dilution_factor = reagent.get('dilution_factor', 1.0)
|
dilution_factor = reagent.get('dilution_factor', 1.0)
|
||||||
|
|
||||||
@@ -167,20 +174,19 @@ def calculate_medium_composition(
|
|||||||
|
|
||||||
# 1. Объём реагента в среде (исходя из процента)
|
# 1. Объём реагента в среде (исходя из процента)
|
||||||
amount_in_base = (percentage / 100) * total_base
|
amount_in_base = (percentage / 100) * total_base
|
||||||
# print ("amount_in_base = ",amount_in_base)
|
print ("amount_in_base = ",amount_in_base)
|
||||||
# 2. Применяем коэффициент конверсии
|
# 2. Применяем коэффициент конверсии
|
||||||
# adjusted_amount_base = amount_in_base * conversion_factor
|
# adjusted_amount_base = amount_in_base * conversion_factor
|
||||||
|
|
||||||
# 3. Конвертируем в нужную единицу (без учёта разбавления)
|
# 3. Конвертируем в нужную единицу (без учёта разбавления)
|
||||||
# undiluted_amount = convert_units(adjusted_amount_base, volume_unit, unit)
|
# undiluted_amount = convert_units(adjusted_amount_base, volume_unit, unit)
|
||||||
undiluted_amount = convert_units(amount_in_base, 'мкл', unit)
|
undiluted_amount = convert_units(amount_in_base, base_unit, unit)
|
||||||
# print ("volume_unit = ",volume_unit)
|
print ("undiluted_amount = ",undiluted_amount)
|
||||||
|
|
||||||
# 4. Применяем разбавление
|
# 4. Применяем разбавление
|
||||||
if dilution_factor <= 0:
|
if dilution_factor <= 0:
|
||||||
dilution_factor = 1.0
|
dilution_factor = 1.0
|
||||||
diluted_amount = undiluted_amount * dilution_factor
|
diluted_amount = undiluted_amount * dilution_factor
|
||||||
# print ("diluted_amount = ", diluted_amount)
|
print ("diluted_amount = ", diluted_amount)
|
||||||
# 5. Для объёмных реагентов учитываем в расчёте растворителя
|
# 5. Для объёмных реагентов учитываем в расчёте растворителя
|
||||||
if is_volume:
|
if is_volume:
|
||||||
reagent_volume_base = convert_units(diluted_amount, unit)
|
reagent_volume_base = convert_units(diluted_amount, unit)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
Единый графический интерфейс для калькулятора сред и DoE
|
Единый графический интерфейс для калькулятора сред и DoE
|
||||||
"""
|
"""
|
||||||
|
from theme import Colors, Fonts, Spacing, ButtonStyles, get_full_stylesheet, apply_theme
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
@@ -49,6 +49,7 @@ class MainWindow(QMainWindow):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("Биохимический помощник")
|
self.setWindowTitle("Биохимический помощник")
|
||||||
|
self.setStyleSheet(get_full_stylesheet())
|
||||||
self.setGeometry(100, 100, 1300, 800)
|
self.setGeometry(100, 100, 1300, 800)
|
||||||
self.setStyleSheet(self._get_stylesheet())
|
self.setStyleSheet(self._get_stylesheet())
|
||||||
|
|
||||||
@@ -60,22 +61,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._add_file_toolbar()
|
self._add_file_toolbar()
|
||||||
|
|
||||||
def _get_stylesheet(self):
|
def _get_stylesheet(self):
|
||||||
return """
|
return get_full_stylesheet()
|
||||||
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; }
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _add_file_toolbar(self):
|
def _add_file_toolbar(self):
|
||||||
"""Добавляет панель инструментов с кнопками сохранения/загрузки"""
|
"""Добавляет панель инструментов с кнопками сохранения/загрузки"""
|
||||||
@@ -210,6 +196,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.reagents_table.setItem(row, 1, QTableWidgetItem("0"))
|
self.reagents_table.setItem(row, 1, QTableWidgetItem("0"))
|
||||||
|
|
||||||
unit_combo = QComboBox()
|
unit_combo = QComboBox()
|
||||||
|
unit_combo.setStyleSheet("""
|
||||||
|
QComboBox {
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
|
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
|
||||||
unit_combo.setCurrentText("мл")
|
unit_combo.setCurrentText("мл")
|
||||||
self.reagents_table.setCellWidget(row, 2, unit_combo)
|
self.reagents_table.setCellWidget(row, 2, unit_combo)
|
||||||
|
|||||||
@@ -1,14 +1,30 @@
|
|||||||
|
# main.py
|
||||||
"""
|
"""
|
||||||
Биохимический помощник - точка входа в приложение
|
Биохимический помощник - точка входа в приложение
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
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__))
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user