from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit ) from PySide6.QtCore import Signal, Qt from PySide6.QtGui import QRegularExpressionValidator from PySide6.QtCore import QRegularExpression class NumberLineEdit(QLineEdit): """Специализированное поле для ввода только чисел""" def __init__(self, max_length: int, parent=None): super().__init__(parent) self.setMaxLength(max_length) # Создаем валидатор для цифр regex = QRegularExpression("[0-9]*") validator = QRegularExpressionValidator(regex) self.setValidator(validator) class SerialNumberWidget(QWidget): """Виджет для ввода серийного номера (только цифры)""" valueChanged = Signal() def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() layout.setSpacing(5) # Описание формата desc_label = QLabel("Формат: Axxx/Dxxxxxxxx/Vxxx/Nxxx (только цифры)") layout.addWidget(desc_label) # Контейнер для полей ввода fields_container = QWidget() fields_layout = QVBoxLayout(fields_container) fields_layout.setSpacing(2) fields_layout.setContentsMargins(0, 0, 0, 0) # Верхний ряд - метки полей labels_layout = QHBoxLayout() labels_layout.setSpacing(2) labels_layout.addSpacing(10) label_a = QLabel("A") label_a.setFixedWidth(40) label_a.setAlignment(Qt.AlignCenter) labels_layout.addWidget(label_a) labels_layout.addSpacing(30) label_d = QLabel("D") label_d.setFixedWidth(70) label_d.setAlignment(Qt.AlignCenter) labels_layout.addWidget(label_d) labels_layout.addSpacing(30) label_v = QLabel("V") label_v.setFixedWidth(40) label_v.setAlignment(Qt.AlignCenter) labels_layout.addWidget(label_v) labels_layout.addSpacing(30) label_n = QLabel("N") label_n.setFixedWidth(40) label_n.setAlignment(Qt.AlignCenter) labels_layout.addWidget(label_n) labels_layout.addStretch() fields_layout.addLayout(labels_layout) # Ряд с полями ввода input_layout = QHBoxLayout() input_layout.setSpacing(2) input_layout.addSpacing(10) # Поле A self.part_a = NumberLineEdit(3) self.part_a.setFixedWidth(40) self.part_a.setPlaceholderText("xxx") self.part_a.setAlignment(Qt.AlignCenter) self.part_a.textChanged.connect(self.validate_and_update) self.part_a.returnPressed.connect(lambda: self.focus_next_field(self.part_a, self.part_d)) input_layout.addWidget(self.part_a) input_layout.addSpacing(30) # Поле D self.part_d = NumberLineEdit(8) self.part_d.setFixedWidth(70) self.part_d.setPlaceholderText("xxxxxxxx") self.part_d.setAlignment(Qt.AlignCenter) self.part_d.textChanged.connect(self.validate_and_update) self.part_d.returnPressed.connect(lambda: self.focus_next_field(self.part_d, self.part_v)) input_layout.addWidget(self.part_d) input_layout.addSpacing(30) # Поле V self.part_v = NumberLineEdit(3) self.part_v.setFixedWidth(40) self.part_v.setPlaceholderText("xxx") self.part_v.setAlignment(Qt.AlignCenter) self.part_v.textChanged.connect(self.validate_and_update) self.part_v.returnPressed.connect(lambda: self.focus_next_field(self.part_v, self.part_n)) input_layout.addWidget(self.part_v) input_layout.addSpacing(30) # Поле N self.part_n = NumberLineEdit(3) self.part_n.setFixedWidth(40) self.part_n.setPlaceholderText("xxx") self.part_n.setAlignment(Qt.AlignCenter) self.part_n.textChanged.connect(self.validate_and_update) self.part_n.returnPressed.connect(lambda: self.focus_next_field(self.part_n, None)) input_layout.addWidget(self.part_n) input_layout.addStretch() fields_layout.addLayout(input_layout) # Нижний ряд - индикаторы заполнения indicators_layout = QHBoxLayout() indicators_layout.setSpacing(2) indicators_layout.addSpacing(10) self.indicator_a = QLabel("0/3") self.indicator_a.setFixedWidth(40) self.indicator_a.setAlignment(Qt.AlignCenter) indicators_layout.addWidget(self.indicator_a) indicators_layout.addSpacing(30) self.indicator_d = QLabel("0/8") self.indicator_d.setFixedWidth(70) self.indicator_d.setAlignment(Qt.AlignCenter) indicators_layout.addWidget(self.indicator_d) indicators_layout.addSpacing(30) self.indicator_v = QLabel("0/3") self.indicator_v.setFixedWidth(40) self.indicator_v.setAlignment(Qt.AlignCenter) indicators_layout.addWidget(self.indicator_v) indicators_layout.addSpacing(30) self.indicator_n = QLabel("0/3") self.indicator_n.setFixedWidth(40) self.indicator_n.setAlignment(Qt.AlignCenter) indicators_layout.addWidget(self.indicator_n) indicators_layout.addStretch() fields_layout.addLayout(indicators_layout) layout.addWidget(fields_container) # Предпросмотр с темным стилем preview_label = QLabel("Результирующий серийный номер:") preview_label.setStyleSheet("font-weight: bold; margin-top: 15px;") layout.addWidget(preview_label) self.preview = QLabel() self.preview.setStyleSheet(""" QLabel { font-family: monospace; font-size: 16px; padding: 12px; background-color: #2b2b2b; color: #00ff00; border: 2px solid #555; border-radius: 5px; font-weight: bold; } """) self.preview.setAlignment(Qt.AlignCenter) layout.addWidget(self.preview) # Статус заполнения (без стилей) self.status_label = QLabel() self.status_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.status_label) layout.addStretch() self.setLayout(layout) # Первоначальное обновление self.update_preview() def focus_next_field(self, current_field, next_field): """Перемещает фокус на следующее поле при нажатии Enter""" if next_field: next_field.setFocus() def validate_and_update(self): """Проверяет ввод и обновляет предпросмотр""" # Автоматический переход при полном заполнении поля sender = self.sender() if sender: text = sender.text() max_length = sender.maxLength() if len(text) == max_length: if sender == self.part_a: self.part_d.setFocus() elif sender == self.part_d: self.part_v.setFocus() elif sender == self.part_v: self.part_n.setFocus() elif sender == self.part_n: self.part_n.clearFocus() self.update_preview() self.valueChanged.emit() def update_preview(self): """Обновляет предпросмотр и индикаторы""" a_text = self.part_a.text() d_text = self.part_d.text() v_text = self.part_v.text() n_text = self.part_n.text() # Обновляем индикаторы self.indicator_a.setText(f"{len(a_text)}/3") self.indicator_d.setText(f"{len(d_text)}/8") self.indicator_v.setText(f"{len(v_text)}/3") self.indicator_n.setText(f"{len(n_text)}/3") # Формируем серийный номер a = a_text if len(a_text) == 3 else a_text.ljust(3, 'x') d = d_text if len(d_text) == 8 else d_text.ljust(8, 'x') v = v_text if len(v_text) == 3 else v_text.ljust(3, 'x') n = n_text if len(n_text) == 3 else n_text.ljust(3, 'x') serial = f"A{a}/D{d}/V{v}/N{n}" self.preview.setText(serial) # Проверяем полноту заполнения all_filled = len(a_text) == 3 and len(d_text) == 8 and len(v_text) == 3 and len(n_text) == 3 if all_filled: self.status_label.setText("✓ Все поля заполнены корректно") else: missing = [] if len(a_text) < 3: missing.append("A") if len(d_text) < 8: missing.append("D") if len(v_text) < 3: missing.append("V") if len(n_text) < 3: missing.append("N") if missing: self.status_label.setText( f"Не заполнены поля: {', '.join(missing)} (недостающие символы будут заменены на 'x')" ) else: self.status_label.setText("") def get_serial(self) -> str: """Возвращает серийный номер с заполнением недостающих символов 'x'""" a = self.part_a.text().ljust(3, 'x')[:3] d = self.part_d.text().ljust(8, 'x')[:8] v = self.part_v.text().ljust(3, 'x')[:3] n = self.part_n.text().ljust(3, 'x')[:3] return f"A{a}/D{d}/V{v}/N{n}" def set_serial(self, serial: str): """Устанавливает серийный номер из строки""" try: import re pattern = r'A([^/]+)/D([^/]+)/V([^/]+)/N(.+)' match = re.match(pattern, serial) if match: a, d, v, n = match.groups() a = a.replace('A', '') if a.startswith('A') else a d = d.replace('D', '') if d.startswith('D') else d v = v.replace('V', '') if v.startswith('V') else v n = n.replace('N', '') if n.startswith('N') else n a = a.rstrip('x') d = d.rstrip('x') v = v.rstrip('x') n = n.rstrip('x') a = ''.join(c for c in a if c.isdigit()) d = ''.join(c for c in d if c.isdigit()) v = ''.join(c for c in v if c.isdigit()) n = ''.join(c for c in n if c.isdigit()) self.part_a.setText(a) self.part_d.setText(d) self.part_v.setText(v) self.part_n.setText(n) except Exception as e: print(f"Ошибка парсинга серийного номера: {e}") def clear(self): """Очищает все поля""" self.part_a.clear() self.part_d.clear() self.part_v.clear() self.part_n.clear()