feat: инициализация проекта калькулятора питательных сред

This commit is contained in:
2026-05-05 12:25:30 +05:00
commit cde52d1123
10 changed files with 269674 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyQt
*.ui
*.qrc
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Локальные настройки
*.local
+34
View File
@@ -0,0 +1,34 @@
{
"total_amount": 0.03,
"amount_unit": "l",
"reagents": [
{
"name": "H2O",
"percentage": 73.0,
"unit": "l",
"prefix": "milli",
"conversion_factor": 1.0
},
{
"name": "C2H5OH",
"percentage": 5.0,
"unit": "l",
"prefix": "milli",
"conversion_factor": 1.0
},
{
"name": "Меласса",
"percentage": 20.0,
"unit": "l",
"prefix": "milli",
"conversion_factor": 1.0
},
{
"name": "Лимонная кислота",
"percentage": 2.0,
"unit": "g",
"prefix": "milli",
"conversion_factor": 3.0
}
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"total_amount": 30.0,
"amount_unit": "мл",
"solvent": "Вода",
"reagents": [
{
"name": "Спирт",
"percentage": 10.0,
"unit": "мл",
"conversion_factor": 1.0
},
{
"name": "Меласса",
"percentage": 12.0,
"unit": "мл",
"conversion_factor": 1.0
}
]
}
+19
View File
@@ -0,0 +1,19 @@
{
"total_amount": 30.0,
"amount_unit": "мл",
"solvent": "Вода",
"reagents": [
{
"name": "Спирт",
"percentage": 10.0,
"unit": "мл",
"conversion_factor": 1.0
},
{
"name": "Меласса",
"percentage": 12.0,
"unit": "л",
"conversion_factor": 1.0
}
]
}
+142
View File
@@ -0,0 +1,142 @@
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox
from model import Reagent
class Controller:
def __init__(self, view, model):
self.view = view
self.model = model
self.setup_connections()
def setup_connections(self):
"""Подключает сигналы виджетов к методам контроллера"""
self.view.add_row_btn.clicked.connect(self.add_reagent_row)
self.view.remove_row_btn.clicked.connect(self.remove_reagent_row)
self.view.calculate_btn.clicked.connect(self.calculate)
self.view.save_btn.clicked.connect(self.save_composition)
self.view.load_btn.clicked.connect(self.load_composition)
# Авторасчёт при изменении количества среды
self.view.amount_input.valueChanged.connect(self.calculate)
# Авторасчёт при смене единицы измерения
self.view.amount_unit_combo.currentTextChanged.connect(self.calculate)
def add_reagent_row(self):
"""Добавляет новую строку в таблицу реагентов с предустановленными значениями"""
row = self.view.table.rowCount()
self.view.table.insertRow(row)
self.view.table.setItem(row, 0, QTableWidgetItem("Реагент"))
self.view.table.setItem(row, 1, QTableWidgetItem("1.0"))
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText("мг")
self.view.table.setCellWidget(row, 2, unit_combo)
self.view.table.setItem(row, 3, QTableWidgetItem("1.0"))
self.view.table.setItem(row, 4, QTableWidgetItem(""))
def remove_reagent_row(self):
"""Удаляет выделенную строку из таблицы реагентов и из модели"""
current_row = self.view.table.currentRow()
if current_row >= 0:
self.view.table.removeRow(current_row)
if 0 <= current_row < len(self.model.reagents):
del self.model.reagents[current_row]
def calculate(self):
"""Выполняет расчёт количеств реагентов и растворителя"""
try:
self._update_model_from_view()
results = self.model.calculate_amounts()
self.view.update_results(results)
except ValueError as e:
self.view.show_error(f"Ошибка в данных: {str(e)}")
except Exception as e:
self.view.show_error(f"Неожиданная ошибка: {str(e)}")
def _update_model_from_view(self):
"""Обновляет модель данными из интерфейса"""
self.model.reagents.clear()
self.model.total_amount = self.view.amount_input.value()
self.model.amount_unit = self.view.amount_unit_combo.currentText()
self.model.solvent = self.view.solvent_input.text()
for row in range(self.view.table.rowCount()):
name_item = self.view.table.item(row, 0)
percentage_item = self.view.table.item(row, 1)
unit_widget = self.view.table.cellWidget(row, 2)
conversion_item = self.view.table.item(row, 3)
if not all([name_item, percentage_item, conversion_item]):
continue
name = name_item.text()
percentage = float(percentage_item.text())
unit = unit_widget.currentText()
conversion_factor = float(conversion_item.text())
reagent = Reagent(name, percentage, unit, conversion_factor)
self.model.add_reagent(reagent)
def _update_view_from_model(self):
"""Обновляет интерфейс данными из модели"""
self.view.table.setRowCount(0)
self.view.amount_input.setValue(self.model.total_amount)
index = self.view.amount_unit_combo.findText(self.model.amount_unit)
if index >= 0:
self.view.amount_unit_combo.setCurrentIndex(index)
self.view.solvent_input.setText(self.model.solvent)
for reagent in self.model.reagents:
row = self.view.table.rowCount()
self.view.table.insertRow(row)
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText(reagent.unit)
self.view.table.setCellWidget(row, 2, unit_combo)
self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}"))
self.view.table.setItem(row, 4, QTableWidgetItem(""))
def save_composition(self):
"""Сохраняет состав среды в JSON‑файл"""
filename, _ = QFileDialog.getSaveFileName(
self.view,
"Сохранить состав среды",
"",
"JSON Files (*.json);;All Files (*)"
)
if filename:
if not filename.lower().endswith('.json'):
filename += '.json'
try:
self._update_model_from_view()
self.model.save_to_file(filename)
QMessageBox.information(self.view, "Успех", "Состав среды успешно сохранён!")
except Exception as e:
self.view.show_error(f"Ошибка сохранения: {str(e)}")
def load_composition(self):
"""Загружает состав среды из JSON‑файла"""
filename, _ = QFileDialog.getOpenFileName(
self.view,
"Загрузить состав среды",
"",
"JSON Files (*.json);;All Files (*)"
)
if filename:
try:
self.model.load_from_file(filename)
self._update_view_from_model() # Исправлено: добавлены скобки ()
self.calculate()
QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен!")
except Exception as e:
self.view.show_error(f"Ошибка загрузки: {str(e)}")
Executable
+34
View File
@@ -0,0 +1,34 @@
import sys
from PyQt5.QtWidgets import QApplication
from model import Model
from view import MainWindow
from controller import Controller
def main():
# Создаём приложение Qt
app = QApplication(sys.argv)
# Инициализируем компоненты MVC
model = Model() # Модель данных
view = MainWindow() # Графический интерфейс
controller = Controller(view, model) # Контроллер, связывающий модель и представление
# Связываем модель с представлением — критически важный шаг для устранения ошибки «Модель не установлена»
view.set_model(model)
# Настраиваем контроллер (подключаем обработчики событий к кнопкам)
controller.setup_connections()
# Отображаем главное окно приложения
view.show()
# Запускаем главный цикл обработки событий Qt и ожидаем завершения приложения
sys.exit(app.exec_())
if __name__ == '__main__':
main()
+150
View File
@@ -0,0 +1,150 @@
import json
VOLUME_UNITS = {
'нл': 0.001, # 1 нл = 0.001 мкл
'мкл': 1.0, # базовая единица объёма
'мл': 1000.0, # 1 мл = 1000 мкл
'л': 1000000.0 # 1 л = 1 000 000 мкл
}
MASS_UNITS = {
'нг': 0.000001, # 1 нг = 0.001 мкг
'мкг': 0.001, # базовая единица массы
'мг': 1.0, # 1 мг = 1000 мкг
'г': 1000.0, # 1 г = 1 000 000 мкг
'кг': 1000000.0 # 1 кг = 1 000 000 000 мкг
}
class Reagent:
def __init__(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0):
self.name = name
self.percentage = percentage
self.unit = unit
self.conversion_factor = conversion_factor
def to_dict(self):
"""Преобразует реагент в словарь для сохранения в JSON"""
return {
'name': self.name,
'percentage': self.percentage,
'unit': self.unit,
'conversion_factor': self.conversion_factor
}
@classmethod
def from_dict(cls, data):
"""Создаёт реагент из словаря, загруженного из JSON"""
return cls(
name=data['name'],
percentage=data['percentage'],
unit=data['unit'],
conversion_factor=data['conversion_factor']
)
class Model:
def __init__(self):
self.reagents = []
self.total_amount = 1000.0 # по умолчанию 1000 мл
self.amount_unit = "мл"
self.solvent = "Вода"
def convert_amount(self, amount_mkl_or_mkg: float, target_unit: str, is_volume: bool) -> float:
"""
Пересчитывает количество из базовых единиц (мкл/мкг) в целевую единицу.
Args:
amount_mkl_or_mkg: количество в базовых единицах (мкл или мкг)
target_unit: целевая единица измерения
is_volume: True — объём, False — масса
Returns:
Количество в целевой единице
"""
if is_volume:
conversion_factor = VOLUME_UNITS.get(target_unit, 1.0)
else:
conversion_factor = MASS_UNITS.get(target_unit, 1.0)
return amount_mkl_or_mkg / conversion_factor
def add_reagent(self, reagent: Reagent):
"""Добавляет реагент в список"""
self.reagents.append(reagent)
def calculate_amounts(self) -> list:
"""
Рассчитывает абсолютное количество каждого реагента с учётом единиц измерения.
Возвращает список количеств в единицах измерения реагента.
"""
results = []
for reagent in self.reagents:
# Базовый расчёт в процентах от общего количества
base_amount = (reagent.percentage / 100) * self.total_amount
# Определяем, объём или масса
is_volume = reagent.unit in VOLUME_UNITS
is_mass = reagent.unit in MASS_UNITS
if is_volume:
# Переводим общее количество среды в мкл для расчёта
total_in_mkl = self.total_amount * VOLUME_UNITS[self.amount_unit]
# Расчёт в мкл
amount_in_mkl = (reagent.percentage / 100) * total_in_mkl
# Переводим обратно в единицы реагента
final_amount = self.convert_amount(amount_in_mkl, reagent.unit, True)
elif is_mass:
# Переводим общее количество среды в мкг для расчёта (если среда в единицах объёма,
# предполагаем плотность = 1 г/мл)
if self.amount_unit in VOLUME_UNITS:
# Предполагаем плотность 1 г/мл: 1 мл ≈ 1 г
total_in_mkg = self.total_amount * VOLUME_UNITS[self.amount_unit] # в мкл
total_in_mkg *= 1000 # переводим в мкг (1 мкл воды ≈ 1 мкг)
else:
total_in_mkg = self.total_amount * MASS_UNITS[self.amount_unit]
# Расчёт в мкг
amount_in_mkg = (reagent.percentage / 100) * total_in_mkg
# Переводим в единицы реагента
final_amount = self.convert_amount(amount_in_mkg, reagent.unit, False)
else:
# Если единица неизвестна, возвращаем базовый расчёт
final_amount = base_amount
results.append(final_amount)
return results
def get_solvent_info(self) -> dict:
"""Возвращает информацию о растворителе: процент и количество"""
total_percentage = sum(r.percentage for r in self.reagents)
solvent_percentage = max(0, 100 - total_percentage) # не меньше 0%
solvent_amount = (solvent_percentage / 100) * self.total_amount
return {
'percentage': solvent_percentage,
'amount': solvent_amount,
'unit': self.amount_unit
}
def save_to_file(self, filename: str):
"""Сохраняет состав среды в JSON‑файл"""
data = {
'total_amount': self.total_amount,
'amount_unit': self.amount_unit,
'solvent': self.solvent,
'reagents': [r.to_dict() for r in self.reagents]
}
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
def load_from_file(self, filename: str):
"""Загружает состав среды из JSON‑файла"""
with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f)
# Восстанавливаем данные модели
self.total_amount = data['total_amount']
self.amount_unit = data['amount_unit']
self.solvent = data['solvent']
# Очищаем и заполняем список реагентов
self.reagents.clear()
for reagent_data in data['reagents']:
reagent = Reagent.from_dict(reagent_data)
self.reagents.append(reagent)
+1
View File
@@ -0,0 +1 @@
PyQt5>=5.15.0
+269095
View File
File diff suppressed because it is too large Load Diff
+134
View File
@@ -0,0 +1,134 @@
from PyQt5.QtWidgets import (
QMainWindow, QVBoxLayout, QHBoxLayout, QWidget,
QLabel, QDoubleSpinBox, QComboBox, QPushButton,
QTableWidget, QTableWidgetItem, QLineEdit, QMessageBox
)
from PyQt5.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Калькулятор питательных сред")
self.setGeometry(100, 100, 800, 600)
self._init_ui()
def _init_ui(self):
"""Инициализирует все виджеты интерфейса"""
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# Секция параметров среды
params_layout = QHBoxLayout()
# Общее количество среды
amount_layout = QHBoxLayout()
amount_layout.addWidget(QLabel("Общее количество:"))
self.amount_input = QDoubleSpinBox()
self.amount_input.setRange(0.001, 1000000)
self.amount_input.setValue(1000.0)
self.amount_input.setSingleStep(10)
amount_layout.addWidget(self.amount_input)
# Единица измерения
self.amount_unit_combo = QComboBox()
self.amount_unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
self.amount_unit_combo.setCurrentText("мл")
amount_layout.addWidget(self.amount_unit_combo)
# Растворитель
solvent_layout = QHBoxLayout()
solvent_layout.addWidget(QLabel("Растворитель:"))
self.solvent_input = QLineEdit()
self.solvent_input.setText("Вода")
solvent_layout.addWidget(self.solvent_input)
params_layout.addLayout(amount_layout)
params_layout.addSpacing(20)
params_layout.addLayout(solvent_layout)
params_layout.addStretch()
layout.addLayout(params_layout)
# Таблица реагентов
self.table = QTableWidget()
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels([
"Название реагента",
"Процент (%)",
"Единица измерения",
"Коэффициент пересчёта",
"Результат"
])
layout.addWidget(self.table)
# Кнопки управления
buttons_layout = QHBoxLayout()
self.add_row_btn = QPushButton("Добавить реагент")
self.remove_row_btn = QPushButton("Удалить реагент")
self.calculate_btn = QPushButton("Рассчитать")
self.save_btn = QPushButton("Сохранить")
self.load_btn = QPushButton("Загрузить")
buttons_layout.addWidget(self.add_row_btn)
buttons_layout.addWidget(self.remove_row_btn)
buttons_layout.addWidget(self.calculate_btn)
buttons_layout.addWidget(self.save_btn)
buttons_layout.addWidget(self.load_btn)
buttons_layout.addStretch()
layout.addLayout(buttons_layout)
# Добавляем начальную строку
self.add_initial_row()
def add_initial_row(self):
"""Добавляет начальную строку в таблицу"""
self.table.insertRow(0)
self.table.setItem(0, 0, QTableWidgetItem("Реагент"))
self.table.setItem(0, 1, QTableWidgetItem("1.0"))
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText("мг")
self.table.setCellWidget(0, 2, unit_combo)
self.table.setItem(0, 3, QTableWidgetItem("1.0"))
self.table.setItem(0, 4, QTableWidgetItem(""))
def set_model(self, model):
"""Устанавливает связь с моделью"""
self.model = model
def update_results(self, results: list):
"""Обновляет столбец результатов в таблице"""
for row, amount in enumerate(results):
if row < self.table.rowCount():
# Округление до 4 знаков после запятой
self.table.setItem(row, 4, QTableWidgetItem(f"{amount:.4f}"))
def show_error(self, message: str):
"""Показывает диалоговое окно с ошибкой"""
QMessageBox.critical(self, "Ошибка", message)
def get_table_data(self) -> list:
"""Возвращает данные из таблицы для отладки"""
data = []
for row in range(self.table.rowCount()):
row_data = []
for col in range(self.table.columnCount() - 1): # не берём столбец результатов
item = self.table.item(row, col)
if item:
row_data.append(item.text())
else:
widget = self.table.cellWidget(row, col)
if widget and isinstance(widget, QComboBox):
row_data.append(widget.currentText())
else:
row_data.append("")
data.append(row_data)
return data