From 6798fd5f63bab16e97f0f5d04dc5a57fbb90173e Mon Sep 17 00:00:00 2001 From: Artemiy Date: Tue, 5 May 2026 22:31:08 +0500 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F:=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D1=81=D1=82=D0=BE=D0=BB=D0=B1=D0=B5=D1=86?= =?UTF-8?q?=20'=D0=A0=D0=B0=D0=B7=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20(x)'=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=B0=D0=B6?= =?UTF-8?q?=D0=B4=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B5=D0=B0=D0=B3=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=20-=20=D0=9F=D0=B5=D1=80=D0=B5=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=80=D0=B0=D1=81=D1=87=D1=91=D1=82=D0=B0=20=D1=81=20?= =?UTF-8?q?=D1=83=D1=87=D1=91=D1=82=D0=BE=D0=BC=20=D1=80=D0=B0=D0=B7=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20-=20=D0=98=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D1=87=D1=91=D1=82=20=D0=BA=D0=BE=D0=BB=D0=B8=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B0=20=D1=80=D0=B0=D1=81=D1=82=D0=B2=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=82=D0=B5=D0=BB=D1=8F=20-=20=D0=A0=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=BE=D1=80=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=B2=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE=D0=B9=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=D0=B5=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= =?UTF-8?q?=D1=8B=20-=20=D0=9F=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B4=D1=80=D0=BE=D0=B1=D0=BD=D1=8B=D1=85=20=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B9=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller.py | 181 ++++++++++++++++++++----------- main.py | 29 ++--- model.py | 240 +++++++++++++++++++++++------------------ reagent.py | 7 ++ view.py | 287 ++++++++++++++++++++++++++++++++++---------------- 5 files changed, 467 insertions(+), 277 deletions(-) create mode 100644 reagent.py diff --git a/controller.py b/controller.py index 243246a..a9b965d 100644 --- a/controller.py +++ b/controller.py @@ -1,100 +1,120 @@ -from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QComboBox -from model import Reagent +from PyQt5.QtWidgets import QMessageBox, QFileDialog, QTableWidgetItem, QComboBox, QDoubleSpinBox, QLineEdit +from PyQt5.QtCore import Qt +from model import Model +from view import MainWindow +import json +from reagent import Reagent + class Controller: - def __init__(self, view, model): - self.view = view - self.model = model - self.setup_connections() + def __init__(self): + self.model = Model() + self.view = MainWindow() + self._connect_signals() - - def setup_connections(self): - """Подключает сигналы виджетов к методам контроллера""" + def _connect_signals(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.calculate_btn.clicked.connect(self._perform_calculation) 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) + # Подключаем обновление названия растворителя в таблице при изменении поля + self.view.solvent_input.textChanged.connect(self.view.update_solvent_name) 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("")) + """Добавляет новую строку для реагента""" + self.view.add_new_row() 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] + """Удаляет выбранную строку реагента""" + self.view.remove_selected_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): - """Выполняет расчёт количеств реагентов и растворителя""" - 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)}") - + # Обновляем информацию о растворителе в первой строке таблицы + self.view.update_solvent_result(solvent_amount, self.model.amount_unit) + self.view.update_solvent_percent(solvent_percentage) + 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()): + # Заполняем реагенты из таблицы (начиная с 1 строки, пропуская растворитель) + for row in range(1, 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) + dilution_widget = self.view.table.cellWidget(row, 4) + # Пропускаем строку, если какие-то обязательные поля отсутствуют 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()) + try: + name = name_item.text() + percentage = float(percentage_item.text()) + unit = unit_widget.currentText() if unit_widget else "мг" + conversion_factor = float(conversion_item.text()) - reagent = Reagent(name, percentage, unit, conversion_factor) - self.model.add_reagent(reagent) + # Получаем коэффициент разбавления (поддерживаем QDoubleSpinBox и QLineEdit) + 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): """Обновляет интерфейс данными из модели""" - 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) 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) + # Устанавливаем название растворителя и обновляем его в таблице + self.view.solvent_input.setText(self.model.solvent) + self.view.update_solvent_name() + + # Заполняем таблицу реагентами из модели 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}")) @@ -102,21 +122,34 @@ class Controller: 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("")) + 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): - """Сохраняет состав среды в JSON‑файл""" + """Сохраняет состав среды в 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) @@ -125,18 +158,38 @@ class Controller: self.view.show_error(f"Ошибка сохранения: {str(e)}") def load_composition(self): - """Загружает состав среды из JSON‑файла""" + """Загружает состав среды из 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, "Успех", "Состав среды успешно загружен!") + self._update_view_from_model() + QMessageBox.information( + 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: - self.view.show_error(f"Ошибка загрузки: {str(e)}") + QMessageBox.critical( + self.view, + "Ошибка", + f"Ошибка при загрузке состава: {str(e)}" + ) diff --git a/main.py b/main.py index ee01129..91576e5 100755 --- a/main.py +++ b/main.py @@ -1,34 +1,19 @@ +# main.py 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) # Контроллер, связывающий модель и представление + # Создаём контроллер - он сам создаст модель и представление + controller = Controller() - # Связываем модель с представлением — критически важный шаг для устранения ошибки «Модель не установлена» - view.set_model(model) + # Показываем главное окно + controller.view.show() - # Настраиваем контроллер (подключаем обработчики событий к кнопкам) - controller.setup_connections() - - - # Отображаем главное окно приложения - view.show() - - # Запускаем главный цикл обработки событий Qt и ожидаем завершения приложения + # Запускаем цикл обработки событий sys.exit(app.exec_()) - - -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/model.py b/model.py index 19f30ab..f66bdd4 100644 --- a/model.py +++ b/model.py @@ -1,150 +1,190 @@ import json - +from reagent import Reagent VOLUME_UNITS = { 'нл': 0.001, # 1 нл = 0.001 мкл 'мкл': 1.0, # базовая единица объёма - 'мл': 1000.0, # 1 мл = 1000 мкл - 'л': 1000000.0 # 1 л = 1 000 000 мкл + 'мл': 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 мкг + 'мкг': 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: + self.total_amount = 100.0 # общее количество среды (по умолчанию — 1000 мл) + self.amount_unit = 'мл' # единица измерения общего количества + self.solvent = 'Вода' # растворитель по умолчанию + self.reagents = [] # список реагентов + + 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: 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 + return amount_base / conversion_factor - def add_reagent(self, reagent: Reagent): - """Добавляет реагент в список""" - self.reagents.append(reagent) - - def calculate_amounts(self) -> list: + def calculate_amounts(self) -> tuple: """ - Рассчитывает абсолютное количество каждого реагента с учётом единиц измерения. - Возвращает список количеств в единицах измерения реагента. + Рассчитывает: + 1. Исходное количество реагента для неразбавленного состава + 2. Количество разбавленного реагента, которое нужно взять + 3. Количество растворителя с учётом добавленных разбавленных реагентов + + Возвращает: (список количеств разбавленных реагентов, количество растворителя, процент растворителя) """ 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: - # Базовый расчёт в процентах от общего количества - 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_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) + # Если реагент в объёмных единицах, его объём = diluted_amount + reagent_volume_base = diluted_amount * VOLUME_UNITS[reagent.unit] else: - # Если единица неизвестна, возвращаем базовый расчёт - final_amount = base_amount + # Если реагент в массовых единицах, считаем, что он вносит пренебрежимый объём + # или можно добавить коэффициент плотности, пока игнорируем + reagent_volume_base = 0 - results.append(final_amount) - return results + total_diluted_volume_base += reagent_volume_base + # Шаг 4: Рассчитываем количество растворителя + # Растворитель = общий объём - объём всех разбавленных реагентов + solvent_volume_base = total_in_base - total_diluted_volume_base - 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 - } + # Переводим количество растворителя в целевую единицу измерения + solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit] + + # Если объём реагентов превышает общий объём, корректируем + if solvent_amount < 0: + solvent_amount = 0 + + # Процент растворителя в неразбавленном составе + solvent_percentage = 100 - total_percentage + + return diluted_amounts, solvent_amount, solvent_percentage def save_to_file(self, filename: str): - """Сохраняет состав среды в JSON‑файл""" + """Сохраняет модель в JSON-файл""" data = { 'total_amount': self.total_amount, 'amount_unit': self.amount_unit, '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: - 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): - """Загружает состав среды из JSON‑файла""" + """Загружает модель из 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) + + 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) + + 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 diff --git a/reagent.py b/reagent.py new file mode 100644 index 0000000..cd8a384 --- /dev/null +++ b/reagent.py @@ -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 = без разбавления) diff --git a/view.py b/view.py index aa652a2..757a206 100644 --- a/view.py +++ b/view.py @@ -1,134 +1,239 @@ -from PyQt5.QtWidgets import ( - QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, - QLabel, QDoubleSpinBox, QComboBox, QPushButton, - QTableWidget, QTableWidgetItem, QLineEdit, QMessageBox -) +from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout, + QTableWidget, QTableWidgetItem, QPushButton, + QLabel, QDoubleSpinBox, QComboBox, QLineEdit, + QWidget, QMessageBox) from PyQt5.QtCore import Qt - class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Калькулятор питательных сред") - self.setGeometry(100, 100, 800, 600) + self.setGeometry(100, 100, 1000, 600) self._init_ui() def _init_ui(self): - """Инициализирует все виджеты интерфейса""" central_widget = QWidget() self.setCentralWidget(central_widget) + layout = QVBoxLayout() - layout = QVBoxLayout(central_widget) - - # Секция параметров среды - params_layout = QHBoxLayout() - - # Общее количество среды - amount_layout = QHBoxLayout() - amount_layout.addWidget(QLabel("Общее количество:")) + # Верхняя панель: параметры среды + top_layout = QHBoxLayout() + top_layout.addWidget(QLabel("Общее количество:")) 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.setSingleStep(10) - amount_layout.addWidget(self.amount_input) + top_layout.addWidget(self.amount_input) - # Единица измерения self.amount_unit_combo = QComboBox() - self.amount_unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) + self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"]) self.amount_unit_combo.setCurrentText("мл") - amount_layout.addWidget(self.amount_unit_combo) + top_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) + top_layout.addWidget(QLabel("Растворитель:")) + self.solvent_input = QLineEdit("Вода") + top_layout.addWidget(self.solvent_input) + layout.addLayout(top_layout) - params_layout.addLayout(amount_layout) - params_layout.addSpacing(20) - params_layout.addLayout(solvent_layout) - params_layout.addStretch() - - layout.addLayout(params_layout) - - # Таблица реагентов + # Таблица реагентов (с растворителем в первой строке) + layout.addWidget(QLabel("Состав среды:")) self.table = QTableWidget() - self.table.setColumnCount(5) - self.table.setHorizontalHeaderLabels([ - "Название реагента", - "Процент (%)", - "Единица измерения", - "Коэффициент пересчёта", - "Результат" - ]) + self.table.setColumnCount(6) # Увеличиваем до 6 колонок + self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Результат"]) layout.addWidget(self.table) # Кнопки управления - buttons_layout = QHBoxLayout() - + btn_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() + btn_layout.addWidget(self.add_row_btn) + btn_layout.addWidget(self.remove_row_btn) + btn_layout.addWidget(self.calculate_btn) + btn_layout.addWidget(self.save_btn) + btn_layout.addWidget(self.load_btn) + layout.addLayout(btn_layout) - layout.addLayout(buttons_layout) + central_widget.setLayout(layout) + self.add_initial_rows() - # Добавляем начальную строку - self.add_initial_row() + def add_initial_rows(self): + """Добавляет начальные строки: растворитель и первый реагент""" + # Добавляем строку растворителя (первая, нередактируемая) + self.add_solvent_row() + # Добавляем строку для первого реагента + self.add_new_row() - def add_initial_row(self): - """Добавляет начальную строку в таблицу""" - self.table.insertRow(0) - self.table.setItem(0, 0, QTableWidgetItem("Реагент")) - self.table.setItem(0, 1, QTableWidgetItem("1.0")) + def add_solvent_row(self): + """Добавляет строку растворителя (нередактируемая)""" + row_count = self.table.rowCount() + self.table.insertRow(row_count) + + # Название растворителя (берём из поля ввода) + 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.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"]) - unit_combo.setCurrentText("мг") - self.table.setCellWidget(0, 2, unit_combo) + unit_combo.setCurrentText("г") + self.table.setCellWidget(row_count, 2, unit_combo) - self.table.setItem(0, 3, QTableWidgetItem("1.0")) - self.table.setItem(0, 4, QTableWidgetItem("")) + self.table.setItem(row_count, 3, QTableWidgetItem("1.0")) - def set_model(self, model): - """Устанавливает связь с моделью""" - self.model = model + # Разбавление - обычное текстовое поле + dilution_edit = QLineEdit("1.0") + dilution_edit.setAlignment(Qt.AlignRight) + dilution_edit.setToolTip("Во сколько раз разбавить (1 = без разбавления)") + self.table.setCellWidget(row_count, 4, dilution_edit) - 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}")) + self.table.setItem(row_count, 5, QTableWidgetItem("")) + 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): - """Показывает диалоговое окно с ошибкой""" + """Показывает сообщение об ошибке""" 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 + def update_results(self, results: list): + """Обновляет столбец результатов (индекс 5) в таблице""" + # Начинаем с 1 строки (реагенты), 0 строка - растворитель + for row, amount in enumerate(results, start=1): + if row < self.table.rowCount(): + formatted_amount = f"{amount:.4f}" + self.table.setItem(row, 5, QTableWidgetItem(formatted_amount)) + + def update_solvent_result(self, solvent_amount: float, unit: str): + """Обновляет результат для растворителя в первой строке""" + formatted_amount = f"{solvent_amount:.4f}" + result_item = self.table.item(0, 5) + if result_item: + result_item.setText(formatted_amount) + + # Также обновляем единицу измерения в колонке 2 для информации + 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("-")