Исправления:

- Добавлен столбец 'Разбавление (x)' для каждого реагента
- Переработана логика расчёта с учётом разбавления
- Исправлен расчёт количества растворителя
- Растворитель отображается в первой строке таблицы
- Поддержка дробных значений разбавления
This commit is contained in:
2026-05-05 22:31:08 +05:00
parent cde52d1123
commit 6798fd5f63
5 changed files with 467 additions and 277 deletions
+117 -64
View File
@@ -1,100 +1,120 @@
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox from PyQt5.QtWidgets import QMessageBox, QFileDialog, QTableWidgetItem, QComboBox, QDoubleSpinBox, QLineEdit
from model import Reagent from PyQt5.QtCore import Qt
from model import Model
from view import MainWindow
import json
from reagent import Reagent
class Controller: class Controller:
def __init__(self, view, model): def __init__(self):
self.view = view self.model = Model()
self.model = model self.view = MainWindow()
self.setup_connections() self._connect_signals()
def _connect_signals(self):
def setup_connections(self): """Подключает обработчики событий интерфейса"""
"""Подключает сигналы виджетов к методам контроллера"""
self.view.add_row_btn.clicked.connect(self.add_reagent_row) self.view.add_row_btn.clicked.connect(self.add_reagent_row)
self.view.remove_row_btn.clicked.connect(self.remove_reagent_row) self.view.remove_row_btn.clicked.connect(self.remove_reagent_row)
self.view.calculate_btn.clicked.connect(self.calculate) self.view.calculate_btn.clicked.connect(self._perform_calculation)
self.view.save_btn.clicked.connect(self.save_composition) self.view.save_btn.clicked.connect(self.save_composition)
self.view.load_btn.clicked.connect(self.load_composition) self.view.load_btn.clicked.connect(self.load_composition)
# Подключаем обновление названия растворителя в таблице при изменении поля
# Авторасчёт при изменении количества среды self.view.solvent_input.textChanged.connect(self.view.update_solvent_name)
self.view.amount_input.valueChanged.connect(self.calculate)
# Авторасчёт при смене единицы измерения
self.view.amount_unit_combo.currentTextChanged.connect(self.calculate)
def add_reagent_row(self): def add_reagent_row(self):
"""Добавляет новую строку в таблицу реагентов с предустановленными значениями""" """Добавляет новую строку для реагента"""
row = self.view.table.rowCount() self.view.add_new_row()
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): def remove_reagent_row(self):
"""Удаляет выделенную строку из таблицы реагентов и из модели""" """Удаляет выбранную строку реагента"""
current_row = self.view.table.currentRow() self.view.remove_selected_row()
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 _perform_calculation(self):
"""Выполняет расчёт и обновляет интерфейс"""
try:
self._update_model_from_view()
results, solvent_amount, solvent_percentage = self.model.calculate_amounts()
self.view.update_results(results)
def calculate(self): # Обновляем информацию о растворителе в первой строке таблицы
"""Выполняет расчёт количеств реагентов и растворителя""" self.view.update_solvent_result(solvent_amount, self.model.amount_unit)
try: self.view.update_solvent_percent(solvent_percentage)
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)}")
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): def _update_model_from_view(self):
"""Обновляет модель данными из интерфейса""" """Обновляет модель данными из интерфейса (только реагенты, без растворителя)"""
# Очищаем список реагентов в модели
self.model.reagents.clear() self.model.reagents.clear()
# Обновляем общее количество и единицу измерения
self.model.total_amount = self.view.amount_input.value() self.model.total_amount = self.view.amount_input.value()
self.model.amount_unit = self.view.amount_unit_combo.currentText() self.model.amount_unit = self.view.amount_unit_combo.currentText()
self.model.solvent = self.view.solvent_input.text() self.model.solvent = self.view.solvent_input.text()
for row in range(self.view.table.rowCount()): # Заполняем реагенты из таблицы (начиная с 1 строки, пропуская растворитель)
for row in range(1, self.view.table.rowCount()):
name_item = self.view.table.item(row, 0) name_item = self.view.table.item(row, 0)
percentage_item = self.view.table.item(row, 1) percentage_item = self.view.table.item(row, 1)
unit_widget = self.view.table.cellWidget(row, 2) unit_widget = self.view.table.cellWidget(row, 2)
conversion_item = self.view.table.item(row, 3) conversion_item = self.view.table.item(row, 3)
dilution_widget = self.view.table.cellWidget(row, 4)
# Пропускаем строку, если какие-то обязательные поля отсутствуют
if not all([name_item, percentage_item, conversion_item]): if not all([name_item, percentage_item, conversion_item]):
continue continue
name = name_item.text() try:
percentage = float(percentage_item.text()) name = name_item.text()
unit = unit_widget.currentText() percentage = float(percentage_item.text())
conversion_factor = float(conversion_item.text()) unit = unit_widget.currentText() if unit_widget else "мг"
conversion_factor = float(conversion_item.text())
reagent = Reagent(name, percentage, unit, conversion_factor) # Получаем коэффициент разбавления (поддерживаем QDoubleSpinBox и QLineEdit)
self.model.add_reagent(reagent) dilution_factor = 1.0
if dilution_widget:
if isinstance(dilution_widget, QDoubleSpinBox):
dilution_factor = dilution_widget.value()
elif isinstance(dilution_widget, QLineEdit):
try:
dilution_factor = float(dilution_widget.text())
except ValueError:
dilution_factor = 1.0
# Добавляем реагент в модель
self.model.add_reagent(name, percentage, unit, conversion_factor, dilution_factor)
except ValueError as e:
raise ValueError(f"Ошибка в строке {row + 1}: {str(e)}")
def _update_view_from_model(self): def _update_view_from_model(self):
"""Обновляет интерфейс данными из модели""" """Обновляет интерфейс данными из модели"""
self.view.table.setRowCount(0) # Очищаем таблицу, но сохраняем строку растворителя
while self.view.table.rowCount() > 1:
self.view.table.removeRow(1)
# Если нет строки растворителя, добавляем её
if self.view.table.rowCount() == 0:
self.view.add_solvent_row()
# Устанавливаем общее количество и единицу измерения
self.view.amount_input.setValue(self.model.total_amount) self.view.amount_input.setValue(self.model.total_amount)
index = self.view.amount_unit_combo.findText(self.model.amount_unit) index = self.view.amount_unit_combo.findText(self.model.amount_unit)
if index >= 0: if index >= 0:
self.view.amount_unit_combo.setCurrentIndex(index) self.view.amount_unit_combo.setCurrentIndex(index)
self.view.solvent_input.setText(self.model.solvent)
# Устанавливаем название растворителя и обновляем его в таблице
self.view.solvent_input.setText(self.model.solvent)
self.view.update_solvent_name()
# Заполняем таблицу реагентами из модели
for reagent in self.model.reagents: for reagent in self.model.reagents:
row = self.view.table.rowCount() row = self.view.table.rowCount()
self.view.table.insertRow(row) self.view.table.insertRow(row)
self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name)) self.view.table.setItem(row, 0, QTableWidgetItem(reagent.name))
self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}")) self.view.table.setItem(row, 1, QTableWidgetItem(f"{reagent.percentage:.2f}"))
@@ -102,21 +122,34 @@ class Controller:
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText(reagent.unit) unit_combo.setCurrentText(reagent.unit)
self.view.table.setCellWidget(row, 2, unit_combo) 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(""))
self.view.table.setItem(row, 3, QTableWidgetItem(f"{reagent.conversion_factor:.2f}"))
# Создаём поле для разбавления (QLineEdit для ручного ввода)
dilution_edit = QLineEdit()
dilution_edit.setText(f"{getattr(reagent, 'dilution_factor', 1.0):.3f}")
dilution_edit.setAlignment(Qt.AlignRight)
dilution_edit.setToolTip("Во сколько раз разбавить (1 = без разбавления)")
self.view.table.setCellWidget(row, 4, dilution_edit)
self.view.table.setItem(row, 5, QTableWidgetItem(""))
# Очищаем результаты
self.view.clear_results()
def save_composition(self): def save_composition(self):
"""Сохраняет состав среды в JSONфайл""" """Сохраняет состав среды в JSON-файл"""
filename, _ = QFileDialog.getSaveFileName( filename, _ = QFileDialog.getSaveFileName(
self.view, self.view,
"Сохранить состав среды", "Сохранить состав среды",
"", "",
"JSON Files (*.json);;All Files (*)" "JSON Files (*.json);;All Files (*)"
) )
if filename: if filename:
if not filename.lower().endswith('.json'): if not filename.lower().endswith('.json'):
filename += '.json' filename += '.json'
try: try:
self._update_model_from_view() self._update_model_from_view()
self.model.save_to_file(filename) self.model.save_to_file(filename)
@@ -125,18 +158,38 @@ class Controller:
self.view.show_error(f"Ошибка сохранения: {str(e)}") self.view.show_error(f"Ошибка сохранения: {str(e)}")
def load_composition(self): def load_composition(self):
"""Загружает состав среды из JSONфайла""" """Загружает состав среды из JSON-файла"""
filename, _ = QFileDialog.getOpenFileName( filename, _ = QFileDialog.getOpenFileName(
self.view, self.view,
"Загрузить состав среды", "Загрузить состав среды",
"", "",
"JSON Files (*.json);;All Files (*)" "JSON Files (*.json);;All Files (*)"
) )
if filename: if filename:
try: try:
self.model.load_from_file(filename) self.model.load_from_file(filename)
self._update_view_from_model() # Исправлено: добавлены скобки () self._update_view_from_model()
self.calculate() QMessageBox.information(
QMessageBox.information(self.view, "Успех", "Состав среды успешно загружен!") self.view,
"Успех",
"Состав среды успешно загружен"
)
except FileNotFoundError:
QMessageBox.critical(
self.view,
"Ошибка",
f"Файл не найден: {filename}"
)
except json.JSONDecodeError as e:
QMessageBox.critical(
self.view,
"Ошибка",
f"Неверный формат JSON-файла: {str(e)}"
)
except Exception as e: except Exception as e:
self.view.show_error(f"Ошибка загрузки: {str(e)}") QMessageBox.critical(
self.view,
"Ошибка",
f"Ошибка при загрузке состава: {str(e)}"
)
+7 -22
View File
@@ -1,34 +1,19 @@
# main.py
import sys import sys
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from model import Model
from view import MainWindow
from controller import Controller from controller import Controller
def main(): def main():
# Создаём приложение Qt
app = QApplication(sys.argv) app = QApplication(sys.argv)
# Инициализируем компоненты MVC # Создаём контроллер - он сам создаст модель и представление
model = Model() # Модель данных controller = Controller()
view = MainWindow() # Графический интерфейс
controller = Controller(view, model) # Контроллер, связывающий модель и представление
# Связываем модель с представлением — критически важный шаг для устранения ошибки «Модель не установлена» # Показываем главное окно
view.set_model(model) controller.view.show()
# Настраиваем контроллер (подключаем обработчики событий к кнопкам) # Запускаем цикл обработки событий
controller.setup_connections()
# Отображаем главное окно приложения
view.show()
# Запускаем главный цикл обработки событий Qt и ожидаем завершения приложения
sys.exit(app.exec_()) sys.exit(app.exec_())
if __name__ == "__main__":
if __name__ == '__main__':
main() main()
+140 -100
View File
@@ -1,150 +1,190 @@
import json import json
from reagent import Reagent
VOLUME_UNITS = { VOLUME_UNITS = {
'нл': 0.001, # 1 нл = 0.001 мкл 'нл': 0.001, # 1 нл = 0.001 мкл
'мкл': 1.0, # базовая единица объёма 'мкл': 1.0, # базовая единица объёма
'мл': 1000.0, # 1 мл = 1000 мкл 'мл': 1000.0, # 1 мл = 1000 мкл
'л': 1000000.0 # 1 л = 1 000 000 мкл 'л': 1000000.0 # 1 л = 1 000 000 мкл
} }
MASS_UNITS = { MASS_UNITS = {
'нг': 0.000001, # 1 нг = 0.001 мкг 'нг': 0.000001, # 1 нг = 0.001 мкг
'мкг': 0.001, # базовая единица массы 'мкг': 0.001, # базовая единица массы
'мг': 1.0, # 1 мг = 1000 мкг 'мг': 1.0, # 1 мг = 1000 мкг
'г': 1000.0, # 1 г = 1 000 000 мкг 'г': 1000.0, # 1 г = 1 000 000 мкг
'кг': 1000000.0 # 1 кг = 1 000 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: class Model:
def __init__(self): def __init__(self):
self.reagents = [] self.total_amount = 100.0 # общее количество среды (по умолчанию — 1000 мл)
self.total_amount = 1000.0 # по умолчанию 1000 мл self.amount_unit = 'мл' # единица измерения общего количества
self.amount_unit = "мл" self.solvent = 'Вода' # растворитель по умолчанию
self.solvent = "Вода" self.reagents = [] # список реагентов
def convert_amount(self, amount_mkl_or_mkg: float, target_unit: str, is_volume: bool) -> float:
def convert_amount(self, amount_base: float, target_unit: str, is_volume: bool) -> float:
""" """
Пересчитывает количество из базовых единиц (мкл/мкг) в целевую единицу. Пересчитывает количество из базовых единиц (мкл/мкг) в целевую единицу.
Args:
amount_mkl_or_mkg: количество в базовых единицах (мкл или мкг)
target_unit: целевая единица измерения
is_volume: True — объём, False — масса
Returns:
Количество в целевой единице
""" """
if is_volume: if is_volume:
conversion_factor = VOLUME_UNITS.get(target_unit, 1.0) conversion_factor = VOLUME_UNITS.get(target_unit, 1.0)
else: else:
conversion_factor = MASS_UNITS.get(target_unit, 1.0) conversion_factor = MASS_UNITS.get(target_unit, 1.0)
return amount_mkl_or_mkg / conversion_factor return amount_base / conversion_factor
def add_reagent(self, reagent: Reagent): def calculate_amounts(self) -> tuple:
"""Добавляет реагент в список"""
self.reagents.append(reagent)
def calculate_amounts(self) -> list:
""" """
Рассчитывает абсолютное количество каждого реагента с учётом единиц измерения. Рассчитывает:
Возвращает список количеств в единицах измерения реагента. 1. Исходное количество реагента для неразбавленного состава
2. Количество разбавленного реагента, которое нужно взять
3. Количество растворителя с учётом добавленных разбавленных реагентов
Возвращает: (список количеств разбавленных реагентов, количество растворителя, процент растворителя)
""" """
results = [] results = []
# Проверяем, есть ли реагенты
if not self.reagents:
return results, self.total_amount, 100.0
# Суммируем проценты всех реагентов
total_percentage = sum(reagent.percentage for reagent in self.reagents)
# Проверяем, что сумма процентов не превышает 100
if total_percentage > 100:
raise ValueError(f"Сумма процентов реагентов ({total_percentage:.2f}%) превышает 100%")
# Шаг 1: Рассчитываем общий объём среды в базовых единицах (мкл)
total_in_base = self.total_amount * VOLUME_UNITS[self.amount_unit]
# Шаг 2: Рассчитываем количество каждого реагента для неразбавленного состава
undiluted_amounts = []
for reagent in self.reagents: for reagent in self.reagents:
# Базовый расчёт в процентах от общего количества # Расчёт количества реагента в базовых единицах
base_amount = (reagent.percentage / 100) * self.total_amount amount_in_base = (reagent.percentage / 100) * total_in_base
# Определяем, объём или масса # Определение типа единицы измерения реагента
is_volume = reagent.unit in VOLUME_UNITS
# Применение коэффициента пересчёта
adjusted_amount = amount_in_base * reagent.conversion_factor
# Пересчёт в целевую единицу измерения
final_amount = self.convert_amount(adjusted_amount, reagent.unit, is_volume)
undiluted_amounts.append(final_amount)
# Шаг 3: Рассчитываем количество разбавленного реагента, которое нужно взять
# и общий объём вносимых разбавленных реагентов (в базовых единицах)
diluted_amounts = []
total_diluted_volume_base = 0
for i, reagent in enumerate(self.reagents):
dilution_factor = getattr(reagent, 'dilution_factor', 1.0)
if dilution_factor <= 0:
dilution_factor = 1.0
# Количество разбавленного реагента = исходное количество / фактор разбавления
diluted_amount = undiluted_amounts[i] * dilution_factor
diluted_amounts.append(diluted_amount)
# Вычисляем объём разбавленного реагента в базовых единицах (мкл)
is_volume = reagent.unit in VOLUME_UNITS is_volume = reagent.unit in VOLUME_UNITS
is_mass = reagent.unit in MASS_UNITS
if is_volume: if is_volume:
# Переводим общее количество среды в мкл для расчёта # Если реагент в объёмных единицах, его объём = diluted_amount
total_in_mkl = self.total_amount * VOLUME_UNITS[self.amount_unit] reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.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: else:
# Если единица неизвестна, возвращаем базовый расчёт # Если реагент в массовых единицах, считаем, что он вносит пренебрежимый объём
final_amount = base_amount # или можно добавить коэффициент плотности, пока игнорируем
reagent_volume_base = 0
results.append(final_amount) total_diluted_volume_base += reagent_volume_base
return results
# Шаг 4: Рассчитываем количество растворителя
# Растворитель = общий объём - объём всех разбавленных реагентов
solvent_volume_base = total_in_base - total_diluted_volume_base
def get_solvent_info(self) -> dict: # Переводим количество растворителя в целевую единицу измерения
"""Возвращает информацию о растворителе: процент и количество""" solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit]
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 if solvent_amount < 0:
return { solvent_amount = 0
'percentage': solvent_percentage,
'amount': solvent_amount, # Процент растворителя в неразбавленном составе
'unit': self.amount_unit solvent_percentage = 100 - total_percentage
}
return diluted_amounts, solvent_amount, solvent_percentage
def save_to_file(self, filename: str): def save_to_file(self, filename: str):
"""Сохраняет состав среды в JSONфайл""" """Сохраняет модель в JSON-файл"""
data = { data = {
'total_amount': self.total_amount, 'total_amount': self.total_amount,
'amount_unit': self.amount_unit, 'amount_unit': self.amount_unit,
'solvent': self.solvent, 'solvent': self.solvent,
'reagents': [r.to_dict() for r in self.reagents] 'reagents': [
{
'name': r.name,
'percentage': r.percentage,
'unit': r.unit,
'conversion_factor': r.conversion_factor,
'dilution_factor': getattr(r, 'dilution_factor', 1.0)
} for r in self.reagents
]
} }
with open(filename, 'w', encoding='utf-8') as f: with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2) json.dump(data, f, ensure_ascii=False, indent=4)
def load_from_file(self, filename: str): def load_from_file(self, filename: str):
"""Загружает состав среды из JSONфайла""" """Загружает модель из JSON-файла"""
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
data = json.load(f) data = json.load(f)
# Восстанавливаем данные модели
self.total_amount = data['total_amount'] self.total_amount = data['total_amount']
self.amount_unit = data['amount_unit'] self.amount_unit = data['amount_unit']
self.solvent = data['solvent'] self.solvent = data['solvent']
# Очищаем и заполняем список реагентов
self.reagents.clear() self.reagents.clear()
for reagent_data in data['reagents']:
reagent = Reagent.from_dict(reagent_data) for r_data in data['reagents']:
reagent = Reagent(
name=r_data['name'],
percentage=r_data['percentage'],
unit=r_data['unit'],
conversion_factor=r_data.get('conversion_factor', 1.0)
)
# Добавляем коэффициент разбавления
reagent.dilution_factor = r_data.get('dilution_factor', 1.0)
self.reagents.append(reagent) self.reagents.append(reagent)
def add_reagent(self, name: str, percentage: float, unit: str, conversion_factor: float = 1.0,
dilution_factor: float = 1.0):
"""Добавляет новый реагент в модель"""
reagent = Reagent(name, percentage, unit, conversion_factor)
reagent.dilution_factor = dilution_factor
self.reagents.append(reagent)
def remove_reagent(self, index: int):
"""Удаляет реагент по индексу"""
if 0 <= index < len(self.reagents):
del self.reagents[index]
def clear_reagents(self):
"""Очищает список реагентов"""
self.reagents.clear()
def get_reagent_count(self) -> int:
"""Возвращает количество реагентов в модели"""
return len(self.reagents)
def set_total_amount(self, amount: float, unit: str):
"""Устанавливает общее количество среды и единицу измерения"""
self.total_amount = amount
self.amount_unit = unit
def set_solvent(self, solvent_name: str):
"""Устанавливает название растворителя"""
self.solvent = solvent_name
+7
View File
@@ -0,0 +1,7 @@
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 # коэффициент пересчёта
self.dilution_factor = 1.0 # во сколько раз разбавляем (1 = без разбавления)
+196 -91
View File
@@ -1,134 +1,239 @@
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QTableWidget, QTableWidgetItem, QPushButton,
QLabel, QDoubleSpinBox, QComboBox, QPushButton, QLabel, QDoubleSpinBox, QComboBox, QLineEdit,
QTableWidget, QTableWidgetItem, QLineEdit, QMessageBox QWidget, QMessageBox)
)
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("Калькулятор питательных сред") self.setWindowTitle("Калькулятор питательных сред")
self.setGeometry(100, 100, 800, 600) self.setGeometry(100, 100, 1000, 600)
self._init_ui() self._init_ui()
def _init_ui(self): def _init_ui(self):
"""Инициализирует все виджеты интерфейса"""
central_widget = QWidget() central_widget = QWidget()
self.setCentralWidget(central_widget) self.setCentralWidget(central_widget)
layout = QVBoxLayout()
layout = QVBoxLayout(central_widget) # Верхняя панель: параметры среды
top_layout = QHBoxLayout()
# Секция параметров среды top_layout.addWidget(QLabel("Общее количество:"))
params_layout = QHBoxLayout()
# Общее количество среды
amount_layout = QHBoxLayout()
amount_layout.addWidget(QLabel("Общее количество:"))
self.amount_input = QDoubleSpinBox() self.amount_input = QDoubleSpinBox()
self.amount_input.setRange(0.001, 1000000) self.amount_input.setRange(0.001, 1000000.0)
self.amount_input.setValue(1000.0) self.amount_input.setValue(1000.0)
self.amount_input.setSingleStep(10) top_layout.addWidget(self.amount_input)
amount_layout.addWidget(self.amount_input)
# Единица измерения
self.amount_unit_combo = QComboBox() self.amount_unit_combo = QComboBox()
self.amount_unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"])
self.amount_unit_combo.setCurrentText("мл") self.amount_unit_combo.setCurrentText("мл")
amount_layout.addWidget(self.amount_unit_combo) top_layout.addWidget(self.amount_unit_combo)
# Растворитель top_layout.addWidget(QLabel("Растворитель:"))
solvent_layout = QHBoxLayout() self.solvent_input = QLineEdit("Вода")
solvent_layout.addWidget(QLabel("Растворитель:")) top_layout.addWidget(self.solvent_input)
self.solvent_input = QLineEdit() layout.addLayout(top_layout)
self.solvent_input.setText("Вода")
solvent_layout.addWidget(self.solvent_input)
params_layout.addLayout(amount_layout) # Таблица реагентов (с растворителем в первой строке)
params_layout.addSpacing(20) layout.addWidget(QLabel("Состав среды:"))
params_layout.addLayout(solvent_layout)
params_layout.addStretch()
layout.addLayout(params_layout)
# Таблица реагентов
self.table = QTableWidget() self.table = QTableWidget()
self.table.setColumnCount(5) self.table.setColumnCount(6) # Увеличиваем до 6 колонок
self.table.setHorizontalHeaderLabels([ self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Результат"])
"Название реагента",
"Процент (%)",
"Единица измерения",
"Коэффициент пересчёта",
"Результат"
])
layout.addWidget(self.table) layout.addWidget(self.table)
# Кнопки управления # Кнопки управления
buttons_layout = QHBoxLayout() btn_layout = QHBoxLayout()
self.add_row_btn = QPushButton("Добавить реагент") self.add_row_btn = QPushButton("Добавить реагент")
self.remove_row_btn = QPushButton("Удалить реагент") self.remove_row_btn = QPushButton("Удалить реагент")
self.calculate_btn = QPushButton("Рассчитать") self.calculate_btn = QPushButton("Рассчитать")
self.save_btn = QPushButton("Сохранить") self.save_btn = QPushButton("Сохранить")
self.load_btn = QPushButton("Загрузить") self.load_btn = QPushButton("Загрузить")
buttons_layout.addWidget(self.add_row_btn) btn_layout.addWidget(self.add_row_btn)
buttons_layout.addWidget(self.remove_row_btn) btn_layout.addWidget(self.remove_row_btn)
buttons_layout.addWidget(self.calculate_btn) btn_layout.addWidget(self.calculate_btn)
buttons_layout.addWidget(self.save_btn) btn_layout.addWidget(self.save_btn)
buttons_layout.addWidget(self.load_btn) btn_layout.addWidget(self.load_btn)
buttons_layout.addStretch() layout.addLayout(btn_layout)
layout.addLayout(buttons_layout) central_widget.setLayout(layout)
self.add_initial_rows()
# Добавляем начальную строку def add_initial_rows(self):
self.add_initial_row() """Добавляет начальные строки: растворитель и первый реагент"""
# Добавляем строку растворителя (первая, нередактируемая)
self.add_solvent_row()
# Добавляем строку для первого реагента
self.add_new_row()
def add_initial_row(self): def add_solvent_row(self):
"""Добавляет начальную строку в таблицу""" """Добавляет строку растворителя (нередактируемая)"""
self.table.insertRow(0) row_count = self.table.rowCount()
self.table.setItem(0, 0, QTableWidgetItem("Реагент")) self.table.insertRow(row_count)
self.table.setItem(0, 1, QTableWidgetItem("1.0"))
# Название растворителя (берём из поля ввода)
solvent_name = self.solvent_input.text()
solvent_item = QTableWidgetItem(solvent_name)
solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable)
solvent_item.setBackground(Qt.lightGray)
self.table.setItem(row_count, 0, solvent_item)
# Процент (будет рассчитан автоматически)
percent_item = QTableWidgetItem("")
percent_item.setFlags(percent_item.flags() & ~Qt.ItemIsEditable)
percent_item.setBackground(Qt.lightGray)
self.table.setItem(row_count, 1, percent_item)
# Единица измерения (не используется для растворителя)
unit_item = QTableWidgetItem("-")
unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable)
unit_item.setBackground(Qt.lightGray)
self.table.setItem(row_count, 2, unit_item)
# Коэффициент (не используется для растворителя)
coeff_item = QTableWidgetItem("-")
coeff_item.setFlags(coeff_item.flags() & ~Qt.ItemIsEditable)
coeff_item.setBackground(Qt.lightGray)
self.table.setItem(row_count, 3, coeff_item)
# Разбавление (не используется для растворителя)
dilution_item = QTableWidgetItem("-")
dilution_item.setFlags(dilution_item.flags() & ~Qt.ItemIsEditable)
dilution_item.setBackground(Qt.lightGray)
self.table.setItem(row_count, 4, dilution_item)
# Результат (будет заполнен при расчёте)
result_item = QTableWidgetItem("")
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
result_item.setBackground(Qt.lightGray)
self.table.setItem(row_count, 5, result_item)
def update_solvent_name(self):
"""Обновляет название растворителя в первой строке таблицы"""
solvent_name = self.solvent_input.text()
name_item = self.table.item(0, 0)
if name_item:
name_item.setText(solvent_name)
def add_new_row(self):
"""Добавляет новую строку для реагента"""
row_count = self.table.rowCount()
self.table.insertRow(row_count)
self.table.setItem(row_count, 0, QTableWidgetItem(f"Реагент_{row_count}"))
self.table.setItem(row_count, 1, QTableWidgetItem("0.0"))
unit_combo = QComboBox() unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText("мг") unit_combo.setCurrentText("г")
self.table.setCellWidget(0, 2, unit_combo) self.table.setCellWidget(row_count, 2, unit_combo)
self.table.setItem(0, 3, QTableWidgetItem("1.0")) self.table.setItem(row_count, 3, QTableWidgetItem("1.0"))
self.table.setItem(0, 4, QTableWidgetItem(""))
def set_model(self, model): # Разбавление - обычное текстовое поле
"""Устанавливает связь с моделью""" dilution_edit = QLineEdit("1.0")
self.model = model dilution_edit.setAlignment(Qt.AlignRight)
dilution_edit.setToolTip("Во сколько раз разбавить (1 = без разбавления)")
self.table.setCellWidget(row_count, 4, dilution_edit)
def update_results(self, results: list): self.table.setItem(row_count, 5, QTableWidgetItem(""))
"""Обновляет столбец результатов в таблице"""
for row, amount in enumerate(results):
if row < self.table.rowCount():
# Округление до 4 знаков после запятой
self.table.setItem(row, 4, QTableWidgetItem(f"{amount:.4f}"))
def remove_selected_row(self):
"""Удаляет выделенную строку из таблицы (кроме строки растворителя)"""
selected_rows = set()
for item in self.table.selectedItems():
selected_rows.add(item.row())
# Удаляем строки в обратном порядке, пропуская строку растворителя (индекс 0)
for row in sorted(selected_rows, reverse=True):
if row > 0: # Не удаляем строку растворителя
self.table.removeRow(row)
def get_table_data(self) -> list:
"""Возвращает данные таблицы в виде списка списков (только реагенты, без растворителя)"""
data = []
# Начинаем с 1 строки (пропускаем растворитель)
for row in range(1, self.table.rowCount()):
row_data = []
# Название (колонка 0)
name_item = self.table.item(row, 0)
row_data.append(name_item.text() if name_item else "")
# Процент (колонка 1)
percent_item = self.table.item(row, 1)
row_data.append(percent_item.text() if percent_item else "0")
# Единица измерения (колонка 2 - комбобокс)
unit_widget = self.table.cellWidget(row, 2)
if unit_widget and isinstance(unit_widget, QComboBox):
row_data.append(unit_widget.currentText())
else:
row_data.append("мг")
# Коэффициент (колонка 3)
coeff_item = self.table.item(row, 3)
row_data.append(coeff_item.text() if coeff_item else "1.0")
# Разбавление (колонка 4 - spinbox)
dilution_widget = self.table.cellWidget(row, 4)
if dilution_widget and isinstance(dilution_widget, QDoubleSpinBox):
dilution_factor = dilution_widget.value()
row_data.append(dilution_factor)
else:
row_data.append(1.0)
data.append(row_data)
return data
def update_solvent_percent(self, solvent_percent: float):
"""Обновляет процент растворителя в первой строке"""
percent_item = self.table.item(0, 1)
if percent_item:
percent_item.setText(f"{solvent_percent:.2f}")
def show_error(self, message: str): def show_error(self, message: str):
"""Показывает диалоговое окно с ошибкой""" """Показывает сообщение об ошибке"""
QMessageBox.critical(self, "Ошибка", message) QMessageBox.critical(self, "Ошибка", message)
def get_table_data(self) -> list: def update_results(self, results: list):
"""Возвращает данные из таблицы для отладки""" """Обновляет столбец результатов (индекс 5) в таблице"""
data = [] # Начинаем с 1 строки (реагенты), 0 строка - растворитель
for row in range(self.table.rowCount()): for row, amount in enumerate(results, start=1):
row_data = [] if row < self.table.rowCount():
for col in range(self.table.columnCount() - 1): # не берём столбец результатов formatted_amount = f"{amount:.4f}"
item = self.table.item(row, col) self.table.setItem(row, 5, QTableWidgetItem(formatted_amount))
if item:
row_data.append(item.text()) def update_solvent_result(self, solvent_amount: float, unit: str):
else: """Обновляет результат для растворителя в первой строке"""
widget = self.table.cellWidget(row, col) formatted_amount = f"{solvent_amount:.4f}"
if widget and isinstance(widget, QComboBox): result_item = self.table.item(0, 5)
row_data.append(widget.currentText()) if result_item:
else: result_item.setText(formatted_amount)
row_data.append("")
data.append(row_data) # Также обновляем единицу измерения в колонке 2 для информации
return data unit_item = self.table.item(0, 2)
if unit_item:
unit_item.setText(unit)
def update_display(self, solvent: str, total_amount: float, amount_unit: str):
"""Обновляет отображение растворителя и общего количества среды"""
self.solvent_input.setText(solvent)
self.update_solvent_name()
self.amount_input.setValue(total_amount)
self.amount_unit_combo.setCurrentText(amount_unit)
def clear_results(self):
"""Очищает столбец результатов для всех строк"""
for row in range(self.table.rowCount()):
self.table.setItem(row, 5, QTableWidgetItem(""))
# Очищаем процент растворителя
percent_item = self.table.item(0, 1)
if percent_item:
percent_item.setText("")
# Очищаем единицу измерения растворителя
unit_item = self.table.item(0, 2)
if unit_item:
unit_item.setText("-")