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

- Добавлен столбец 'Разбавление (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
+109 -56
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):
def calculate(self): """Выполняет расчёт и обновляет интерфейс"""
"""Выполняет расчёт количеств реагентов и растворителя"""
try: try:
self._update_model_from_view() self._update_model_from_view()
results = self.model.calculate_amounts() results, solvent_amount, solvent_percentage = self.model.calculate_amounts()
self.view.update_results(results) self.view.update_results(results)
# Обновляем информацию о растворителе в первой строке таблицы
self.view.update_solvent_result(solvent_amount, self.model.amount_unit)
self.view.update_solvent_percent(solvent_percentage)
except ValueError as e: except ValueError as e:
self.view.show_error(f"Ошибка в данных: {str(e)}") self.view.show_error(f"Ошибка в данных: {str(e)}")
except Exception as e: except Exception as e:
self.view.show_error(f"Неожиданная ошибка: {str(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
try:
name = name_item.text() name = name_item.text()
percentage = float(percentage_item.text()) percentage = float(percentage_item.text())
unit = unit_widget.currentText() unit = unit_widget.currentText() if unit_widget else "мг"
conversion_factor = float(conversion_item.text()) 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()
+133 -93
View File
@@ -1,5 +1,5 @@
import json import json
from reagent import Reagent
VOLUME_UNITS = { VOLUME_UNITS = {
'нл': 0.001, # 1 нл = 0.001 мкл 'нл': 0.001, # 1 нл = 0.001 мкл
@@ -16,135 +16,175 @@ MASS_UNITS = {
'кг': 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: else:
total_in_mkg = self.total_amount * MASS_UNITS[self.amount_unit] # Если реагент в массовых единицах, считаем, что он вносит пренебрежимый объём
# или можно добавить коэффициент плотности, пока игнорируем
reagent_volume_base = 0
# Расчёт в мкг total_diluted_volume_base += reagent_volume_base
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) # Шаг 4: Рассчитываем количество растворителя
return results # Растворитель = общий объём - объём всех разбавленных реагентов
solvent_volume_base = total_in_base - total_diluted_volume_base
# Переводим количество растворителя в целевую единицу измерения
solvent_amount = solvent_volume_base / VOLUME_UNITS[self.amount_unit]
def get_solvent_info(self) -> dict: # Если объём реагентов превышает общий объём, корректируем
"""Возвращает информацию о растворителе: процент и количество""" if solvent_amount < 0:
total_percentage = sum(r.percentage for r in self.reagents) solvent_amount = 0
solvent_percentage = max(0, 100 - total_percentage) # не меньше 0%
solvent_amount = (solvent_percentage / 100) * self.total_amount # Процент растворителя в неразбавленном составе
return { solvent_percentage = 100 - total_percentage
'percentage': solvent_percentage,
'amount': solvent_amount, return diluted_amounts, solvent_amount, solvent_percentage
'unit': self.amount_unit
}
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 = без разбавления)
+191 -86
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())
def show_error(self, message: str): # Удаляем строки в обратном порядке, пропуская строку растворителя (индекс 0)
"""Показывает диалоговое окно с ошибкой""" for row in sorted(selected_rows, reverse=True):
QMessageBox.critical(self, "Ошибка", message) if row > 0: # Не удаляем строку растворителя
self.table.removeRow(row)
def get_table_data(self) -> list: def get_table_data(self) -> list:
"""Возвращает данные из таблицы для отладки""" """Возвращает данные таблицы в виде списка списков (только реагенты, без растворителя)"""
data = [] data = []
for row in range(self.table.rowCount()): # Начинаем с 1 строки (пропускаем растворитель)
for row in range(1, self.table.rowCount()):
row_data = [] row_data = []
for col in range(self.table.columnCount() - 1): # не берём столбец результатов
item = self.table.item(row, col) # Название (колонка 0)
if item: name_item = self.table.item(row, 0)
row_data.append(item.text()) 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: else:
widget = self.table.cellWidget(row, col) row_data.append("мг")
if widget and isinstance(widget, QComboBox):
row_data.append(widget.currentText()) # Коэффициент (колонка 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: else:
row_data.append("") row_data.append(1.0)
data.append(row_data) data.append(row_data)
return 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 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("-")