2026, Jan 01 21:02
Как безопасно перемещать виджеты между ячейками QTableWidget в PySide6
Почему перенос виджета в QTableWidget приводит к сбою и как сделать это безопасно в PySide6: используем контейнер и reparenting вместо прямого удаления.
Перемещение виджета между ячейками в QTableWidget на первый взгляд кажется простым: получить виджет, убрать его из старой ячейки и поместить в новую. На деле такой прямолинейный способ нередко приводит к падению приложения. Разберёмся, почему так происходит, и как реализовать безопасный, предсказуемый подход, который стабильно работает в PySide6.
Демонстрация проблемы
Следующий пример создаёт QLabel в ячейке (4, 0) и пытается перенести его в (3, 0) по нажатию кнопки. Код выглядит логично, но заканчивается крахом процесса.
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QTableWidget, QLabel, QPushButton, QVBoxLayout, QWidget
)
class AppWindow(QMainWindow):
def __init__(self):
super().__init__()
self.grid = QTableWidget(5, 2) # 5 строк, 2 столбца
self.grid.setHorizontalHeaderLabels(["Column 1", "Column 2"])
# Поместить QLabel в ячейку (4, 0)
sample = QLabel("Test")
self.grid.setCellWidget(4, 0, sample)
# Кнопка для запуска перемещения
self.trigger_btn = QPushButton("Move widget")
self.trigger_btn.clicked.connect(self.relocate_widget)
vbox = QVBoxLayout()
vbox.addWidget(self.grid)
vbox.addWidget(self.trigger_btn)
shell = QWidget()
shell.setLayout(vbox)
self.setCentralWidget(shell)
def relocate_widget(self):
"""Move the widget from cell (4, 0) to (3, 0)."""
unit = self.grid.cellWidget(4, 0)
self.grid.removeCellWidget(4, 0)
self.grid.setCellWidget(3, 0, unit)
unit.show()
# Отладка
print(f"Cell (3,0) now has: {self.grid.cellWidget(3, 0)}")
print(f"Cell (4,0) now has: {self.grid.cellWidget(4, 0)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = AppWindow()
win.show()
sys.exit(app.exec())
Обычный вывод в консоль подтверждает, что виджет будто бы оказался в новой ячейке, но процесс завершается с кодом -1073741819 (0xC0000005).
Что происходит на самом деле
QTableWidget использует так называемые «index widgets» для встроенных в ячейки виджетов. Управление ими через высокоуровневые методы вроде setCellWidget() и removeCellWidget() имеет важную особенность: установка для ячейки нового виджета — включая установку «без виджета» — удаляет ранее связанный виджет на стороне Qt. В таком сценарии removeCellWidget() фактически работает как установка пустого значения и инициирует удаление старого виджета. После этого попытка повторно использовать тот же экземпляр, будто он всё ещё валиден, приводит к проблемам.
Предотвратить это удаление при «очевидном» использовании этих API нельзя. Безопасный подход — не перемещать «index widget» напрямую. Вместо этого поместите нужный вам элемент управления внутрь контейнера QWidget, а сам внутренний виджет переносите между контейнерами, сменив ему родителя (reparenting) до вызовов removeCellWidget() и setCellWidget().
Рабочее решение
Решение простое: храните реальный виджет внутри контейнера и используйте компоновку. Компоновка обеспечивает корректную геометрию, а перенос родителя для внутреннего виджета не даёт ему уничтожиться при обновлении ячейки.
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QTableWidget, QLabel, QPushButton, QVBoxLayout, QWidget, QHBoxLayout
)
class AppWindow(QMainWindow):
def __init__(self):
super().__init__()
self.grid = QTableWidget(5, 2) # 5 строк, 2 столбца
self.grid.setHorizontalHeaderLabels(["Column 1", "Column 2"])
# Создать контейнер для метки и добавить компоновку для корректного размера
wrapper = QWidget()
hbox = QHBoxLayout(wrapper)
self.demo_lbl = QLabel("Test", wrapper)
hbox.addWidget(self.demo_lbl)
self.grid.setCellWidget(4, 0, wrapper)
# Кнопка для запуска перемещения
self.trigger_btn = QPushButton("Move widget")
self.trigger_btn.clicked.connect(self.relocate_widget)
vbox = QVBoxLayout()
vbox.addWidget(self.grid)
vbox.addWidget(self.trigger_btn)
shell = QWidget()
shell.setLayout(vbox)
self.setCentralWidget(shell)
def relocate_widget(self):
"""Move the label from cell (4, 0) to (3, 0) safely."""
new_wrapper = QWidget()
new_layout = QHBoxLayout(new_wrapper)
new_layout.addWidget(self.demo_lbl)
self.grid.setCellWidget(3, 0, new_wrapper)
self.grid.removeCellWidget(4, 0)
# Отладка
print(f"Cell (3,0) now has: {self.grid.cellWidget(3, 0)}")
print(f"Cell (4,0) now has: {self.grid.cellWidget(4, 0)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = AppWindow()
win.show()
sys.exit(app.exec())
В этом подходе внутренний QLabel получает нового родителя — свежий контейнер — до очистки старой ячейки. Представление безопасно заменяет прежний «index widget», а ваша метка остаётся в целости.
Почему это важно
Высокоуровневые представления, такие как QTableWidget, удобны, но их правила владения объектами легко упустить из виду. Когда вы назначаете виджет ячейке, представление управляет его жизненным циклом. Очистка или замена ячейки удаляет прежний экземпляр. Понимание этого убережёт от трудноуловимых падений и сделает работу с виджетами надёжной.
Если вам нужно перемещать элементы не только визуально, но и логически, используйте модельный подход. Для перемещения строк в QAbstractItemModel предусмотрены beginMoveRows() и endMoveRows(). Если цель — лишь изменить порядок отображения строк без изменения данных, иногда достаточно передвинуть секцию в вертикальном заголовке через moveSection().
Выводы
Встраивая элементы управления в ячейки QTableWidget, воспринимайте их как «index widgets», которыми владеет представление. Не пытайтесь переносить один и тот же экземпляр напрямую между ячейками. Оборачивайте нужный контроль в контейнер и сначала меняйте родителя у внутреннего виджета, а уже затем очищайте или назначайте виджет ячейки. Не забывайте о компоновке в контейнере — так встроенный элемент будет корректно изменять размер вместе с ячейкой. Для структурных перестановок опирайтесь на операции модели или переупорядочивание секций заголовка, когда это уместно.