Реализация функционала полнофакторного эксперимента

This commit is contained in:
2026-05-06 23:10:55 +05:00
parent 361b934e8a
commit 15193d2403
5 changed files with 751 additions and 212 deletions
+290 -107
View File
@@ -1,239 +1,422 @@
from PyQt5.QtWidgets import (QMainWindow, QVBoxLayout, QHBoxLayout,
QTableWidget, QTableWidgetItem, QPushButton,
QLabel, QDoubleSpinBox, QComboBox, QLineEdit,
QWidget, QMessageBox)
QWidget, QMessageBox, QGroupBox, QFrame, QHeaderView)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QColor
class MainWindow(QMainWindow):
class MediumCalculatorWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Калькулятор питательных сред")
self.setGeometry(100, 100, 1000, 600)
self.setWindowTitle("Калькулятор питательных сред - Цифровой помощник биохимика")
self.setGeometry(200, 100, 1200, 700)
self.setStyleSheet("""
QMainWindow {
background-color: #f5f5f5;
}
QGroupBox {
font-weight: bold;
border: 2px solid #ccc;
border-radius: 5px;
margin-top: 10px;
padding-top: 10px;
background-color: white;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #1565C0;
}
QTableWidget {
gridline-color: #ddd;
background-color: white;
alternate-background-color: #f9f9f9;
}
QHeaderView::section {
background-color: #1565C0;
color: white;
padding: 8px;
border: none;
font-weight: bold;
}
QPushButton {
background-color: #2196F3;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
font-weight: bold;
}
QPushButton:hover {
background-color: #1976D2;
}
QPushButton:pressed {
background-color: #0D47A1;
}
QPushButton#danger {
background-color: #f44336;
}
QPushButton#danger:hover {
background-color: #da190b;
}
QPushButton#success {
background-color: #4CAF50;
}
QPushButton#success:hover {
background-color: #45a049;
}
QDoubleSpinBox, QLineEdit {
padding: 4px;
border: 1px solid #ccc;
border-radius: 3px;
background-color: white;
color: black;
font-size: 12px;
min-height: 20px;
}
QComboBox {
border: 1px solid #ccc;
border-radius: 3px;
background-color: white;
color: black;
font-size: 12px;
min-height: 20px;
padding: 2px;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox QAbstractItemView {
background-color: white;
color: black;
selection-background-color: #2196F3;
selection-color: white;
}
QLabel {
color: black;
font-size: 12px;
}
QLabel#title {
font-size: 18px;
font-weight: bold;
color: #0D47A1;
}
QLabel#info {
color: #1565C0;
font-size: 11px;
}
""")
self._init_ui()
def _init_ui(self):
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
layout = QVBoxLayout(central_widget)
layout.setSpacing(15)
layout.setContentsMargins(20, 20, 20, 20)
# Верхняя панель: параметры среды
top_layout = QHBoxLayout()
top_layout.addWidget(QLabel("Общее количество:"))
title_label = QLabel("Калькулятор питательных сред")
title_label.setObjectName("title")
title_label.setAlignment(Qt.AlignCenter)
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
layout.addWidget(title_label)
params_group = QGroupBox("Параметры среды")
params_layout = QHBoxLayout()
params_layout.setSpacing(20)
amount_layout = QHBoxLayout()
amount_layout.addWidget(QLabel("Общее количество:"))
self.amount_input = QDoubleSpinBox()
self.amount_input.setRange(0.001, 1000000.0)
self.amount_input.setValue(1000.0)
top_layout.addWidget(self.amount_input)
self.amount_input.setMinimumWidth(150)
amount_layout.addWidget(self.amount_input)
self.amount_unit_combo = QComboBox()
self.amount_unit_combo.addItems(["нл", "мкл", "мл", "л"])
self.amount_unit_combo.setCurrentText("мл")
top_layout.addWidget(self.amount_unit_combo)
self.amount_unit_combo.setMinimumWidth(80)
amount_layout.addWidget(self.amount_unit_combo)
params_layout.addLayout(amount_layout)
top_layout.addWidget(QLabel("Растворитель:"))
solvent_layout = QHBoxLayout()
solvent_layout.addWidget(QLabel("Растворитель:"))
self.solvent_input = QLineEdit("Вода")
top_layout.addWidget(self.solvent_input)
layout.addLayout(top_layout)
self.solvent_input.setMinimumWidth(150)
solvent_layout.addWidget(self.solvent_input)
params_layout.addLayout(solvent_layout)
# Таблица реагентов (с растворителем в первой строке)
layout.addWidget(QLabel("Состав среды:"))
params_layout.addStretch()
params_group.setLayout(params_layout)
layout.addWidget(params_group)
table_group = QGroupBox("Состав среды")
table_layout = QVBoxLayout()
self.table = QTableWidget()
self.table.setColumnCount(6) # Увеличиваем до 6 колонок
self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Результат"])
layout.addWidget(self.table)
self.table.setColumnCount(6)
self.table.setHorizontalHeaderLabels(["Название", "%", "Единица", "Коэфф.", "Разбавление (x)", "Количество"])
self.table.setAlternatingRowColors(True)
self.table.setSelectionBehavior(QTableWidget.SelectRows)
self.table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed)
self.table.verticalHeader().setVisible(False)
self.table.setColumnWidth(0, 180)
self.table.setColumnWidth(1, 70)
self.table.setColumnWidth(2, 90)
self.table.setColumnWidth(3, 70)
self.table.setColumnWidth(4, 100)
self.table.setColumnWidth(5, 120)
table_layout.addWidget(self.table)
table_group.setLayout(table_layout)
layout.addWidget(table_group)
# Кнопки управления
btn_group = QGroupBox("Управление")
btn_layout = QHBoxLayout()
btn_layout.setSpacing(10)
self.add_row_btn = QPushButton("Добавить реагент")
self.remove_row_btn = QPushButton("Удалить реагент")
self.calculate_btn = QPushButton("Рассчитать")
self.save_btn = QPushButton("Сохранить")
self.load_btn = QPushButton("Загрузить")
self.add_row_btn.setMinimumWidth(150)
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)
central_widget.setLayout(layout)
self.remove_row_btn = QPushButton("Удалить реагент")
self.remove_row_btn.setObjectName("danger")
self.remove_row_btn.setMinimumWidth(150)
btn_layout.addWidget(self.remove_row_btn)
btn_layout.addStretch()
self.calculate_btn = QPushButton("Рассчитать")
self.calculate_btn.setObjectName("success")
self.calculate_btn.setMinimumWidth(150)
btn_layout.addWidget(self.calculate_btn)
self.save_btn = QPushButton("Сохранить")
self.save_btn.setMinimumWidth(150)
btn_layout.addWidget(self.save_btn)
self.load_btn = QPushButton("Загрузить")
self.load_btn.setMinimumWidth(150)
btn_layout.addWidget(self.load_btn)
btn_group.setLayout(btn_layout)
layout.addWidget(btn_group)
info_frame = QFrame()
info_frame.setFrameShape(QFrame.StyledPanel)
info_frame.setStyleSheet("background-color: #e3f2fd; border-radius: 5px;")
info_layout = QHBoxLayout(info_frame)
info_label = QLabel("Подсказка: Реагенты в массовых единицах (г, мг и т.д.) не учитываются при расчёте объёма растворителя")
info_label.setObjectName("info")
info_layout.addWidget(info_label)
info_layout.addStretch()
layout.addWidget(info_frame)
self.add_initial_rows()
def add_initial_rows(self):
"""Добавляет начальные строки: растворитель и первый реагент"""
# Добавляем строку растворителя (первая, нередактируемая)
self.add_solvent_row()
# Добавляем строку для первого реагента
self.add_new_row()
def add_solvent_row(self):
"""Добавляет строку растворителя (нередактируемая)"""
row_count = self.table.rowCount()
self.table.insertRow(row_count)
# Название растворителя (берём из поля ввода)
self.table.setRowHeight(row_count, 30)
solvent_name = self.solvent_input.text()
solvent_item = QTableWidgetItem(solvent_name)
solvent_item.setFlags(solvent_item.flags() & ~Qt.ItemIsEditable)
solvent_item.setBackground(Qt.lightGray)
solvent_item.setBackground(QColor(230, 230, 230))
solvent_item.setForeground(QColor(0, 0, 0))
font = QFont()
font.setBold(True)
solvent_item.setFont(font)
self.table.setItem(row_count, 0, solvent_item)
# Процент (будет рассчитан автоматически)
percent_item = QTableWidgetItem("")
percent_item.setFlags(percent_item.flags() & ~Qt.ItemIsEditable)
percent_item.setBackground(Qt.lightGray)
percent_item.setBackground(QColor(230, 230, 230))
percent_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 1, percent_item)
# Единица измерения (не используется для растворителя)
unit_item = QTableWidgetItem("-")
unit_item = QTableWidgetItem(self.amount_unit_combo.currentText())
unit_item.setFlags(unit_item.flags() & ~Qt.ItemIsEditable)
unit_item.setBackground(Qt.lightGray)
unit_item.setBackground(QColor(230, 230, 230))
unit_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 2, unit_item)
# Коэффициент (не используется для растворителя)
coeff_item = QTableWidgetItem("-")
coeff_item.setFlags(coeff_item.flags() & ~Qt.ItemIsEditable)
coeff_item.setBackground(Qt.lightGray)
coeff_item.setBackground(QColor(230, 230, 230))
coeff_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 3, coeff_item)
# Разбавление (не используется для растворителя)
dilution_item = QTableWidgetItem("-")
dilution_item.setFlags(dilution_item.flags() & ~Qt.ItemIsEditable)
dilution_item.setBackground(Qt.lightGray)
dilution_item.setBackground(QColor(230, 230, 230))
dilution_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 4, dilution_item)
# Результат (будет заполнен при расчёте)
result_item = QTableWidgetItem("")
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
result_item.setBackground(Qt.lightGray)
result_item.setBackground(QColor(240, 240, 240))
result_item.setForeground(QColor(0, 0, 0))
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 format_number(self, value):
if value == int(value):
return str(int(value))
else:
formatted = f"{value:.6f}".rstrip('0').rstrip('.')
if '.' in formatted and len(formatted.split('.')[1]) > 4:
formatted = f"{value:.4f}".rstrip('0').rstrip('.')
return formatted
def add_new_row(self):
"""Добавляет новую строку для реагента"""
row_count = self.table.rowCount()
self.table.insertRow(row_count)
self.table.setRowHeight(row_count, 30)
self.table.setItem(row_count, 0, QTableWidgetItem(f"Реагент_{row_count}"))
self.table.setItem(row_count, 1, QTableWidgetItem("0.0"))
name_item = QTableWidgetItem(f"Реагент_{row_count}")
name_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 0, name_item)
percent_item = QTableWidgetItem("0")
percent_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 1, percent_item)
unit_combo = QComboBox()
unit_combo.addItems(["нг", "мкг", "мг", "г", "кг", "нл", "мкл", "мл", "л"])
unit_combo.setCurrentText("г")
unit_combo.setCurrentText("мг")
self.table.setCellWidget(row_count, 2, unit_combo)
self.table.setItem(row_count, 3, QTableWidgetItem("1.0"))
# Разбавление - обычное текстовое поле
dilution_edit = QLineEdit("1.0")
dilution_edit.setAlignment(Qt.AlignRight)
dilution_edit.setToolTip("Во сколько раз разбавить (1 = без разбавления)")
self.table.setCellWidget(row_count, 4, dilution_edit)
self.table.setItem(row_count, 5, QTableWidgetItem(""))
coeff_item = QTableWidgetItem("1")
coeff_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 3, coeff_item)
dilution_item = QTableWidgetItem("1")
dilution_item.setForeground(QColor(0, 0, 0))
dilution_item.setFlags(dilution_item.flags() | Qt.ItemIsEditable)
self.table.setItem(row_count, 4, dilution_item)
result_item = QTableWidgetItem("")
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
result_item.setBackground(QColor(250, 250, 250))
result_item.setForeground(QColor(0, 0, 0))
self.table.setItem(row_count, 5, result_item)
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: # Не удаляем строку растворителя
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(coeff_item.text() if coeff_item else "1")
dilution_item = self.table.item(row, 4)
if dilution_item:
try:
dilution_factor = float(dilution_item.text())
except ValueError:
dilution_factor = 1.0
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}")
percent_item.setText(self.format_number(solvent_percent))
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))
formatted_amount = self.format_number(amount)
result_item = QTableWidgetItem(formatted_amount)
result_item.setFlags(result_item.flags() & ~Qt.ItemIsEditable)
result_item.setBackground(QColor(220, 255, 220))
result_item.setForeground(QColor(0, 0, 0))
font = QFont()
font.setBold(True)
result_item.setFont(font)
self.table.setItem(row, 5, result_item)
def update_solvent_result(self, solvent_amount: float, unit: str):
"""Обновляет результат для растворителя в первой строке"""
formatted_amount = f"{solvent_amount:.4f}"
formatted_amount = self.format_number(solvent_amount)
result_item = self.table.item(0, 5)
if result_item:
result_item.setText(formatted_amount)
# Также обновляем единицу измерения в колонке 2 для информации
result_item.setBackground(QColor(220, 255, 220))
result_item.setForeground(QColor(0, 0, 0))
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(""))
# Очищаем процент растворителя
result_item = self.table.item(row, 5)
if result_item:
result_item.setText("")
if row == 0:
result_item.setBackground(QColor(230, 230, 230))
else:
result_item.setBackground(QColor(250, 250, 250))
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("-")
unit_item.setText(self.amount_unit_combo.currentText())