Compare commits
9 Commits
3ddd05acff
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 85e6aa6c72 | |||
| 2392735641 | |||
| 6400f04f1c | |||
| acf3ad0dd5 | |||
| a2bc606336 | |||
| f479674332 | |||
| 1ddfe20a8d | |||
| 63b5f0a49f | |||
| 75b710bf59 |
@@ -1,6 +1,9 @@
|
|||||||
# Python
|
# Python
|
||||||
backup
|
backup
|
||||||
Backup
|
Backup
|
||||||
|
*.csv
|
||||||
|
*.xls
|
||||||
|
*.xlsx
|
||||||
*.json
|
*.json
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
+62
-126
@@ -13,50 +13,70 @@
|
|||||||
from typing import List, Dict, Tuple, Optional
|
from typing import List, Dict, Tuple, Optional
|
||||||
import random
|
import random
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from calculations.medium import (
|
||||||
|
calculate_medium_composition,
|
||||||
|
convert_units,
|
||||||
|
VOLUME_UNITS,
|
||||||
|
MASS_UNITS
|
||||||
|
)
|
||||||
|
from calculations.models.project_data import FactorData
|
||||||
# Типы расчёта шага
|
# Типы расчёта шага
|
||||||
FACTOR_TYPES = {
|
FACTOR_TYPES = {
|
||||||
'absolute': 'абс', # абсолютный шаг
|
'absolute': 'ед.', # абсолютный шаг
|
||||||
'relative': '%', # относительный шаг (процент от нулевого уровня)
|
'relative': '%', # относительный шаг (процент от нулевого уровня)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def calculate_factor_levels(
|
def calculate_factor_levels(
|
||||||
center_value: float,
|
factor: 'FactorData',
|
||||||
step_value: float,
|
total_volume: float = 1000,
|
||||||
step_type: str,
|
volume_unit: str = "мл"
|
||||||
base_value: float = None
|
) -> 'FactorData':
|
||||||
) -> Tuple[float, float]:
|
"""Рассчитывает high/low уровни фактора через калькулятор сред"""
|
||||||
"""
|
|
||||||
РАССЧИТЫВАЕТ ВЕРХНИЙ И НИЖНИЙ УРОВНИ ФАКТОРА
|
|
||||||
|
|
||||||
Параметры:
|
# Функция для расчёта количества реагента при заданном проценте
|
||||||
center_value: нулевой уровень фактора (центральная точка)
|
def calc_amount(percentage: float) -> float:
|
||||||
step_value: значение шага
|
reagents = [{
|
||||||
step_type: тип шага ("абс" - абсолютный, "%" - относительный)
|
'name': factor.name,
|
||||||
base_value: базовое значение для относительного шага (если None, используется center_value)
|
'percentage': max(0, percentage),
|
||||||
|
'unit': factor.unit,
|
||||||
|
'dilution_factor': factor.dilution_factor or 1.0
|
||||||
|
}]
|
||||||
|
result = calculate_medium_composition(total_volume, volume_unit, reagents)
|
||||||
|
return result['reagents'][0]['calculated_amount']
|
||||||
|
|
||||||
Возвращает:
|
|
||||||
(high_level, low_level): верхний и нижний уровни
|
|
||||||
|
|
||||||
Пример:
|
# Определяем проценты для верхнего и нижнего уровней
|
||||||
>>> calculate_factor_levels(100, 10, "%")
|
if factor.step_type == "%":
|
||||||
(110.0, 90.0)
|
low = calc_amount(factor.percentage - factor.step) # low - нижний уровень
|
||||||
>>> calculate_factor_levels(100, 20, "абс")
|
high = calc_amount(factor.percentage + factor.step) # high - верхний уровень
|
||||||
(120.0, 80.0)
|
else: # "ед."
|
||||||
"""
|
# Конвертируем абсолютный шаг в проценты
|
||||||
# Определяем абсолютное значение шага
|
low = calc_amount(factor.percentage) - factor.step
|
||||||
if step_type == "%":
|
high = calc_amount(factor.center) + factor.step
|
||||||
base = base_value if base_value is not None else center_value
|
|
||||||
step_abs = center_value * step_value / 100
|
|
||||||
else: # "абс" или "absolute"
|
|
||||||
step_abs = step_value
|
|
||||||
|
|
||||||
high_level = center_value + step_abs
|
# ВОЗВРАЩАЕМ НОВЫЙ ОБЪЕКТ FactorData
|
||||||
low_level = center_value - step_abs
|
return FactorData(
|
||||||
|
name=factor.name,
|
||||||
|
center=factor.center,
|
||||||
|
low=low, # low - нижний уровень
|
||||||
|
high=high, # high - верхний уровень
|
||||||
|
step=factor.step,
|
||||||
|
step_type=factor.step_type,
|
||||||
|
unit=factor.unit,
|
||||||
|
percentage=factor.percentage or factor.center,
|
||||||
|
dilution_factor=factor.dilution_factor
|
||||||
|
)
|
||||||
|
|
||||||
return high_level, low_level
|
def calculate_all_factors_levels(factors: List['FactorData'], **kwargs) -> List['FactorData']:
|
||||||
|
"""Рассчитывает уровни для всех факторов"""
|
||||||
|
return [calculate_factor_levels(f, **kwargs) for f in factors]
|
||||||
|
|
||||||
|
def get_active_factors(factors):
|
||||||
|
return [f for f in factors if f['step'] != 0]
|
||||||
|
|
||||||
|
def get_inactive_factors(factors):
|
||||||
|
return [f for f in factors if f['step'] == 0]
|
||||||
|
|
||||||
def generate_factorial_design(
|
def generate_factorial_design(
|
||||||
factors: List[Dict],
|
factors: List[Dict],
|
||||||
@@ -64,56 +84,11 @@ def generate_factorial_design(
|
|||||||
randomize: bool = True
|
randomize: bool = True
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
ГЕНЕРИРУЕТ ПОЛНОФАКТОРНЫЙ ПЛАН ЭКСПЕРИМЕНТА
|
Генерирует полнофакторный план 2^k с правильным порядком изменения факторов:
|
||||||
|
- фактор 1 меняется через 1 эксперимент (2^0)
|
||||||
Создаёт матрицу планирования для 2^k полнофакторного эксперимента
|
- фактор 2 меняется через 2 эксперимента (2^1)
|
||||||
с добавлением центральных точек.
|
- фактор 3 меняется через 4 эксперимента (2^2)
|
||||||
|
- и т.д.
|
||||||
ПАРАМЕТРЫ:
|
|
||||||
----------
|
|
||||||
factors : List[Dict]
|
|
||||||
Список факторов. Каждый фактор - словарь с ключами:
|
|
||||||
- name (str): название фактора
|
|
||||||
- low (float): нижний уровень (-1)
|
|
||||||
- high (float): верхний уровень (+1)
|
|
||||||
- center (float): нулевой уровень (0)
|
|
||||||
- unit (str): единица измерения
|
|
||||||
- step (float, опционально): шаг варьирования
|
|
||||||
- step_type (str, опционально): тип шага ("абс" или "%")
|
|
||||||
|
|
||||||
Минимально необходимые ключи: name, low, high, center, unit
|
|
||||||
|
|
||||||
center_points : int
|
|
||||||
Количество центральных точек (повторений в центре плана)
|
|
||||||
По умолчанию 3
|
|
||||||
|
|
||||||
randomize : bool
|
|
||||||
Перемешивать ли порядок опытов случайным образом
|
|
||||||
По умолчанию True
|
|
||||||
|
|
||||||
ВОЗВРАЩАЕТ:
|
|
||||||
-----------
|
|
||||||
List[Dict]
|
|
||||||
Список экспериментов. Каждый эксперимент - словарь:
|
|
||||||
- для каждого фактора: "Фактор_N" с полями:
|
|
||||||
- coded: кодированное значение (-1, 0, +1)
|
|
||||||
- natural: натуральное значение
|
|
||||||
- name: название фактора
|
|
||||||
- unit: единица измерения
|
|
||||||
- is_center (bool): является ли точка центральной
|
|
||||||
- center_num (int): номер центральной точки (если is_center)
|
|
||||||
|
|
||||||
ПРИМЕР ИСПОЛЬЗОВАНИЯ:
|
|
||||||
---------------------
|
|
||||||
>>> factors = [
|
|
||||||
... {'name': 'Температура', 'low': 25, 'high': 37, 'center': 31, 'unit': '°C'},
|
|
||||||
... {'name': 'pH', 'low': 6.5, 'high': 7.5, 'center': 7.0, 'unit': ''}
|
|
||||||
... ]
|
|
||||||
>>> design = generate_factorial_design(factors, center_points=2)
|
|
||||||
>>> print(len(design)) # 2^2 + 2 = 6
|
|
||||||
6
|
|
||||||
>>> design[0]['Фактор_1']['coded'] # первый фактор в первом опыте
|
|
||||||
-1
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
k = len(factors)
|
k = len(factors)
|
||||||
@@ -123,15 +98,14 @@ def generate_factorial_design(
|
|||||||
n_factorial = 2 ** k
|
n_factorial = 2 ** k
|
||||||
design = []
|
design = []
|
||||||
|
|
||||||
# Генерация факторных точек (все комбинации уровней)
|
# Генерация факторных точек в правильном порядке (двоичный счётчик)
|
||||||
for i in range(n_factorial):
|
for i in range(n_factorial):
|
||||||
experiment = {}
|
experiment = {}
|
||||||
for j in range(k):
|
for j in range(k):
|
||||||
# Кодированный уровень: -1 для 0, +1 для 1 в бите
|
# Правильный порядок битов: младший бит - первый фактор
|
||||||
# (k-1-j) для правильного порядка факторов
|
# (j) - для прямого порядка: фактор 1 меняется чаще всего
|
||||||
coded_level = -1 if (i >> (k - 1 - j)) & 1 == 0 else 1
|
coded_level = -1 if (i >> j) & 1 == 0 else 1
|
||||||
|
|
||||||
# Натуральное значение
|
|
||||||
natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high']
|
natural_value = factors[j]['low'] if coded_level == -1 else factors[j]['high']
|
||||||
|
|
||||||
experiment[f"Фактор_{j+1}"] = {
|
experiment[f"Фактор_{j+1}"] = {
|
||||||
@@ -156,7 +130,7 @@ def generate_factorial_design(
|
|||||||
center_experiment['center_num'] = i + 1
|
center_experiment['center_num'] = i + 1
|
||||||
design.append(center_experiment)
|
design.append(center_experiment)
|
||||||
|
|
||||||
# Перемешивание порядка
|
# Перемешивание порядка (опционально)
|
||||||
if randomize:
|
if randomize:
|
||||||
random.shuffle(design)
|
random.shuffle(design)
|
||||||
|
|
||||||
@@ -273,44 +247,6 @@ def analyze_experiment(
|
|||||||
return analysis
|
return analysis
|
||||||
|
|
||||||
|
|
||||||
def create_factor_from_reagent(
|
|
||||||
reagent: Dict,
|
|
||||||
total_volume: float,
|
|
||||||
volume_unit: str,
|
|
||||||
step_percent: float = 10.0
|
|
||||||
) -> Dict:
|
|
||||||
"""
|
|
||||||
СОЗДАЁТ ФАКТОР ИЗ РЕАГЕНТА (для интеграции калькулятора и DoE)
|
|
||||||
|
|
||||||
Преобразует рассчитанный реагент в фактор для планирования эксперимента.
|
|
||||||
|
|
||||||
Параметры:
|
|
||||||
reagent: рассчитанный реагент (из calculate_medium_composition)
|
|
||||||
total_volume: общий объём среды
|
|
||||||
volume_unit: единица объёма
|
|
||||||
step_percent: шаг варьирования в процентах от нулевого уровня
|
|
||||||
|
|
||||||
Возвращает:
|
|
||||||
Dict: фактор для использования в generate_factorial_design()
|
|
||||||
"""
|
|
||||||
center_value = reagent.get('undiluted_amount', reagent.get('calculated_amount', 0))
|
|
||||||
step_value = center_value * step_percent / 100
|
|
||||||
|
|
||||||
high_level, low_level = calculate_factor_levels(
|
|
||||||
center_value, step_value, "абс"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'name': reagent['name'],
|
|
||||||
'center': center_value,
|
|
||||||
'low': low_level,
|
|
||||||
'high': high_level,
|
|
||||||
'step': step_value,
|
|
||||||
'step_type': 'абс',
|
|
||||||
'unit': reagent.get('unit', volume_unit),
|
|
||||||
'percentage': reagent.get('percentage', 0),
|
|
||||||
'dilution_factor': reagent.get('dilution_factor', 1.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
def create_factor_from_reagent(
|
def create_factor_from_reagent(
|
||||||
reagent: Dict,
|
reagent: Dict,
|
||||||
@@ -336,7 +272,7 @@ def create_factor_from_reagent(
|
|||||||
step_value = center_value * step_percent / 100
|
step_value = center_value * step_percent / 100
|
||||||
|
|
||||||
high_level, low_level = calculate_factor_levels(
|
high_level, low_level = calculate_factor_levels(
|
||||||
center_value, step_value, "абс"
|
center_value, step_value, "ед."
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -52,12 +52,10 @@ 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}")
|
||||||
|
|
||||||
# Конвертируем в базовую единицу (мкл для объёма, мг для массы)
|
# Конвертируем в базовую единицу (мкл для объёма, мг для массы)
|
||||||
value_in_base = value * units_map[from_unit]
|
value_in_base = value * units_map[from_unit]
|
||||||
|
|
||||||
# Если нужна конвертация в другую единицу
|
# Если нужна конвертация в другую единицу
|
||||||
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 +156,12 @@ 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}")
|
||||||
# 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 +170,16 @@ 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)
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
# 5. Для объёмных реагентов учитываем в расчёте растворителя
|
# 5. Для объёмных реагентов учитываем в расчёте растворителя
|
||||||
if is_volume:
|
if is_volume:
|
||||||
reagent_volume_base = convert_units(diluted_amount, unit)
|
reagent_volume_base = convert_units(diluted_amount, unit)
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ from dataclasses import dataclass, asdict
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
VERSION="alpha_0.3"
|
||||||
|
"""
|
||||||
|
Условно
|
||||||
|
0.1 - Разработка калькулятора сред
|
||||||
|
0.2 - Разработка факторов эксперимента
|
||||||
|
0.3 - разработка матрицы планирования
|
||||||
|
"""
|
||||||
@dataclass
|
@dataclass
|
||||||
class ReagentData:
|
class ReagentData:
|
||||||
"""Данные реагента"""
|
"""Данные реагента"""
|
||||||
@@ -111,7 +117,7 @@ class ProjectData:
|
|||||||
project_name: str
|
project_name: str
|
||||||
created_at: str
|
created_at: str
|
||||||
modified_at: str
|
modified_at: str
|
||||||
version: str = "1.0"
|
version: str = VERSION
|
||||||
|
|
||||||
# Данные калькулятора сред
|
# Данные калькулятора сред
|
||||||
medium_total_volume: float = 1000.0
|
medium_total_volume: float = 1000.0
|
||||||
@@ -120,6 +126,9 @@ class ProjectData:
|
|||||||
medium_reagents: List[ReagentData] = None
|
medium_reagents: List[ReagentData] = None
|
||||||
|
|
||||||
# Данные эксперимента
|
# Данные эксперимента
|
||||||
|
experiment_total_volume: float = 1000.0
|
||||||
|
experiment_volume_unit: str = "мл"
|
||||||
|
experiment_solvent: str = "Вода"
|
||||||
experiment_factors: List[FactorData] = None
|
experiment_factors: List[FactorData] = None
|
||||||
experiment_responses: List[ResponseData] = None
|
experiment_responses: List[ResponseData] = None
|
||||||
experiment_center_points: int = 3
|
experiment_center_points: int = 3
|
||||||
@@ -150,6 +159,9 @@ class ProjectData:
|
|||||||
'reagents': [r.to_dict() for r in self.medium_reagents]
|
'reagents': [r.to_dict() for r in self.medium_reagents]
|
||||||
},
|
},
|
||||||
'experiment': {
|
'experiment': {
|
||||||
|
'total_volume': self.medium_total_volume,
|
||||||
|
'volume_unit': self.medium_volume_unit,
|
||||||
|
'solvent': self.medium_solvent,
|
||||||
'factors': [f.to_dict() for f in self.experiment_factors],
|
'factors': [f.to_dict() for f in self.experiment_factors],
|
||||||
'responses': [r.to_dict() for r in self.experiment_responses],
|
'responses': [r.to_dict() for r in self.experiment_responses],
|
||||||
'center_points': self.experiment_center_points,
|
'center_points': self.experiment_center_points,
|
||||||
@@ -175,6 +187,9 @@ class ProjectData:
|
|||||||
medium_volume_unit=medium.get('volume_unit', 'мл'),
|
medium_volume_unit=medium.get('volume_unit', 'мл'),
|
||||||
medium_solvent=medium.get('solvent', 'Вода'),
|
medium_solvent=medium.get('solvent', 'Вода'),
|
||||||
medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])],
|
medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])],
|
||||||
|
experiment_total_volume=experiment.get('total_volume', 1000.0),
|
||||||
|
experiment_volume_unit=experiment.get('volume_unit', 'мл'),
|
||||||
|
experiment_solvent=experiment.get('solvent', 'Вода'),
|
||||||
experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])],
|
experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])],
|
||||||
experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])],
|
experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])],
|
||||||
experiment_center_points=experiment.get('center_points', 3),
|
experiment_center_points=experiment.get('center_points', 3),
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
"""
|
"""
|
||||||
Единый графический интерфейс для калькулятора сред и DoE
|
Единый графический интерфейс для калькулятора сред и DoE
|
||||||
"""
|
"""
|
||||||
|
from theme import Colors, Fonts, Spacing, ButtonStyles, get_full_stylesheet, apply_theme, TitleStyles
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QTabWidget, QGroupBox, QLabel, QPushButton, QTableWidget,
|
QTabWidget, QGroupBox, QLabel, QPushButton, QTableWidget,
|
||||||
@@ -28,9 +27,10 @@ from calculations.doe import (
|
|||||||
generate_factorial_design,
|
generate_factorial_design,
|
||||||
analyze_experiment,
|
analyze_experiment,
|
||||||
calculate_factor_levels,
|
calculate_factor_levels,
|
||||||
|
get_active_factors,
|
||||||
|
get_inactive_factors,
|
||||||
FACTOR_TYPES
|
FACTOR_TYPES
|
||||||
)
|
)
|
||||||
|
|
||||||
# Импорт моделей для JSON
|
# Импорт моделей для JSON
|
||||||
try:
|
try:
|
||||||
from calculations.models.project_data import (
|
from calculations.models.project_data import (
|
||||||
@@ -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):
|
||||||
"""Добавляет панель инструментов с кнопками сохранения/загрузки"""
|
"""Добавляет панель инструментов с кнопками сохранения/загрузки"""
|
||||||
@@ -99,12 +85,8 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Заголовок
|
# Заголовок
|
||||||
title = QLabel("Цифровой помощник биохимика")
|
title = QLabel("Цифровой помощник биохимика")
|
||||||
title_font = QFont()
|
title.setObjectName("mainTitle") # Стиль подтянется из theme
|
||||||
title_font.setPointSize(18)
|
|
||||||
title_font.setBold(True)
|
|
||||||
title.setFont(title_font)
|
|
||||||
title.setAlignment(Qt.AlignCenter)
|
title.setAlignment(Qt.AlignCenter)
|
||||||
title.setStyleSheet("color: #2c3e50; padding: 10px;")
|
|
||||||
layout.addWidget(title)
|
layout.addWidget(title)
|
||||||
|
|
||||||
# Вкладки
|
# Вкладки
|
||||||
@@ -203,19 +185,22 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
def _add_reagent_row(self):
|
def _add_reagent_row(self, name = "", percentage = "0", unit = "мл", dilution = "1", amount = ""):
|
||||||
row = self.reagents_table.rowCount()
|
row = self.reagents_table.rowCount()
|
||||||
self.reagents_table.insertRow(row)
|
self.reagents_table.insertRow(row)
|
||||||
self.reagents_table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row+1}"))
|
if name == "":
|
||||||
self.reagents_table.setItem(row, 1, QTableWidgetItem("0"))
|
self.reagents_table.setItem(row, 0, QTableWidgetItem(f"Реагент_{row+1}"))
|
||||||
|
else:
|
||||||
|
self.reagents_table.setItem(row, 0, QTableWidgetItem(name))
|
||||||
|
self.reagents_table.setItem(row, 1, QTableWidgetItem(percentage))
|
||||||
|
|
||||||
unit_combo = QComboBox()
|
unit_combo = QComboBox()
|
||||||
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
|
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л"])
|
||||||
unit_combo.setCurrentText("мл")
|
unit_combo.setCurrentText(unit)
|
||||||
self.reagents_table.setCellWidget(row, 2, unit_combo)
|
self.reagents_table.setCellWidget(row, 2, unit_combo)
|
||||||
|
|
||||||
self.reagents_table.setItem(row, 3, QTableWidgetItem("1"))
|
self.reagents_table.setItem(row, 3, QTableWidgetItem(dilution))
|
||||||
self.reagents_table.setItem(row, 4, QTableWidgetItem(""))
|
self.reagents_table.setItem(row, 4, QTableWidgetItem(amount))
|
||||||
|
|
||||||
def _remove_reagent_row(self):
|
def _remove_reagent_row(self):
|
||||||
for row in sorted(set(i.row() for i in self.reagents_table.selectedItems()), reverse=True):
|
for row in sorted(set(i.row() for i in self.reagents_table.selectedItems()), reverse=True):
|
||||||
@@ -285,17 +270,15 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
self.info_label.setText(solvent_text)
|
self.info_label.setText(solvent_text)
|
||||||
self.info_label.setStyleSheet("background-color: #d5f5e3; padding: 8px; border-radius: 5px;")
|
self.info_label.setStyleSheet("background-color: #d5f5e3; padding: 8px; border-radius: 5px;")
|
||||||
|
|
||||||
# Сохраняем результаты для передачи в DoE
|
# Сохраняем результаты для передачи в DoE
|
||||||
self.last_medium_result = result
|
self.last_medium_result = result
|
||||||
|
|
||||||
QMessageBox.information(self, "Успех", "Расчёт выполнен успешно!")
|
# QMessageBox.information(self, "Успех", "Расчёт выполнен успешно!")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Ошибка", str(e))
|
QMessageBox.critical(self, "Ошибка", str(e))
|
||||||
|
|
||||||
# ========== ВКЛАДКА 2: ФАКТОРЫ ЭКСПЕРИМЕНТА ==========
|
# ========== ВКЛАДКА 2: ФАКТОРЫ ЭКСПЕРИМЕНТА ==========
|
||||||
|
|
||||||
def _create_factors_tab(self):
|
def _create_factors_tab(self):
|
||||||
tab = QWidget()
|
tab = QWidget()
|
||||||
layout = QVBoxLayout(tab)
|
layout = QVBoxLayout(tab)
|
||||||
@@ -322,7 +305,7 @@ class MainWindow(QMainWindow):
|
|||||||
"Фактор", "%", "Разбавление", "Нулевой уровень", "Шаг",
|
"Фактор", "%", "Разбавление", "Нулевой уровень", "Шаг",
|
||||||
"Тип шага", "Верхний (+1)", "Нижний (-1)", "Ед. изм."
|
"Тип шага", "Верхний (+1)", "Нижний (-1)", "Ед. изм."
|
||||||
])
|
])
|
||||||
self.factors_table.setAlternatingRowColors(True)
|
#self.factors_table.setAlternatingRowColors(True)
|
||||||
|
|
||||||
factors_layout.addWidget(self.factors_table)
|
factors_layout.addWidget(self.factors_table)
|
||||||
|
|
||||||
@@ -343,19 +326,43 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Настройки эксперимента
|
# Настройки эксперимента
|
||||||
settings_box = QGroupBox("Настройки эксперимента")
|
settings_box = QGroupBox("Настройки эксперимента")
|
||||||
settings_layout = QHBoxLayout()
|
settings_layout = QVBoxLayout()
|
||||||
|
|
||||||
settings_layout.addWidget(QLabel("Центральных точек:"))
|
# Параметры среды (из калькулятора)
|
||||||
|
env_layout = QHBoxLayout()
|
||||||
|
env_layout.addWidget(QLabel("Общий объём:"))
|
||||||
|
self.exp_total_volume = QDoubleSpinBox()
|
||||||
|
self.exp_total_volume.setRange(0.001, 1000000)
|
||||||
|
self.exp_total_volume.setValue(100)
|
||||||
|
self.exp_total_volume.setSuffix(" ")
|
||||||
|
env_layout.addWidget(self.exp_total_volume)
|
||||||
|
|
||||||
|
self.exp_volume_unit = QComboBox()
|
||||||
|
self.exp_volume_unit.addItems(["мл", "л", "мкл", "нл"])
|
||||||
|
self.exp_volume_unit.setCurrentText("мл")
|
||||||
|
env_layout.addWidget(self.exp_volume_unit)
|
||||||
|
|
||||||
|
env_layout.addSpacing(20)
|
||||||
|
env_layout.addWidget(QLabel("Растворитель:"))
|
||||||
|
self.exp_solvent = QLineEdit("Вода")
|
||||||
|
env_layout.addWidget(self.exp_solvent)
|
||||||
|
env_layout.addStretch()
|
||||||
|
settings_layout.addLayout(env_layout)
|
||||||
|
|
||||||
|
# Настройки эксперимента
|
||||||
|
exp_layout = QHBoxLayout()
|
||||||
|
exp_layout.addWidget(QLabel("Центральных точек:"))
|
||||||
self.center_points_spin = QSpinBox()
|
self.center_points_spin = QSpinBox()
|
||||||
self.center_points_spin.setRange(0, 10)
|
self.center_points_spin.setRange(0, 10)
|
||||||
self.center_points_spin.setValue(3)
|
self.center_points_spin.setValue(3)
|
||||||
settings_layout.addWidget(self.center_points_spin)
|
exp_layout.addWidget(self.center_points_spin)
|
||||||
settings_layout.addSpacing(20)
|
exp_layout.addSpacing(20)
|
||||||
|
|
||||||
self.randomize_check = QCheckBox("Рэндомизировать порядок")
|
self.randomize_check = QCheckBox("Рандомизировать порядок")
|
||||||
self.randomize_check.setChecked(True)
|
self.randomize_check.setChecked(False)
|
||||||
settings_layout.addWidget(self.randomize_check)
|
exp_layout.addWidget(self.randomize_check)
|
||||||
settings_layout.addStretch()
|
exp_layout.addStretch()
|
||||||
|
settings_layout.addLayout(exp_layout)
|
||||||
|
|
||||||
settings_box.setLayout(settings_layout)
|
settings_box.setLayout(settings_layout)
|
||||||
layout.addWidget(settings_box)
|
layout.addWidget(settings_box)
|
||||||
@@ -367,33 +374,47 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# Добавляем начальный фактор
|
# Добавляем начальный фактор
|
||||||
self._add_factor_row()
|
self._add_factor_row()
|
||||||
|
# ПОДКЛЮЧАЕМ СИГНАЛ ИЗМЕНЕНИЯ ЯЧЕЕК
|
||||||
|
self.factors_table.cellChanged.connect(self._on_factor_changed)
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
def _add_factor_row(self):
|
def _add_factor_row(self, name = "", percentage = "0", dilution = "0", center = "0", step ="0", step_type = "%", high = "", low = "", unit= "г"):
|
||||||
row = self.factors_table.rowCount()
|
row = self.factors_table.rowCount()
|
||||||
self.factors_table.insertRow(row)
|
self.factors_table.insertRow(row)
|
||||||
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
|
if name == "":
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
self.factors_table.setItem(row, 0, QTableWidgetItem(f"Фактор_{row+1}"))
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
else:
|
||||||
self.factors_table.setItem(row, 3, QTableWidgetItem("0"))
|
self.factors_table.setItem(row, 0, QTableWidgetItem(name))
|
||||||
self.factors_table.setItem(row, 4, QTableWidgetItem("1"))
|
self.factors_table.setItem(row, 1, QTableWidgetItem(percentage))
|
||||||
|
self.factors_table.setItem(row, 2, QTableWidgetItem(dilution))
|
||||||
|
self.factors_table.setItem(row, 3, QTableWidgetItem(center))
|
||||||
|
self.factors_table.setItem(row, 4, QTableWidgetItem(step))
|
||||||
|
|
||||||
|
|
||||||
step_type_combo = QComboBox()
|
step_type_combo = QComboBox()
|
||||||
step_type_combo.addItems(["абс", "%"])
|
step_type_combo.addItems(["%", "ед."])
|
||||||
|
step_type_combo.setCurrentText(step_type)
|
||||||
|
step_type_combo.currentTextChanged.connect(lambda: self._on_factor_changed(row, 5))
|
||||||
self.factors_table.setCellWidget(row, 5, step_type_combo)
|
self.factors_table.setCellWidget(row, 5, step_type_combo)
|
||||||
|
|
||||||
high_item = QTableWidgetItem("1")
|
high_item = QTableWidgetItem(high)
|
||||||
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
|
high_item.setFlags(high_item.flags() & ~Qt.ItemIsEditable)
|
||||||
high_item.setBackground(QColor(240, 240, 240))
|
high_item.setBackground(QColor(240, 240, 240))
|
||||||
self.factors_table.setItem(row, 6, high_item)
|
self.factors_table.setItem(row, 6, high_item)
|
||||||
|
|
||||||
low_item = QTableWidgetItem("-1")
|
low_item = QTableWidgetItem(low)
|
||||||
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
|
low_item.setFlags(low_item.flags() & ~Qt.ItemIsEditable)
|
||||||
low_item.setBackground(QColor(240, 240, 240))
|
low_item.setBackground(QColor(240, 240, 240))
|
||||||
self.factors_table.setItem(row, 7, low_item)
|
self.factors_table.setItem(row, 7, low_item)
|
||||||
|
unit_measure = QComboBox()
|
||||||
|
unit_measure.addItems(["мкл", "мл", "л", "мг", "г", "кг"])
|
||||||
|
unit_measure.setCurrentText(unit)
|
||||||
|
unit_measure.currentTextChanged.connect(lambda: self._on_factor_changed(row, 8))
|
||||||
|
|
||||||
self.factors_table.setItem(row, 8, QTableWidgetItem(""))
|
self.factors_table.setCellWidget(row, 8, unit_measure)
|
||||||
|
|
||||||
|
|
||||||
|
return row
|
||||||
|
|
||||||
def _remove_factor_row(self):
|
def _remove_factor_row(self):
|
||||||
for row in sorted(set(i.row() for i in self.factors_table.selectedItems()), reverse=True):
|
for row in sorted(set(i.row() for i in self.factors_table.selectedItems()), reverse=True):
|
||||||
@@ -412,38 +433,25 @@ class MainWindow(QMainWindow):
|
|||||||
# Очищаем таблицу факторов
|
# Очищаем таблицу факторов
|
||||||
self.factors_table.setRowCount(0)
|
self.factors_table.setRowCount(0)
|
||||||
|
|
||||||
for reagent in result['reagents']:
|
|
||||||
row = self.factors_table.rowCount()
|
|
||||||
self.factors_table.insertRow(row)
|
|
||||||
|
|
||||||
|
self.exp_total_volume.setValue(result['total_volume'])
|
||||||
|
self.exp_volume_unit.setCurrentText(result['total_unit'])
|
||||||
|
self.exp_solvent.setText(result['solvent_name'])
|
||||||
|
for reagent in result['reagents']:
|
||||||
name = reagent['name']
|
name = reagent['name']
|
||||||
if reagent.get('dilution_factor', 1.0) != 1.0:
|
if reagent.get('dilution_factor', 1.0) != 1.0:
|
||||||
name += f" (разб. ×{reagent['dilution_factor']:.2f})"
|
name += f"(разб. ×{reagent['dilution_factor']:g})"
|
||||||
|
percentage = f"{reagent['percentage']:g}"
|
||||||
self.factors_table.setItem(row, 0, QTableWidgetItem(name))
|
dilution = f"{reagent.get('dilution_factor', 1.0):g}"
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem(f"{reagent['percentage']:.2f}"))
|
center = str(reagent.get('calculated_amount'))
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem(f"{reagent.get('dilution_factor', 1.0):.3f}"))
|
step = ""
|
||||||
|
step_type = "%"
|
||||||
# Нулевой уровень - исходное количество (неразбавленное)
|
high = ""
|
||||||
center = reagent.get('undiluted_amount', reagent['calculated_amount'])
|
low = ""
|
||||||
self.factors_table.setItem(row, 3, QTableWidgetItem(self._format_number(center)))
|
unit = reagent["unit"]
|
||||||
|
self._add_factor_row(name, percentage, dilution, center, step, step_type, high, low, unit)
|
||||||
# Шаг - 10% от нулевого уровня
|
# QMessageBox.information(self, "Успех",
|
||||||
step = center * 0.1
|
# f"Импортировано {len(result['reagents'])} факторов из калькулятора сред")
|
||||||
self.factors_table.setItem(row, 4, QTableWidgetItem(self._format_number(step)))
|
|
||||||
|
|
||||||
# Тип шага - абсолютный
|
|
||||||
step_combo = self.factors_table.cellWidget(row, 5)
|
|
||||||
if step_combo:
|
|
||||||
step_combo.setCurrentText("абс")
|
|
||||||
|
|
||||||
# Верхний и нижний уровни
|
|
||||||
self.factors_table.setItem(row, 6, QTableWidgetItem(self._format_number(center + step)))
|
|
||||||
self.factors_table.setItem(row, 7, QTableWidgetItem(self._format_number(center - step)))
|
|
||||||
self.factors_table.setItem(row, 8, QTableWidgetItem(result['total_unit']))
|
|
||||||
|
|
||||||
QMessageBox.information(self, "Успех",
|
|
||||||
f"Импортировано {len(result['reagents'])} факторов из калькулятора сред")
|
|
||||||
|
|
||||||
def _get_factors_from_table(self) -> List[Dict]:
|
def _get_factors_from_table(self) -> List[Dict]:
|
||||||
"""Собирает данные факторов из таблицы"""
|
"""Собирает данные факторов из таблицы"""
|
||||||
@@ -455,7 +463,7 @@ class MainWindow(QMainWindow):
|
|||||||
step_item = self.factors_table.item(row, 4)
|
step_item = self.factors_table.item(row, 4)
|
||||||
high_item = self.factors_table.item(row, 6)
|
high_item = self.factors_table.item(row, 6)
|
||||||
low_item = self.factors_table.item(row, 7)
|
low_item = self.factors_table.item(row, 7)
|
||||||
unit_item = self.factors_table.item(row, 8)
|
unit_item = self.factors_table.cellWidget(row, 8)
|
||||||
step_combo = self.factors_table.cellWidget(row, 5)
|
step_combo = self.factors_table.cellWidget(row, 5)
|
||||||
|
|
||||||
if not all([name_item, center_item, high_item, low_item]):
|
if not all([name_item, center_item, high_item, low_item]):
|
||||||
@@ -473,9 +481,9 @@ class MainWindow(QMainWindow):
|
|||||||
'center': float(center_text),
|
'center': float(center_text),
|
||||||
'high': float(high_text),
|
'high': float(high_text),
|
||||||
'low': float(low_text),
|
'low': float(low_text),
|
||||||
'unit': unit_item.text() if unit_item else "",
|
'unit': unit_item.currentText() if unit_item else "г",
|
||||||
'step': float(step_item.text()) if step_item and step_item.text() else 0,
|
'step': float(step_item.text()) if step_item and step_item.text() else 0,
|
||||||
'step_type': step_combo.currentText() if step_combo else "абс"
|
'step_type': step_combo.currentText() if step_combo else "ед."
|
||||||
}
|
}
|
||||||
factors.append(factor)
|
factors.append(factor)
|
||||||
except (ValueError, AttributeError) as e:
|
except (ValueError, AttributeError) as e:
|
||||||
@@ -483,6 +491,32 @@ class MainWindow(QMainWindow):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
return factors
|
return factors
|
||||||
|
def _on_factor_changed(self, row, column):
|
||||||
|
"""При изменении ячейки пересчитываем уровни"""
|
||||||
|
if getattr(self, '_loading_data', False) or column not in [3, 4, 5, 8]:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Получаем данные
|
||||||
|
percentage = float(self.factors_table.item(row, 1).text())
|
||||||
|
dilution = float(self.factors_table.item(row, 2).text())
|
||||||
|
center = float(self.factors_table.item(row, 3).text())
|
||||||
|
step = float(self.factors_table.item(row, 4).text())
|
||||||
|
step_type = self.factors_table.cellWidget(row, 5).currentText()
|
||||||
|
unit = self.factors_table.cellWidget(row, 8).currentText()
|
||||||
|
name = self.factors_table.item(row, 0).text()
|
||||||
|
|
||||||
|
# Рассчитываем
|
||||||
|
factor = FactorData(percentage=percentage, dilution_factor=dilution, name=name, center=center, low=0, high=0, step=step, step_type=step_type, unit=unit)
|
||||||
|
result = calculate_factor_levels(factor, self.total_volume_spin.value(), self.volume_unit_combo.currentText())
|
||||||
|
if result:
|
||||||
|
self.factors_table.blockSignals(True)
|
||||||
|
self.factors_table.item(row, 6).setText(self._format_number(result.high))
|
||||||
|
self.factors_table.item(row, 7).setText(self._format_number(result.low))
|
||||||
|
self.factors_table.blockSignals(False)
|
||||||
|
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
pass
|
||||||
|
|
||||||
# ========== ВКЛАДКА 3: МАТРИЦА ПЛАНИРОВАНИЯ ==========
|
# ========== ВКЛАДКА 3: МАТРИЦА ПЛАНИРОВАНИЯ ==========
|
||||||
|
|
||||||
@@ -517,9 +551,11 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _generate_design(self):
|
def _generate_design(self):
|
||||||
"""Генерирует план эксперимента"""
|
"""Генерирует план эксперимента"""
|
||||||
factors = self._get_factors_from_table()
|
all_factors = self._get_factors_from_table()
|
||||||
|
factors = get_active_factors(all_factors)
|
||||||
|
i_factors = get_inactive_factors(all_factors)
|
||||||
|
|
||||||
if len(factors) == 0:
|
if not factors:
|
||||||
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
|
QMessageBox.warning(self, "Предупреждение", "Добавьте хотя бы один фактор!")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -529,59 +565,15 @@ class MainWindow(QMainWindow):
|
|||||||
center_points=self.center_points_spin.value(),
|
center_points=self.center_points_spin.value(),
|
||||||
randomize=self.randomize_check.isChecked()
|
randomize=self.randomize_check.isChecked()
|
||||||
)
|
)
|
||||||
|
|
||||||
self.generated_design = design
|
self.generated_design = design
|
||||||
|
self._fill_design_matrix(design, factors, i_factors)
|
||||||
n_exp = len(design)
|
|
||||||
n_factors = len(factors)
|
|
||||||
|
|
||||||
self.design_matrix.setRowCount(n_exp)
|
|
||||||
self.design_matrix.setColumnCount(n_factors + 2)
|
|
||||||
headers = ["№"] + [f['name'] for f in factors] + ["Тип"]
|
|
||||||
self.design_matrix.setHorizontalHeaderLabels(headers)
|
|
||||||
|
|
||||||
for exp_idx, exp in enumerate(design):
|
|
||||||
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
|
|
||||||
|
|
||||||
for f_idx in range(n_factors):
|
|
||||||
key = f"Фактор_{f_idx + 1}"
|
|
||||||
if key not in exp:
|
|
||||||
continue
|
|
||||||
value = exp[key]['natural']
|
|
||||||
unit = factors[f_idx]['unit']
|
|
||||||
|
|
||||||
display = self._format_number(value)
|
|
||||||
if unit:
|
|
||||||
display += f" {unit}"
|
|
||||||
|
|
||||||
item = QTableWidgetItem(display)
|
|
||||||
if exp.get('is_center', False):
|
|
||||||
item.setBackground(QColor(255, 255, 200))
|
|
||||||
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
|
|
||||||
|
|
||||||
if exp.get('is_center', False):
|
|
||||||
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
|
|
||||||
type_item.setBackground(QColor(255, 255, 200))
|
|
||||||
else:
|
|
||||||
type_item = QTableWidgetItem("Факторная")
|
|
||||||
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
|
|
||||||
|
|
||||||
self.design_matrix.resizeColumnsToContents()
|
|
||||||
|
|
||||||
n_factorial = 2 ** n_factors
|
|
||||||
n_center = self.center_points_spin.value()
|
|
||||||
self.design_info.setText(
|
|
||||||
f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.export_csv_btn.setEnabled(True)
|
self.export_csv_btn.setEnabled(True)
|
||||||
self._setup_results_table(n_exp)
|
QMessageBox.information(self, "Успех", f"Сгенерирован план для {len(factors)} факторов ({len(design)} опытов)")
|
||||||
|
|
||||||
QMessageBox.information(self, "Успех",
|
|
||||||
f"Сгенерирован план для {n_factors} факторов ({n_exp} опытов)")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}")
|
QMessageBox.critical(self, "Ошибка", f"Ошибка генерации плана: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def _setup_results_table(self, n_experiments: int):
|
def _setup_results_table(self, n_experiments: int):
|
||||||
"""Настраивает таблицу результатов для ввода данных"""
|
"""Настраивает таблицу результатов для ввода данных"""
|
||||||
# Получаем отклики (для простоты используем один отклик)
|
# Получаем отклики (для простоты используем один отклик)
|
||||||
@@ -590,7 +582,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.results_table.setHorizontalHeaderLabels(["№ опыта", "Результат"])
|
self.results_table.setHorizontalHeaderLabels(["№ опыта", "Результат"])
|
||||||
|
|
||||||
for i in range(n_experiments):
|
for i in range(n_experiments):
|
||||||
self.results_table.setItem(i, 0, QTableWidgetItem(str(i + 1)))
|
self.results_table.setItem(i, 0, QTableWidgetItem(str(i)))
|
||||||
|
|
||||||
def _export_design_csv(self):
|
def _export_design_csv(self):
|
||||||
"""Экспортирует матрицу планирования в CSV"""
|
"""Экспортирует матрицу планирования в CSV"""
|
||||||
@@ -759,7 +751,7 @@ class MainWindow(QMainWindow):
|
|||||||
try:
|
try:
|
||||||
project = ProjectData.load_from_file(filename)
|
project = ProjectData.load_from_file(filename)
|
||||||
self._apply_project_data(project)
|
self._apply_project_data(project)
|
||||||
QMessageBox.information(self, "Успех", f"Проект загружен из {filename}")
|
# QMessageBox.information(self, "Успех", f"Проект загружен из {filename}")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}")
|
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -797,7 +789,7 @@ class MainWindow(QMainWindow):
|
|||||||
low_item = self.factors_table.item(row, 7)
|
low_item = self.factors_table.item(row, 7)
|
||||||
high_item = self.factors_table.item(row, 6)
|
high_item = self.factors_table.item(row, 6)
|
||||||
step_item = self.factors_table.item(row, 4)
|
step_item = self.factors_table.item(row, 4)
|
||||||
unit_item = self.factors_table.item(row, 8)
|
unit_item = self.factors_table.cellWidget(row, 8)
|
||||||
step_combo = self.factors_table.cellWidget(row, 5)
|
step_combo = self.factors_table.cellWidget(row, 5)
|
||||||
percent_item = self.factors_table.item(row, 1)
|
percent_item = self.factors_table.item(row, 1)
|
||||||
dilution_item = self.factors_table.item(row, 2)
|
dilution_item = self.factors_table.item(row, 2)
|
||||||
@@ -810,8 +802,8 @@ class MainWindow(QMainWindow):
|
|||||||
low=float(low_item.text()) if low_item and low_item.text() else 0,
|
low=float(low_item.text()) if low_item and low_item.text() else 0,
|
||||||
high=float(high_item.text()) if high_item and high_item.text() else 0,
|
high=float(high_item.text()) if high_item and high_item.text() else 0,
|
||||||
step=float(step_item.text()) if step_item and step_item.text() else 0,
|
step=float(step_item.text()) if step_item and step_item.text() else 0,
|
||||||
step_type=step_combo.currentText() if step_combo else "абс",
|
step_type=step_combo.currentText() if step_combo else "ед.",
|
||||||
unit=unit_item.text() if unit_item else "",
|
unit=unit_item.currentText() if unit_item else "",
|
||||||
percentage=float(percent_item.text()) if percent_item and percent_item.text() else None,
|
percentage=float(percent_item.text()) if percent_item and percent_item.text() else None,
|
||||||
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None
|
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None
|
||||||
)
|
)
|
||||||
@@ -854,123 +846,86 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _apply_project_data(self, project):
|
def _apply_project_data(self, project):
|
||||||
"""Применяет загруженные данные к GUI"""
|
"""Применяет загруженные данные к GUI"""
|
||||||
# Применяем данные калькулятора сред
|
self._loading_data = True # <- ОТКЛЮЧАЕМ ОБРАБОТЧИК
|
||||||
self.total_volume_spin.setValue(project.medium_total_volume)
|
try:
|
||||||
|
|
||||||
index = self.volume_unit_combo.findText(project.medium_volume_unit)
|
# Применяем данные калькулятора сред
|
||||||
if index >= 0:
|
self.total_volume_spin.setValue(project.medium_total_volume)
|
||||||
self.volume_unit_combo.setCurrentIndex(index)
|
|
||||||
|
|
||||||
self.solvent_input.setText(project.medium_solvent)
|
index = self.volume_unit_combo.findText(project.medium_volume_unit)
|
||||||
|
if index >= 0:
|
||||||
|
self.volume_unit_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
# Очищаем и заполняем таблицу реагентов
|
self.solvent_input.setText(project.medium_solvent)
|
||||||
self.reagents_table.setRowCount(0)
|
|
||||||
for reagent in project.medium_reagents:
|
|
||||||
row = self.reagents_table.rowCount()
|
|
||||||
self.reagents_table.insertRow(row)
|
|
||||||
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
|
|
||||||
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
|
|
||||||
|
|
||||||
unit_combo = QComboBox()
|
# Очищаем и заполняем таблицу реагентов
|
||||||
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
|
self.reagents_table.setRowCount(0)
|
||||||
unit_combo.setCurrentText(reagent.unit)
|
for reagent in project.medium_reagents:
|
||||||
self.reagents_table.setCellWidget(row, 2, unit_combo)
|
row = self.reagents_table.rowCount()
|
||||||
|
self.reagents_table.insertRow(row)
|
||||||
|
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
|
||||||
|
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
|
||||||
|
|
||||||
self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor)))
|
unit_combo = QComboBox()
|
||||||
self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor)))
|
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
|
||||||
self.reagents_table.setItem(row, 5, QTableWidgetItem(""))
|
unit_combo.setCurrentText(reagent.unit)
|
||||||
|
self.reagents_table.setCellWidget(row, 2, unit_combo)
|
||||||
|
|
||||||
# Применяем данные факторов эксперимента
|
self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor)))
|
||||||
self.factors_table.setRowCount(0)
|
self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor)))
|
||||||
for factor in project.experiment_factors:
|
self.reagents_table.setItem(row, 5, QTableWidgetItem(""))
|
||||||
row = self.factors_table.rowCount()
|
|
||||||
self.factors_table.insertRow(row)
|
|
||||||
self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name))
|
|
||||||
|
|
||||||
if factor.percentage is not None:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
|
||||||
|
|
||||||
if factor.dilution_factor is not None:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center)))
|
# Применяем данные факторов эксперимента
|
||||||
self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step)))
|
self.exp_total_volume.setValue(project.experiment_total_volume)
|
||||||
|
self.exp_volume_unit.setCurrentText(project.experiment_volume_unit)
|
||||||
|
self.exp_solvent.setText(project.experiment_solvent)
|
||||||
|
self.factors_table.setRowCount(0)
|
||||||
|
for factor in project.experiment_factors:
|
||||||
|
if factor.percentage is not None:
|
||||||
|
percentage = str(factor.percentage)
|
||||||
|
if factor.dilution_factor is not None:
|
||||||
|
dilution = str(factor.dilution_factor)
|
||||||
|
self._add_factor_row(factor.name, percentage, dilution, str(factor.center),
|
||||||
|
str(factor.step), factor.step_type, str(factor.high),
|
||||||
|
str(factor.low), factor.unit)
|
||||||
|
# Применяем настройки эксперимента
|
||||||
|
self.center_points_spin.setValue(project.experiment_center_points)
|
||||||
|
self.randomize_check.setChecked(project.experiment_randomize)
|
||||||
|
|
||||||
step_combo = QComboBox()
|
# Если есть результаты, загружаем их
|
||||||
step_combo.addItems(["абс", "%"])
|
if project.experiment_results and project.experiment_results.design:
|
||||||
step_combo.setCurrentText(factor.step_type)
|
self.generated_design = project.experiment_results.design
|
||||||
self.factors_table.setCellWidget(row, 5, step_combo)
|
# Обновляем отображение матрицы
|
||||||
|
self._refresh_design_matrix()
|
||||||
|
|
||||||
self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high)))
|
# Загружаем результаты в таблицу
|
||||||
self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low)))
|
if project.experiment_results.results:
|
||||||
self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit))
|
for i, row_results in enumerate(project.experiment_results.results):
|
||||||
|
if i < self.results_table.rowCount() and row_results:
|
||||||
|
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
|
||||||
|
|
||||||
# Применяем настройки эксперимента
|
# Переключаемся на вкладку с экспериментом
|
||||||
self.center_points_spin.setValue(project.experiment_center_points)
|
if self.tab_widget:
|
||||||
self.randomize_check.setChecked(project.experiment_randomize)
|
self.tab_widget.setCurrentIndex(0)
|
||||||
|
finally:
|
||||||
# Если есть результаты, загружаем их
|
self._loading_data = False # <- ВКЛЮЧАЕМ ОБРАТНО
|
||||||
if project.experiment_results and project.experiment_results.design:
|
|
||||||
self.generated_design = project.experiment_results.design
|
|
||||||
# Обновляем отображение матрицы
|
|
||||||
self._refresh_design_matrix()
|
|
||||||
|
|
||||||
# Загружаем результаты в таблицу
|
|
||||||
if project.experiment_results.results:
|
|
||||||
for i, row_results in enumerate(project.experiment_results.results):
|
|
||||||
if i < self.results_table.rowCount() and row_results:
|
|
||||||
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
|
|
||||||
|
|
||||||
# Переключаемся на вкладку с факторами
|
|
||||||
if self.tab_widget:
|
|
||||||
self.tab_widget.setCurrentIndex(1)
|
|
||||||
|
|
||||||
def _refresh_design_matrix(self):
|
def _refresh_design_matrix(self):
|
||||||
"""Обновляет отображение матрицы планирования"""
|
"""Обновляет отображение матрицы планирования"""
|
||||||
if not self.generated_design:
|
if not self.generated_design:
|
||||||
return
|
return
|
||||||
|
|
||||||
factors = self._get_factors_from_table()
|
all_factors = self._get_factors_from_table()
|
||||||
|
factors = get_active_factors(all_factors)
|
||||||
|
i_factors = get_inactive_factors(all_factors)
|
||||||
|
|
||||||
|
self._fill_design_matrix(self.generated_design, factors, i_factors)
|
||||||
|
|
||||||
n_exp = len(self.generated_design)
|
n_exp = len(self.generated_design)
|
||||||
n_factors = len(factors)
|
n_factors = len(factors)
|
||||||
|
|
||||||
self.design_matrix.setRowCount(n_exp)
|
|
||||||
self.design_matrix.setColumnCount(n_factors + 2)
|
|
||||||
headers = ["№"] + [f['name'] for f in factors] + ["Тип"]
|
|
||||||
self.design_matrix.setHorizontalHeaderLabels(headers)
|
|
||||||
|
|
||||||
for exp_idx, exp in enumerate(self.generated_design):
|
|
||||||
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
|
|
||||||
|
|
||||||
for f_idx in range(n_factors):
|
|
||||||
key = f"Фактор_{f_idx + 1}"
|
|
||||||
if key not in exp:
|
|
||||||
continue
|
|
||||||
value = exp[key]['natural']
|
|
||||||
unit = factors[f_idx]['unit']
|
|
||||||
|
|
||||||
display = self._format_number(value)
|
|
||||||
if unit:
|
|
||||||
display += f" {unit}"
|
|
||||||
|
|
||||||
item = QTableWidgetItem(display)
|
|
||||||
if exp.get('is_center', False):
|
|
||||||
item.setBackground(QColor(255, 255, 200))
|
|
||||||
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
|
|
||||||
|
|
||||||
if exp.get('is_center', False):
|
|
||||||
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
|
|
||||||
type_item.setBackground(QColor(255, 255, 200))
|
|
||||||
else:
|
|
||||||
type_item = QTableWidgetItem("Факторная")
|
|
||||||
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
|
|
||||||
|
|
||||||
self.design_matrix.resizeColumnsToContents()
|
|
||||||
|
|
||||||
if hasattr(self, 'export_csv_btn'):
|
if hasattr(self, 'export_csv_btn'):
|
||||||
self.export_csv_btn.setEnabled(True)
|
self.export_csv_btn.setEnabled(True)
|
||||||
|
|
||||||
@@ -983,6 +938,74 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
|
# ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
|
||||||
|
|
||||||
|
def _fill_design_matrix(self, design, factors, i_factors):
|
||||||
|
"""Заполняет матрицу планирования (общая логика)"""
|
||||||
|
n_exp = len(design)
|
||||||
|
n_factors = len(factors)
|
||||||
|
n_i_factors = len(i_factors)
|
||||||
|
solvent_name = self.exp_solvent.text()
|
||||||
|
total_volume = self.exp_total_volume.value()
|
||||||
|
solvent_unit = self.exp_volume_unit.currentText()
|
||||||
|
|
||||||
|
self.design_matrix.setRowCount(n_exp)
|
||||||
|
self.design_matrix.setColumnCount(n_factors + n_i_factors + 3)
|
||||||
|
headers = [f['name'] for f in factors] + [f['name'] for f in i_factors] + [solvent_name] + ["Тип"] + ["Отклик"]
|
||||||
|
self.design_matrix.setHorizontalHeaderLabels(headers)
|
||||||
|
|
||||||
|
for exp_idx, exp in enumerate(design):
|
||||||
|
remaining = convert_units(total_volume, solvent_unit)
|
||||||
|
|
||||||
|
# Активные факторы
|
||||||
|
for f_idx in range(n_factors):
|
||||||
|
value = exp[f"Фактор_{f_idx+1}"]['natural']
|
||||||
|
unit = factors[f_idx]['unit']
|
||||||
|
remaining -= convert_units(value, unit)
|
||||||
|
item = self._create_item(value, unit, exp.get('is_center', False))
|
||||||
|
if value == factors[f_idx]['high']:
|
||||||
|
item.setBackground(QColor(200, 250, 200))
|
||||||
|
elif value ==factors[f_idx]['low']:
|
||||||
|
item.setBackground(QColor(250, 200, 200))
|
||||||
|
else:
|
||||||
|
item.setBackground(QColor(230, 230, 230))
|
||||||
|
self.design_matrix.setItem(exp_idx, f_idx, item)
|
||||||
|
|
||||||
|
# Неактивные факторы
|
||||||
|
for f_idx in range(n_i_factors):
|
||||||
|
value = i_factors[f_idx]['center']
|
||||||
|
unit = i_factors[f_idx]['unit']
|
||||||
|
remaining -= convert_units(value, unit)
|
||||||
|
item = self._create_item(value, unit, True)
|
||||||
|
item.setBackground(QColor(230, 230, 230))
|
||||||
|
self.design_matrix.setItem(exp_idx, n_factors + f_idx, item)
|
||||||
|
|
||||||
|
# Растворитель
|
||||||
|
remaining = convert_units(remaining, 'мкл', solvent_unit)
|
||||||
|
item = self._create_item(remaining, solvent_unit, False)
|
||||||
|
item.setBackground(QColor(200, 200, 255))
|
||||||
|
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors, item)
|
||||||
|
|
||||||
|
# Тип опыта
|
||||||
|
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}" if exp.get('is_center') else "Факторная")
|
||||||
|
if exp.get('is_center'):
|
||||||
|
type_item.setBackground(QColor(200, 200, 200))
|
||||||
|
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors + 1, type_item)
|
||||||
|
|
||||||
|
# Отклик
|
||||||
|
self.design_matrix.setItem(exp_idx, n_factors + n_i_factors + 2, QTableWidgetItem(""))
|
||||||
|
|
||||||
|
self.design_matrix.resizeColumnsToContents()
|
||||||
|
|
||||||
|
def _create_item(self, value, unit, is_center):
|
||||||
|
"""Создаёт QTableWidgetItem с форматированием"""
|
||||||
|
display = self._format_number(value)
|
||||||
|
if unit:
|
||||||
|
display += f" {unit}"
|
||||||
|
item = QTableWidgetItem(display)
|
||||||
|
if is_center:
|
||||||
|
item.setBackground(QColor(255, 255, 200))
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
def _format_number(self, value: float) -> str:
|
def _format_number(self, value: float) -> str:
|
||||||
"""Форматирует число для отображения"""
|
"""Форматирует число для отображения"""
|
||||||
if value == int(value):
|
if value == int(value):
|
||||||
|
|||||||
@@ -1,14 +1,36 @@
|
|||||||
|
# main.py
|
||||||
"""
|
"""
|
||||||
Биохимический помощник - точка входа в приложение
|
Биохимический помощник - точка входа в приложение
|
||||||
|
TODO:
|
||||||
|
- Добавить информацию о количестве раствора в DOE ОК
|
||||||
|
- Не считать фактор, если его шаг 0 ОК
|
||||||
|
- Добавить столбец в матрицу планирования с информацией о количестве добавленного растворителя, учитывая все реагенты
|
||||||
|
- Начать делать анализ
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -1,904 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Скрипт для добавления функционала сохранения/загрузки данных в JSON
|
|
||||||
# Запускать из корневой директории проекта
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "📁 Добавление функционала сохранения/загрузки данных в JSON..."
|
|
||||||
echo "================================================================"
|
|
||||||
|
|
||||||
# Создаём директорию для моделей, если её нет
|
|
||||||
mkdir -p calculations/models
|
|
||||||
|
|
||||||
# 1. СОЗДАЁМ МОДЕЛЬ ДАННЫХ ДЛЯ СОХРАНЕНИЯ
|
|
||||||
cat > calculations/models/project_data.py << 'EOF'
|
|
||||||
"""
|
|
||||||
Модель данных проекта для сохранения/загрузки в JSON
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import List, Dict, Optional
|
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
from datetime import datetime
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReagentData:
|
|
||||||
"""Данные реагента"""
|
|
||||||
name: str
|
|
||||||
percentage: float
|
|
||||||
unit: str
|
|
||||||
conversion_factor: float
|
|
||||||
dilution_factor: float
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'ReagentData':
|
|
||||||
return cls(
|
|
||||||
name=data['name'],
|
|
||||||
percentage=data['percentage'],
|
|
||||||
unit=data['unit'],
|
|
||||||
conversion_factor=data.get('conversion_factor', 1.0),
|
|
||||||
dilution_factor=data.get('dilution_factor', 1.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FactorData:
|
|
||||||
"""Данные фактора эксперимента"""
|
|
||||||
name: str
|
|
||||||
center: float
|
|
||||||
low: float
|
|
||||||
high: float
|
|
||||||
step: float
|
|
||||||
step_type: str
|
|
||||||
unit: str
|
|
||||||
percentage: Optional[float] = None
|
|
||||||
dilution_factor: Optional[float] = None
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
d = asdict(self)
|
|
||||||
# Удаляем None значения
|
|
||||||
return {k: v for k, v in d.items() if v is not None}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'FactorData':
|
|
||||||
return cls(
|
|
||||||
name=data['name'],
|
|
||||||
center=data['center'],
|
|
||||||
low=data['low'],
|
|
||||||
high=data['high'],
|
|
||||||
step=data.get('step', 0),
|
|
||||||
step_type=data.get('step_type', 'абс'),
|
|
||||||
unit=data.get('unit', ''),
|
|
||||||
percentage=data.get('percentage'),
|
|
||||||
dilution_factor=data.get('dilution_factor')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ResponseData:
|
|
||||||
"""Данные отклика эксперимента"""
|
|
||||||
name: str
|
|
||||||
unit: str
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
return asdict(self)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'ResponseData':
|
|
||||||
return cls(
|
|
||||||
name=data['name'],
|
|
||||||
unit=data.get('unit', '')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ExperimentResultsData:
|
|
||||||
"""Результаты эксперимента"""
|
|
||||||
design: List[Dict] # План эксперимента
|
|
||||||
results: List[List[float]] # Результаты измерений
|
|
||||||
responses: List[ResponseData] # Отклики
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
return {
|
|
||||||
'design': self.design,
|
|
||||||
'results': self.results,
|
|
||||||
'responses': [r.to_dict() for r in self.responses]
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'ExperimentResultsData':
|
|
||||||
return cls(
|
|
||||||
design=data['design'],
|
|
||||||
results=data['results'],
|
|
||||||
responses=[ResponseData.from_dict(r) for r in data['responses']]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ProjectData:
|
|
||||||
"""Полные данные проекта"""
|
|
||||||
# Информация о проекте
|
|
||||||
project_name: str
|
|
||||||
created_at: str
|
|
||||||
modified_at: str
|
|
||||||
version: str = "1.0"
|
|
||||||
|
|
||||||
# Данные калькулятора сред
|
|
||||||
medium_total_volume: float = 1000.0
|
|
||||||
medium_volume_unit: str = "мл"
|
|
||||||
medium_solvent: str = "Вода"
|
|
||||||
medium_reagents: List[ReagentData] = None
|
|
||||||
|
|
||||||
# Данные эксперимента
|
|
||||||
experiment_factors: List[FactorData] = None
|
|
||||||
experiment_responses: List[ResponseData] = None
|
|
||||||
experiment_center_points: int = 3
|
|
||||||
experiment_randomize: bool = True
|
|
||||||
experiment_results: Optional[ExperimentResultsData] = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.medium_reagents is None:
|
|
||||||
self.medium_reagents = []
|
|
||||||
if self.experiment_factors is None:
|
|
||||||
self.experiment_factors = []
|
|
||||||
if self.experiment_responses is None:
|
|
||||||
self.experiment_responses = []
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict:
|
|
||||||
"""Конвертирует в словарь для JSON"""
|
|
||||||
return {
|
|
||||||
'project_info': {
|
|
||||||
'name': self.project_name,
|
|
||||||
'created_at': self.created_at,
|
|
||||||
'modified_at': self.modified_at,
|
|
||||||
'version': self.version
|
|
||||||
},
|
|
||||||
'medium_calculator': {
|
|
||||||
'total_volume': self.medium_total_volume,
|
|
||||||
'volume_unit': self.medium_volume_unit,
|
|
||||||
'solvent': self.medium_solvent,
|
|
||||||
'reagents': [r.to_dict() for r in self.medium_reagents]
|
|
||||||
},
|
|
||||||
'experiment': {
|
|
||||||
'factors': [f.to_dict() for f in self.experiment_factors],
|
|
||||||
'responses': [r.to_dict() for r in self.experiment_responses],
|
|
||||||
'center_points': self.experiment_center_points,
|
|
||||||
'randomize': self.experiment_randomize,
|
|
||||||
'results': self.experiment_results.to_dict() if self.experiment_results else None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: Dict) -> 'ProjectData':
|
|
||||||
"""Создаёт объект из словаря"""
|
|
||||||
project_info = data.get('project_info', {})
|
|
||||||
medium = data.get('medium_calculator', {})
|
|
||||||
experiment = data.get('experiment', {})
|
|
||||||
|
|
||||||
# Создаём объект
|
|
||||||
obj = cls(
|
|
||||||
project_name=project_info.get('name', 'Новый проект'),
|
|
||||||
created_at=project_info.get('created_at', datetime.now().isoformat()),
|
|
||||||
modified_at=project_info.get('modified_at', datetime.now().isoformat()),
|
|
||||||
version=project_info.get('version', '1.0'),
|
|
||||||
medium_total_volume=medium.get('total_volume', 1000.0),
|
|
||||||
medium_volume_unit=medium.get('volume_unit', 'мл'),
|
|
||||||
medium_solvent=medium.get('solvent', 'Вода'),
|
|
||||||
medium_reagents=[ReagentData.from_dict(r) for r in medium.get('reagents', [])],
|
|
||||||
experiment_factors=[FactorData.from_dict(f) for f in experiment.get('factors', [])],
|
|
||||||
experiment_responses=[ResponseData.from_dict(r) for r in experiment.get('responses', [])],
|
|
||||||
experiment_center_points=experiment.get('center_points', 3),
|
|
||||||
experiment_randomize=experiment.get('randomize', True),
|
|
||||||
experiment_results=ExperimentResultsData.from_dict(experiment['results'])
|
|
||||||
if experiment.get('results') else None
|
|
||||||
)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def save_to_file(self, filename: str):
|
|
||||||
"""Сохраняет проект в JSON файл"""
|
|
||||||
self.modified_at = datetime.now().isoformat()
|
|
||||||
with open(filename, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load_from_file(cls, filename: str) -> 'ProjectData':
|
|
||||||
"""Загружает проект из JSON файла"""
|
|
||||||
with open(filename, 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
return cls.from_dict(data)
|
|
||||||
|
|
||||||
|
|
||||||
def create_new_project(name: str = "Новый проект") -> ProjectData:
|
|
||||||
"""Создаёт новый проект с текущей датой"""
|
|
||||||
now = datetime.now().isoformat()
|
|
||||||
return ProjectData(
|
|
||||||
project_name=name,
|
|
||||||
created_at=now,
|
|
||||||
modified_at=now
|
|
||||||
)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Создан calculations/models/project_data.py"
|
|
||||||
|
|
||||||
# 2. ДОБАВЛЯЕМ ФУНКЦИИ В GUI ДЛЯ РАБОТЫ С JSON
|
|
||||||
# Создаём патч для gui.py
|
|
||||||
cat > gui_patch.py << 'EOF'
|
|
||||||
# ДОБАВИТЬ В gui.py ПОСЛЕ ИМПОРТОВ:
|
|
||||||
|
|
||||||
from calculations.models.project_data import (
|
|
||||||
ProjectData, ReagentData, FactorData,
|
|
||||||
ResponseData, ExperimentResultsData, create_new_project
|
|
||||||
)
|
|
||||||
|
|
||||||
# ДОБАВИТЬ В КЛАСС MainWindow НОВЫЕ МЕТОДЫ:
|
|
||||||
|
|
||||||
def save_project_to_json(self):
|
|
||||||
"""Сохраняет весь проект в JSON файл"""
|
|
||||||
filename, _ = QFileDialog.getSaveFileName(
|
|
||||||
self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)"
|
|
||||||
)
|
|
||||||
if not filename:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not filename.endswith('.json'):
|
|
||||||
filename += '.json'
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Собираем данные из GUI
|
|
||||||
project = self._collect_current_data()
|
|
||||||
project.save_to_file(filename)
|
|
||||||
QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}")
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}")
|
|
||||||
|
|
||||||
def load_project_from_json(self):
|
|
||||||
"""Загружает проект из JSON файла"""
|
|
||||||
filename, _ = QFileDialog.getOpenFileName(
|
|
||||||
self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)"
|
|
||||||
)
|
|
||||||
if not filename:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
project = ProjectData.load_from_file(filename)
|
|
||||||
self._apply_project_data(project)
|
|
||||||
QMessageBox.information(self, "Успех", f"Проект загружен из {filename}")
|
|
||||||
except FileNotFoundError:
|
|
||||||
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}")
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}")
|
|
||||||
|
|
||||||
def _collect_current_data(self) -> ProjectData:
|
|
||||||
"""Собирает текущие данные из всех виджетов"""
|
|
||||||
# Данные калькулятора сред
|
|
||||||
reagents = []
|
|
||||||
for row in range(self.reagents_table.rowCount()):
|
|
||||||
name_item = self.reagents_table.item(row, 0)
|
|
||||||
percent_item = self.reagents_table.item(row, 1)
|
|
||||||
unit_widget = self.reagents_table.cellWidget(row, 2)
|
|
||||||
coeff_item = self.reagents_table.item(row, 3)
|
|
||||||
dilution_item = self.reagents_table.item(row, 4)
|
|
||||||
|
|
||||||
if name_item and percent_item:
|
|
||||||
try:
|
|
||||||
reagent = ReagentData(
|
|
||||||
name=name_item.text(),
|
|
||||||
percentage=float(percent_item.text()) if percent_item.text() else 0,
|
|
||||||
unit=unit_widget.currentText() if unit_widget else "мг",
|
|
||||||
conversion_factor=float(coeff_item.text()) if coeff_item and coeff_item.text() else 1.0,
|
|
||||||
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0
|
|
||||||
)
|
|
||||||
reagents.append(reagent)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Данные факторов эксперимента
|
|
||||||
factors = []
|
|
||||||
for row in range(self.factors_table.rowCount()):
|
|
||||||
name_item = self.factors_table.item(row, 0)
|
|
||||||
center_item = self.factors_table.item(row, 3)
|
|
||||||
low_item = self.factors_table.item(row, 7)
|
|
||||||
high_item = self.factors_table.item(row, 6)
|
|
||||||
step_item = self.factors_table.item(row, 4)
|
|
||||||
unit_item = self.factors_table.item(row, 8)
|
|
||||||
step_combo = self.factors_table.cellWidget(row, 5)
|
|
||||||
percent_item = self.factors_table.item(row, 1)
|
|
||||||
dilution_item = self.factors_table.item(row, 2)
|
|
||||||
|
|
||||||
if name_item and center_item:
|
|
||||||
try:
|
|
||||||
factor = FactorData(
|
|
||||||
name=name_item.text(),
|
|
||||||
center=float(center_item.text()) if center_item.text() else 0,
|
|
||||||
low=float(low_item.text()) if low_item and low_item.text() else 0,
|
|
||||||
high=float(high_item.text()) if high_item and high_item.text() else 0,
|
|
||||||
step=float(step_item.text()) if step_item and step_item.text() else 0,
|
|
||||||
step_type=step_combo.currentText() if step_combo else "абс",
|
|
||||||
unit=unit_item.text() if unit_item else "",
|
|
||||||
percentage=float(percent_item.text()) if percent_item and percent_item.text() else None,
|
|
||||||
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None
|
|
||||||
)
|
|
||||||
factors.append(factor)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Данные результатов (если есть)
|
|
||||||
results_data = None
|
|
||||||
if hasattr(self, 'generated_design') and self.generated_design:
|
|
||||||
results = []
|
|
||||||
for i in range(self.results_table.rowCount()):
|
|
||||||
item = self.results_table.item(i, 1)
|
|
||||||
if item and item.text():
|
|
||||||
try:
|
|
||||||
results.append([float(item.text())])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if results:
|
|
||||||
responses = [ResponseData(name="Отклик", unit="")]
|
|
||||||
results_data = ExperimentResultsData(
|
|
||||||
design=self.generated_design,
|
|
||||||
results=results,
|
|
||||||
responses=responses
|
|
||||||
)
|
|
||||||
|
|
||||||
# Создаём проект
|
|
||||||
project = create_new_project("Мой проект")
|
|
||||||
project.medium_total_volume = self.total_volume_spin.value()
|
|
||||||
project.medium_volume_unit = self.volume_unit_combo.currentText()
|
|
||||||
project.medium_solvent = self.solvent_input.text()
|
|
||||||
project.medium_reagents = reagents
|
|
||||||
project.experiment_factors = factors
|
|
||||||
project.experiment_center_points = self.center_points_spin.value()
|
|
||||||
project.experiment_randomize = self.randomize_check.isChecked()
|
|
||||||
project.experiment_results = results_data
|
|
||||||
|
|
||||||
return project
|
|
||||||
|
|
||||||
def _apply_project_data(self, project: ProjectData):
|
|
||||||
"""Применяет загруженные данные к GUI"""
|
|
||||||
# Применяем данные калькулятора сред
|
|
||||||
self.total_volume_spin.setValue(project.medium_total_volume)
|
|
||||||
|
|
||||||
index = self.volume_unit_combo.findText(project.medium_volume_unit)
|
|
||||||
if index >= 0:
|
|
||||||
self.volume_unit_combo.setCurrentIndex(index)
|
|
||||||
|
|
||||||
self.solvent_input.setText(project.medium_solvent)
|
|
||||||
|
|
||||||
# Очищаем и заполняем таблицу реагентов
|
|
||||||
self.reagents_table.setRowCount(0)
|
|
||||||
for reagent in project.medium_reagents:
|
|
||||||
row = self.reagents_table.rowCount()
|
|
||||||
self.reagents_table.insertRow(row)
|
|
||||||
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
|
|
||||||
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
|
|
||||||
|
|
||||||
unit_combo = QComboBox()
|
|
||||||
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
|
|
||||||
unit_combo.setCurrentText(reagent.unit)
|
|
||||||
self.reagents_table.setCellWidget(row, 2, unit_combo)
|
|
||||||
|
|
||||||
self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor)))
|
|
||||||
self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor)))
|
|
||||||
self.reagents_table.setItem(row, 5, QTableWidgetItem(""))
|
|
||||||
|
|
||||||
# Применяем данные факторов эксперимента
|
|
||||||
self.factors_table.setRowCount(0)
|
|
||||||
for factor in project.experiment_factors:
|
|
||||||
row = self.factors_table.rowCount()
|
|
||||||
self.factors_table.insertRow(row)
|
|
||||||
self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name))
|
|
||||||
|
|
||||||
if factor.percentage is not None:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
|
||||||
|
|
||||||
if factor.dilution_factor is not None:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center)))
|
|
||||||
self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step)))
|
|
||||||
|
|
||||||
step_combo = QComboBox()
|
|
||||||
step_combo.addItems(["абс", "%"])
|
|
||||||
step_combo.setCurrentText(factor.step_type)
|
|
||||||
self.factors_table.setCellWidget(row, 5, step_combo)
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high)))
|
|
||||||
self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low)))
|
|
||||||
self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit))
|
|
||||||
|
|
||||||
# Применяем настройки эксперимента
|
|
||||||
self.center_points_spin.setValue(project.experiment_center_points)
|
|
||||||
self.randomize_check.setChecked(project.experiment_randomize)
|
|
||||||
|
|
||||||
# Если есть результаты, загружаем их
|
|
||||||
if project.experiment_results and project.experiment_results.design:
|
|
||||||
self.generated_design = project.experiment_results.design
|
|
||||||
self._display_design_matrix()
|
|
||||||
|
|
||||||
# Загружаем результаты в таблицу
|
|
||||||
if project.experiment_results.results:
|
|
||||||
for i, row_results in enumerate(project.experiment_results.results):
|
|
||||||
if i < self.results_table.rowCount() and row_results:
|
|
||||||
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
|
|
||||||
|
|
||||||
# Переключаемся на вкладку с факторами
|
|
||||||
self.tab_widget.setCurrentIndex(1)
|
|
||||||
|
|
||||||
def _display_design_matrix(self):
|
|
||||||
"""Отображает матрицу планирования (добавить в _generate_design или отдельно)"""
|
|
||||||
if not self.generated_design:
|
|
||||||
return
|
|
||||||
|
|
||||||
n_exp = len(self.generated_design)
|
|
||||||
n_factors = len(self._get_factors_from_table())
|
|
||||||
|
|
||||||
self.design_matrix.setRowCount(n_exp)
|
|
||||||
self.design_matrix.setColumnCount(n_factors + 2)
|
|
||||||
headers = ["№"] + [f['name'] for f in self._get_factors_from_table()] + ["Тип"]
|
|
||||||
self.design_matrix.setHorizontalHeaderLabels(headers)
|
|
||||||
|
|
||||||
for exp_idx, exp in enumerate(self.generated_design):
|
|
||||||
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
|
|
||||||
|
|
||||||
for f_idx in range(n_factors):
|
|
||||||
key = f"Фактор_{f_idx + 1}"
|
|
||||||
if key not in exp:
|
|
||||||
continue
|
|
||||||
value = exp[key]['natural']
|
|
||||||
unit = self._get_factors_from_table()[f_idx]['unit']
|
|
||||||
|
|
||||||
display = self._format_number(value)
|
|
||||||
if unit:
|
|
||||||
display += f" {unit}"
|
|
||||||
|
|
||||||
item = QTableWidgetItem(display)
|
|
||||||
if exp.get('is_center', False):
|
|
||||||
item.setBackground(QColor(255, 255, 200))
|
|
||||||
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
|
|
||||||
|
|
||||||
if exp.get('is_center', False):
|
|
||||||
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
|
|
||||||
type_item.setBackground(QColor(255, 255, 200))
|
|
||||||
else:
|
|
||||||
type_item = QTableWidgetItem("Факторная")
|
|
||||||
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
|
|
||||||
|
|
||||||
self.design_matrix.resizeColumnsToContents()
|
|
||||||
self.export_csv_btn.setEnabled(True)
|
|
||||||
self._setup_results_table(n_exp)
|
|
||||||
|
|
||||||
# ДОБАВИТЬ В МЕТОД _setup_ui ПОСЛЕ СОЗДАНИЯ ВКЛАДОК:
|
|
||||||
|
|
||||||
# Добавляем кнопки сохранения/загрузки в тулбар
|
|
||||||
toolbar = self.addToolBar("Файл")
|
|
||||||
toolbar.setMovable(False)
|
|
||||||
|
|
||||||
save_action = toolbar.addAction("💾 Сохранить проект")
|
|
||||||
save_action.triggered.connect(self.save_project_to_json)
|
|
||||||
|
|
||||||
load_action = toolbar.addAction("📂 Загрузить проект")
|
|
||||||
load_action.triggered.connect(self.load_project_from_json)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
|
|
||||||
# Сохраняем ссылку на tab_widget для переключения вкладок
|
|
||||||
self.tab_widget = tabs
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Создан патч gui_patch.py"
|
|
||||||
|
|
||||||
# 3. ПРИМЕНЯЕМ ПАТЧ К GUI.PY
|
|
||||||
# Создаём резервную копию
|
|
||||||
cp gui.py gui.py.backup
|
|
||||||
echo "✅ Создана резервная копия gui.py.backup"
|
|
||||||
|
|
||||||
# Вставляем импорты после существующих импортов
|
|
||||||
sed -i '/from calculations.doe import/a\
|
|
||||||
from calculations.models.project_data import (\
|
|
||||||
ProjectData, ReagentData, FactorData, \
|
|
||||||
ResponseData, ExperimentResultsData, create_new_project\
|
|
||||||
)' gui.py
|
|
||||||
|
|
||||||
# Находим строку с self._setup_ui() и добавляем после неё toolbar
|
|
||||||
sed -i '/self._setup_ui()/a\ self._add_file_toolbar()' gui.py
|
|
||||||
|
|
||||||
# Добавляем новый метод _add_file_toolbar после _setup_ui
|
|
||||||
cat >> gui.py.new_method << 'EOF'
|
|
||||||
|
|
||||||
def _add_file_toolbar(self):
|
|
||||||
"""Добавляет панель инструментов с кнопками сохранения/загрузки"""
|
|
||||||
toolbar = self.addToolBar("Файл")
|
|
||||||
toolbar.setMovable(False)
|
|
||||||
|
|
||||||
save_action = toolbar.addAction("💾 Сохранить проект")
|
|
||||||
save_action.triggered.connect(self.save_project_to_json)
|
|
||||||
|
|
||||||
load_action = toolbar.addAction("📂 Загрузить проект")
|
|
||||||
load_action.triggered.connect(self.load_project_from_json)
|
|
||||||
|
|
||||||
toolbar.addSeparator()
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Вставляем новый метод после _setup_ui
|
|
||||||
sed -i '/def _setup_ui(self):/,/def _get_stylesheet(self):/ {
|
|
||||||
/def _get_stylesheet(self):/ {
|
|
||||||
r gui.py.new_method
|
|
||||||
}
|
|
||||||
}' gui.py
|
|
||||||
|
|
||||||
rm gui.py.new_method
|
|
||||||
|
|
||||||
# Добавляем методы сохранения/загрузки в конец файла перед if __name__
|
|
||||||
cat >> gui.py << 'EOF'
|
|
||||||
|
|
||||||
# ========== МЕТОДЫ РАБОТЫ С JSON ==========
|
|
||||||
|
|
||||||
def save_project_to_json(self):
|
|
||||||
"""Сохраняет весь проект в JSON файл"""
|
|
||||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox
|
|
||||||
|
|
||||||
filename, _ = QFileDialog.getSaveFileName(
|
|
||||||
self, "Сохранить проект", "", "JSON Files (*.json);;All Files (*)"
|
|
||||||
)
|
|
||||||
if not filename:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not filename.endswith('.json'):
|
|
||||||
filename += '.json'
|
|
||||||
|
|
||||||
try:
|
|
||||||
project = self._collect_current_data()
|
|
||||||
project.save_to_file(filename)
|
|
||||||
QMessageBox.information(self, "Успех", f"Проект сохранён в {filename}")
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.critical(self, "Ошибка", f"Не удалось сохранить проект: {str(e)}")
|
|
||||||
|
|
||||||
def load_project_from_json(self):
|
|
||||||
"""Загружает проект из JSON файла"""
|
|
||||||
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QComboBox, QTableWidgetItem
|
|
||||||
|
|
||||||
filename, _ = QFileDialog.getOpenFileName(
|
|
||||||
self, "Загрузить проект", "", "JSON Files (*.json);;All Files (*)"
|
|
||||||
)
|
|
||||||
if not filename:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
project = ProjectData.load_from_file(filename)
|
|
||||||
self._apply_project_data(project)
|
|
||||||
QMessageBox.information(self, "Успех", f"Проект загружен из {filename}")
|
|
||||||
except FileNotFoundError:
|
|
||||||
QMessageBox.critical(self, "Ошибка", f"Файл не найден: {filename}")
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.critical(self, "Ошибка", f"Не удалось загрузить проект: {str(e)}")
|
|
||||||
|
|
||||||
def _collect_current_data(self) -> ProjectData:
|
|
||||||
"""Собирает текущие данные из всех виджетов"""
|
|
||||||
from PyQt5.QtWidgets import QComboBox
|
|
||||||
|
|
||||||
# Данные калькулятора сред
|
|
||||||
reagents = []
|
|
||||||
for row in range(self.reagents_table.rowCount()):
|
|
||||||
name_item = self.reagents_table.item(row, 0)
|
|
||||||
percent_item = self.reagents_table.item(row, 1)
|
|
||||||
unit_widget = self.reagents_table.cellWidget(row, 2)
|
|
||||||
coeff_item = self.reagents_table.item(row, 3)
|
|
||||||
dilution_item = self.reagents_table.item(row, 4)
|
|
||||||
|
|
||||||
if name_item and percent_item and percent_item.text():
|
|
||||||
try:
|
|
||||||
reagent = ReagentData(
|
|
||||||
name=name_item.text(),
|
|
||||||
percentage=float(percent_item.text()),
|
|
||||||
unit=unit_widget.currentText() if unit_widget else "мг",
|
|
||||||
conversion_factor=float(coeff_item.text()) if coeff_item and coeff_item.text() else 1.0,
|
|
||||||
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else 1.0
|
|
||||||
)
|
|
||||||
reagents.append(reagent)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Данные факторов эксперимента
|
|
||||||
factors = []
|
|
||||||
for row in range(self.factors_table.rowCount()):
|
|
||||||
name_item = self.factors_table.item(row, 0)
|
|
||||||
center_item = self.factors_table.item(row, 3)
|
|
||||||
low_item = self.factors_table.item(row, 7)
|
|
||||||
high_item = self.factors_table.item(row, 6)
|
|
||||||
step_item = self.factors_table.item(row, 4)
|
|
||||||
unit_item = self.factors_table.item(row, 8)
|
|
||||||
step_combo = self.factors_table.cellWidget(row, 5)
|
|
||||||
percent_item = self.factors_table.item(row, 1)
|
|
||||||
dilution_item = self.factors_table.item(row, 2)
|
|
||||||
|
|
||||||
if name_item and center_item and center_item.text():
|
|
||||||
try:
|
|
||||||
factor = FactorData(
|
|
||||||
name=name_item.text(),
|
|
||||||
center=float(center_item.text()),
|
|
||||||
low=float(low_item.text()) if low_item and low_item.text() else 0,
|
|
||||||
high=float(high_item.text()) if high_item and high_item.text() else 0,
|
|
||||||
step=float(step_item.text()) if step_item and step_item.text() else 0,
|
|
||||||
step_type=step_combo.currentText() if step_combo else "абс",
|
|
||||||
unit=unit_item.text() if unit_item else "",
|
|
||||||
percentage=float(percent_item.text()) if percent_item and percent_item.text() else None,
|
|
||||||
dilution_factor=float(dilution_item.text()) if dilution_item and dilution_item.text() else None
|
|
||||||
)
|
|
||||||
factors.append(factor)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Данные результатов (если есть)
|
|
||||||
results_data = None
|
|
||||||
if hasattr(self, 'generated_design') and self.generated_design:
|
|
||||||
results = []
|
|
||||||
for i in range(self.results_table.rowCount()):
|
|
||||||
item = self.results_table.item(i, 1)
|
|
||||||
if item and item.text():
|
|
||||||
try:
|
|
||||||
results.append([float(item.text())])
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if results:
|
|
||||||
responses = [ResponseData(name="Отклик", unit="")]
|
|
||||||
results_data = ExperimentResultsData(
|
|
||||||
design=self.generated_design,
|
|
||||||
results=results,
|
|
||||||
responses=responses
|
|
||||||
)
|
|
||||||
|
|
||||||
# Создаём проект
|
|
||||||
project = create_new_project("Мой проект")
|
|
||||||
project.medium_total_volume = self.total_volume_spin.value()
|
|
||||||
project.medium_volume_unit = self.volume_unit_combo.currentText()
|
|
||||||
project.medium_solvent = self.solvent_input.text()
|
|
||||||
project.medium_reagents = reagents
|
|
||||||
project.experiment_factors = factors
|
|
||||||
project.experiment_center_points = self.center_points_spin.value()
|
|
||||||
project.experiment_randomize = self.randomize_check.isChecked()
|
|
||||||
project.experiment_results = results_data
|
|
||||||
|
|
||||||
return project
|
|
||||||
|
|
||||||
def _apply_project_data(self, project: ProjectData):
|
|
||||||
"""Применяет загруженные данные к GUI"""
|
|
||||||
from PyQt5.QtWidgets import QComboBox, QTableWidgetItem
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
|
|
||||||
# Применяем данные калькулятора сред
|
|
||||||
self.total_volume_spin.setValue(project.medium_total_volume)
|
|
||||||
|
|
||||||
index = self.volume_unit_combo.findText(project.medium_volume_unit)
|
|
||||||
if index >= 0:
|
|
||||||
self.volume_unit_combo.setCurrentIndex(index)
|
|
||||||
|
|
||||||
self.solvent_input.setText(project.medium_solvent)
|
|
||||||
|
|
||||||
# Очищаем и заполняем таблицу реагентов
|
|
||||||
self.reagents_table.setRowCount(0)
|
|
||||||
for reagent in project.medium_reagents:
|
|
||||||
row = self.reagents_table.rowCount()
|
|
||||||
self.reagents_table.insertRow(row)
|
|
||||||
self.reagents_table.setItem(row, 0, QTableWidgetItem(reagent.name))
|
|
||||||
self.reagents_table.setItem(row, 1, QTableWidgetItem(str(reagent.percentage)))
|
|
||||||
|
|
||||||
unit_combo = QComboBox()
|
|
||||||
unit_combo.addItems(["мг", "г", "кг", "мкг", "нг", "мл", "мкл", "л", "нл"])
|
|
||||||
unit_combo.setCurrentText(reagent.unit)
|
|
||||||
self.reagents_table.setCellWidget(row, 2, unit_combo)
|
|
||||||
|
|
||||||
self.reagents_table.setItem(row, 3, QTableWidgetItem(str(reagent.conversion_factor)))
|
|
||||||
self.reagents_table.setItem(row, 4, QTableWidgetItem(str(reagent.dilution_factor)))
|
|
||||||
self.reagents_table.setItem(row, 5, QTableWidgetItem(""))
|
|
||||||
|
|
||||||
# Применяем данные факторов эксперимента
|
|
||||||
self.factors_table.setRowCount(0)
|
|
||||||
for factor in project.experiment_factors:
|
|
||||||
row = self.factors_table.rowCount()
|
|
||||||
self.factors_table.insertRow(row)
|
|
||||||
self.factors_table.setItem(row, 0, QTableWidgetItem(factor.name))
|
|
||||||
|
|
||||||
if factor.percentage is not None:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem(str(factor.percentage)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 1, QTableWidgetItem("0"))
|
|
||||||
|
|
||||||
if factor.dilution_factor is not None:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem(str(factor.dilution_factor)))
|
|
||||||
else:
|
|
||||||
self.factors_table.setItem(row, 2, QTableWidgetItem("1"))
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 3, QTableWidgetItem(str(factor.center)))
|
|
||||||
self.factors_table.setItem(row, 4, QTableWidgetItem(str(factor.step)))
|
|
||||||
|
|
||||||
step_combo = QComboBox()
|
|
||||||
step_combo.addItems(["абс", "%"])
|
|
||||||
step_combo.setCurrentText(factor.step_type)
|
|
||||||
self.factors_table.setCellWidget(row, 5, step_combo)
|
|
||||||
|
|
||||||
self.factors_table.setItem(row, 6, QTableWidgetItem(str(factor.high)))
|
|
||||||
self.factors_table.setItem(row, 7, QTableWidgetItem(str(factor.low)))
|
|
||||||
self.factors_table.setItem(row, 8, QTableWidgetItem(factor.unit))
|
|
||||||
|
|
||||||
# Применяем настройки эксперимента
|
|
||||||
self.center_points_spin.setValue(project.experiment_center_points)
|
|
||||||
self.randomize_check.setChecked(project.experiment_randomize)
|
|
||||||
|
|
||||||
# Если есть результаты, загружаем их
|
|
||||||
if project.experiment_results and project.experiment_results.design:
|
|
||||||
self.generated_design = project.experiment_results.design
|
|
||||||
# Обновляем отображение матрицы
|
|
||||||
self._refresh_design_matrix()
|
|
||||||
|
|
||||||
# Загружаем результаты в таблицу
|
|
||||||
if project.experiment_results.results:
|
|
||||||
for i, row_results in enumerate(project.experiment_results.results):
|
|
||||||
if i < self.results_table.rowCount() and row_results:
|
|
||||||
self.results_table.setItem(i, 1, QTableWidgetItem(str(row_results[0])))
|
|
||||||
|
|
||||||
# Переключаемся на вкладку с факторами
|
|
||||||
if hasattr(self, 'tab_widget'):
|
|
||||||
self.tab_widget.setCurrentIndex(1)
|
|
||||||
|
|
||||||
def _refresh_design_matrix(self):
|
|
||||||
"""Обновляет отображение матрицы планирования"""
|
|
||||||
if not self.generated_design:
|
|
||||||
return
|
|
||||||
|
|
||||||
factors = self._get_factors_from_table()
|
|
||||||
n_exp = len(self.generated_design)
|
|
||||||
n_factors = len(factors)
|
|
||||||
|
|
||||||
self.design_matrix.setRowCount(n_exp)
|
|
||||||
self.design_matrix.setColumnCount(n_factors + 2)
|
|
||||||
headers = ["№"] + [f['name'] for f in factors] + ["Тип"]
|
|
||||||
self.design_matrix.setHorizontalHeaderLabels(headers)
|
|
||||||
|
|
||||||
for exp_idx, exp in enumerate(self.generated_design):
|
|
||||||
self.design_matrix.setItem(exp_idx, 0, QTableWidgetItem(str(exp_idx + 1)))
|
|
||||||
|
|
||||||
for f_idx in range(n_factors):
|
|
||||||
key = f"Фактор_{f_idx + 1}"
|
|
||||||
if key not in exp:
|
|
||||||
continue
|
|
||||||
value = exp[key]['natural']
|
|
||||||
unit = factors[f_idx]['unit']
|
|
||||||
|
|
||||||
display = self._format_number(value)
|
|
||||||
if unit:
|
|
||||||
display += f" {unit}"
|
|
||||||
|
|
||||||
item = QTableWidgetItem(display)
|
|
||||||
if exp.get('is_center', False):
|
|
||||||
item.setBackground(QColor(255, 255, 200))
|
|
||||||
self.design_matrix.setItem(exp_idx, f_idx + 1, item)
|
|
||||||
|
|
||||||
if exp.get('is_center', False):
|
|
||||||
type_item = QTableWidgetItem(f"Центр #{exp['center_num']}")
|
|
||||||
type_item.setBackground(QColor(255, 255, 200))
|
|
||||||
else:
|
|
||||||
type_item = QTableWidgetItem("Факторная")
|
|
||||||
self.design_matrix.setItem(exp_idx, n_factors + 1, type_item)
|
|
||||||
|
|
||||||
self.design_matrix.resizeColumnsToContents()
|
|
||||||
|
|
||||||
if hasattr(self, 'export_csv_btn'):
|
|
||||||
self.export_csv_btn.setEnabled(True)
|
|
||||||
|
|
||||||
n_factorial = 2 ** n_factors
|
|
||||||
n_center = self.center_points_spin.value()
|
|
||||||
self.design_info.setText(
|
|
||||||
f"📊 Факторных точек: {n_factorial}, Центральных: {n_center}, Всего: {n_exp}"
|
|
||||||
)
|
|
||||||
self._setup_results_table(n_exp)
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Добавлены методы работы с JSON в gui.py"
|
|
||||||
|
|
||||||
# 4. СОЗДАЁМ ПРИМЕР JSON ФАЙЛА
|
|
||||||
cat > example_project.json << 'EOF'
|
|
||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"name": "Пример проекта",
|
|
||||||
"created_at": "2026-05-14T16:00:00",
|
|
||||||
"modified_at": "2026-05-14T16:00:00",
|
|
||||||
"version": "1.0"
|
|
||||||
},
|
|
||||||
"medium_calculator": {
|
|
||||||
"total_volume": 1000,
|
|
||||||
"volume_unit": "мл",
|
|
||||||
"solvent": "Вода",
|
|
||||||
"reagents": [
|
|
||||||
{
|
|
||||||
"name": "Глюкоза",
|
|
||||||
"percentage": 2.0,
|
|
||||||
"unit": "г",
|
|
||||||
"conversion_factor": 1.0,
|
|
||||||
"dilution_factor": 1.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Пептон",
|
|
||||||
"percentage": 1.0,
|
|
||||||
"unit": "г",
|
|
||||||
"conversion_factor": 1.0,
|
|
||||||
"dilution_factor": 1.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"experiment": {
|
|
||||||
"factors": [
|
|
||||||
{
|
|
||||||
"name": "Глюкоза",
|
|
||||||
"center": 20.0,
|
|
||||||
"low": 18.0,
|
|
||||||
"high": 22.0,
|
|
||||||
"step": 2.0,
|
|
||||||
"step_type": "абс",
|
|
||||||
"unit": "г",
|
|
||||||
"percentage": 2.0,
|
|
||||||
"dilution_factor": 1.0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Пептон",
|
|
||||||
"center": 10.0,
|
|
||||||
"low": 9.0,
|
|
||||||
"high": 11.0,
|
|
||||||
"step": 1.0,
|
|
||||||
"step_type": "абс",
|
|
||||||
"unit": "г",
|
|
||||||
"percentage": 1.0,
|
|
||||||
"dilution_factor": 1.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": [
|
|
||||||
{"name": "Отклик", "unit": ""}
|
|
||||||
],
|
|
||||||
"center_points": 3,
|
|
||||||
"randomize": true,
|
|
||||||
"results": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "✅ Создан example_project.json"
|
|
||||||
|
|
||||||
# 5. СОЗДАЁМ requirements.txt ЕСЛИ ЕГО НЕТ
|
|
||||||
if [ ! -f requirements.txt ]; then
|
|
||||||
cat > requirements.txt << 'EOF'
|
|
||||||
PyQt5>=5.15.0
|
|
||||||
numpy>=1.21.0
|
|
||||||
EOF
|
|
||||||
echo "✅ Создан requirements.txt"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "================================================================"
|
|
||||||
echo "✅ ГОТОВО! Функционал сохранения/загрузки JSON добавлен!"
|
|
||||||
echo "================================================================"
|
|
||||||
echo ""
|
|
||||||
echo "Что было добавлено:"
|
|
||||||
echo " 1. Модель данных в calculations/models/project_data.py"
|
|
||||||
echo " 2. Кнопки 'Сохранить проект' и 'Загрузить проект' в toolbar"
|
|
||||||
echo " 3. Методы сохранения/загрузки в gui.py"
|
|
||||||
echo " 4. Пример JSON файла example_project.json"
|
|
||||||
echo ""
|
|
||||||
echo "Для запуска программы:"
|
|
||||||
echo " python main.py"
|
|
||||||
echo ""
|
|
||||||
echo "Для восстановления оригинального gui.py (если нужно):"
|
|
||||||
echo " cp gui.py.backup gui.py"
|
|
||||||
echo ""
|
|
||||||
echo "Чтобы применить изменения, перезапустите программу"
|
|
||||||
echo "================================================================"
|
|
||||||
@@ -0,0 +1,745 @@
|
|||||||
|
# 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
|
||||||
Reference in New Issue
Block a user