example current qt pyqt pyqt4 pyside

current - Arrastra y suelta filas dentro de QTableWidget



qtablewidget example (4)

Esto parece un comportamiento predeterminado muy extraño. De todos modos, siguiendo el código en el informe de errores al que se vinculó , he portado algo con éxito a PyQt. Puede o no ser tan robusto como ese código, pero al menos parece funcionar para el caso de prueba simple que usted proporciona en sus capturas de pantalla.

Los posibles problemas con la implementación a continuación son:

  • La fila seleccionada actualmente no sigue la función de arrastrar y soltar (por lo tanto, si mueve la tercera fila, la tercera fila permanece seleccionada después del movimiento). ¡Esto probablemente no es demasiado difícil de arreglar!

  • Es posible que no funcione para las filas con filas secundarias. Ni siquiera estoy seguro de si un QTableWidgetItem puede tener hijos, por lo que quizás esté bien.

  • No he probado con la selección de varias filas, pero creo que debería funcionar

  • Por alguna razón, no tuve que eliminar la fila que se estaba moviendo, a pesar de insertar una nueva fila en la tabla. Esto me parece muy extraño. Casi parece como insertar una fila en cualquier lugar, pero el final no aumenta el rowCount() de la tabla.

  • Mi implementación de GetSelectedRowsFast es un poco diferente a la de ellos. Puede que no sea rápido y podría tener algunos errores (no compruebo si los elementos están habilitados o son seleccionables) como lo hicieron. Esto también sería fácil de arreglar, creo, pero solo es un problema si deshabilita una fila mientras está seleccionada y luego alguien realiza una operación de arrastrar / soltar. En esta situación, creo que la mejor solución podría ser anular la selección de las filas, ya que estaban deshabilitadas, ¡pero depende de lo que esté haciendo con eso, supongo!

Si estuviera usando este código en un entorno de producción, probablemente desee revisarlo con un peine de dientes finos y asegurarse de que todo tenga sentido. Probablemente haya problemas con mi puerto PyQt, y posiblemente problemas con el algoritmo c ++ original en el que se basa mi puerto. Sin embargo, sirve como una prueba de que lo que desea se puede lograr utilizando un QTableWidget .

Código:

import sys, os from PyQt4.QtCore import * from PyQt4.QtGui import * class TableWidgetDragRows(QTableWidget): def __init__(self, *args, **kwargs): QTableWidget.__init__(self, *args, **kwargs) self.setDragEnabled(True) self.setAcceptDrops(True) self.viewport().setAcceptDrops(True) self.setDragDropOverwriteMode(False) self.setDropIndicatorShown(True) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setDragDropMode(QAbstractItemView.InternalMove) def dropEvent(self, event): if event.source() == self and (event.dropAction() == Qt.MoveAction or self.dragDropMode() == QAbstractItemView.InternalMove): success, row, col, topIndex = self.dropOn(event) if success: selRows = self.getSelectedRowsFast() top = selRows[0] # print ''top is %d''%top dropRow = row if dropRow == -1: dropRow = self.rowCount() # print ''dropRow is %d''%dropRow offset = dropRow - top # print ''offset is %d''%offset for i, row in enumerate(selRows): r = row + offset if r > self.rowCount() or r < 0: r = 0 self.insertRow(r) # print ''inserting row at %d''%r selRows = self.getSelectedRowsFast() # print ''selected rows: %s''%selRows top = selRows[0] # print ''top is %d''%top offset = dropRow - top # print ''offset is %d''%offset for i, row in enumerate(selRows): r = row + offset if r > self.rowCount() or r < 0: r = 0 for j in range(self.columnCount()): # print ''source is (%d, %d)''%(row, j) # print ''item text: %s''%self.item(row,j).text() source = QTableWidgetItem(self.item(row, j)) # print ''dest is (%d, %d)''%(r,j) self.setItem(r, j, source) # Why does this NOT need to be here? # for row in reversed(selRows): # self.removeRow(row) event.accept() else: QTableView.dropEvent(event) def getSelectedRowsFast(self): selRows = [] for item in self.selectedItems(): if item.row() not in selRows: selRows.append(item.row()) return selRows def droppingOnItself(self, event, index): dropAction = event.dropAction() if self.dragDropMode() == QAbstractItemView.InternalMove: dropAction = Qt.MoveAction if event.source() == self and event.possibleActions() & Qt.MoveAction and dropAction == Qt.MoveAction: selectedIndexes = self.selectedIndexes() child = index while child.isValid() and child != self.rootIndex(): if child in selectedIndexes: return True child = child.parent() return False def dropOn(self, event): if event.isAccepted(): return False, None, None, None index = QModelIndex() row = -1 col = -1 if self.viewport().rect().contains(event.pos()): index = self.indexAt(event.pos()) if not index.isValid() or not self.visualRect(index).contains(event.pos()): index = self.rootIndex() if self.model().supportedDropActions() & event.dropAction(): if index != self.rootIndex(): dropIndicatorPosition = self.position(event.pos(), self.visualRect(index), index) if dropIndicatorPosition == QAbstractItemView.AboveItem: row = index.row() col = index.column() # index = index.parent() elif dropIndicatorPosition == QAbstractItemView.BelowItem: row = index.row() + 1 col = index.column() # index = index.parent() else: row = index.row() col = index.column() if not self.droppingOnItself(event, index): # print ''row is %d''%row # print ''col is %d''%col return True, row, col, index return False, None, None, None def position(self, pos, rect, index): r = QAbstractItemView.OnViewport margin = 2 if pos.y() - rect.top() < margin: r = QAbstractItemView.AboveItem elif rect.bottom() - pos.y() < margin: r = QAbstractItemView.BelowItem elif rect.contains(pos, True): r = QAbstractItemView.OnItem if r == QAbstractItemView.OnItem and not (self.model().flags(index) & Qt.ItemIsDropEnabled): r = QAbstractItemView.AboveItem if pos.y() < rect.center().y() else QAbstractItemView.BelowItem return r class Window(QWidget): def __init__(self): super(Window, self).__init__() layout = QHBoxLayout() self.setLayout(layout) self.table_widget = TableWidgetDragRows() layout.addWidget(self.table_widget) # setup table widget self.table_widget.setColumnCount(2) self.table_widget.setHorizontalHeaderLabels([''Colour'', ''Model'']) items = [(''Red'', ''Toyota''), (''Blue'', ''RV''), (''Green'', ''Beetle'')] for i, (colour, model) in enumerate(items): c = QTableWidgetItem(colour) m = QTableWidgetItem(model) self.table_widget.insertRow(self.table_widget.rowCount()) self.table_widget.setItem(i, 0, c) self.table_widget.setItem(i, 1, m) self.show() app = QApplication(sys.argv) window = Window() sys.exit(app.exec_())

Gol

Mi objetivo es tener un QTableWidget en el que el usuario pueda arrastrar / soltar filas internamente. Es decir, el usuario puede arrastrar y soltar una fila completa, moviéndola hacia arriba o hacia abajo en la tabla a una ubicación diferente entre otras dos filas. El objetivo se ilustra en esta figura:

Lo que probé y lo que sucede

Una vez que he QTableWidget un QTableWidget con datos, configuré sus propiedades de la siguiente manera:

table.setDragDropMode(QtGui.QAbstractItemView.InternalMove) #select one row at a time table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)

Un código similar hace que QListWidget comporte bien: cuando mueves un elemento internamente, se QListWidget entre dos elementos de la lista, y el resto de los elementos se clasifican de manera razonable, sin sobrescribir los datos (en otras palabras, la vista actúa como la figura de arriba, pero es una lista).

Por el contrario, en una tabla modificada con el código anterior, las cosas no funcionan como se planeó. La siguiente figura muestra lo que realmente sucede:

En palabras: cuando se descarta la fila i , esa fila queda en blanco en la tabla. Además, si accidentalmente dejo caer la fila i en la fila j (en lugar del espacio entre dos filas), los datos de la fila i reemplazan los datos en la fila j . Es decir, en ese desafortunado caso, además de que la fila i queda en blanco, la fila j se sobrescribe.

Tenga en cuenta que también intenté agregar table.setDragDropOverwriteMode(False) pero no modificó el comportamiento.

¿Un camino a seguir?

Este informe de error podría incluir una posible solución en C ++: parece que se dropEvent a QTableWidget dropEvent para QTableWidget , pero no estoy seguro de cómo QTableWidget limpiamente a Python.

Contenido relacionado:


Así que me encontré con este mismo problema recientemente y destilé el bloque de código anterior en algo que creo que tiene el mismo comportamiento, pero es mucho más conciso.

def dropEvent(self, event): if event.source() == self: rows = set([mi.row() for mi in self.selectedIndexes()]) targetRow = self.indexAt(event.pos()).row() rows.discard(targetRow) rows = sorted(rows) if not rows: return if targetRow == -1: targetRow = self.rowCount() for _ in range(len(rows)): self.insertRow(targetRow) rowMapping = dict() # Src row to target row. for idx, row in enumerate(rows): if row < targetRow: rowMapping[row] = targetRow + idx else: rowMapping[row + len(rows)] = targetRow + idx colCount = self.columnCount() for srcRow, tgtRow in sorted(rowMapping.iteritems()): for col in range(0, colCount): self.setItem(tgtRow, col, self.takeItem(srcRow, col)) for row in reversed(sorted(rowMapping.iterkeys())): self.removeRow(row) event.accept() return


Como no encontré ninguna solución adecuada para usar C++ con google, quiero agregar la mía:

#include "mytablewidget.h" MyTableWidget::MyTableWidget(QWidget *parent) : QTableWidget(parent) { } void MyTableWidget::dropEvent(QDropEvent *event) { if(event->source() == this) { int newRow = this->indexAt(event->pos()).row(); QTableWidgetItem *selectedItem; QList<QTableWidgetItem*> selectedItems = this->selectedItems(); if(newRow == -1) newRow = this->rowCount(); int i; for(i = 0; i < selectedItems.length()/this->columnCount(); i++) { this->insertRow(newRow); } int currentOldRow = -1; int currentNewRow = newRow-1; QList<int> deleteRows; foreach(selectedItem, selectedItems) { int column = selectedItem->column(); if(selectedItem->row() != currentOldRow) { currentOldRow = selectedItem->row(); deleteRows.append(currentOldRow); currentNewRow++; } this->takeItem(currentOldRow, column); this->setItem(currentNewRow, column, selectedItem); } for(i = deleteRows.count()-1; i>=0; i--) { this->removeRow(deleteRows.at(i)); } } }


Aquí hay una versión revisada de la respuesta de tres piñas diseñada para PyQt5 y Python 3. También corrige el arrastre y soltar de selección múltiple y vuelve a seleccionar las filas después del movimiento.

import sys from PyQt5.QtCore import Qt from PyQt5.QtGui import QDropEvent from PyQt5.QtWidgets import QTableWidget, QAbstractItemView, QTableWidgetItem, QWidget, QHBoxLayout, / QApplication class TableWidgetDragRows(QTableWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setDragEnabled(True) self.setAcceptDrops(True) self.viewport().setAcceptDrops(True) self.setDragDropOverwriteMode(False) self.setDropIndicatorShown(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setDragDropMode(QAbstractItemView.InternalMove) def dropEvent(self, event: QDropEvent): if not event.isAccepted() and event.source() == self: drop_row = self.drop_on(event) rows = sorted(set(item.row() for item in self.selectedItems())) rows_to_move = [[QTableWidgetItem(self.item(row_index, column_index)) for column_index in range(self.columnCount())] for row_index in rows] for row_index in reversed(rows): self.removeRow(row_index) if row_index < drop_row: drop_row -= 1 for row_index, data in enumerate(rows_to_move): row_index += drop_row self.insertRow(row_index) for column_index, column_data in enumerate(data): self.setItem(row_index, column_index, column_data) event.accept() for row_index in range(len(rows_to_move)): self.item(drop_row + row_index, 0).setSelected(True) self.item(drop_row + row_index, 1).setSelected(True) super().dropEvent(event) def drop_on(self, event): index = self.indexAt(event.pos()) if not index.isValid(): return self.rowCount() return index.row() + 1 if self.is_below(event.pos(), index) else index.row() def is_below(self, pos, index): rect = self.visualRect(index) margin = 2 if pos.y() - rect.top() < margin: return False elif rect.bottom() - pos.y() < margin: return True # noinspection PyTypeChecker return rect.contains(pos, True) and not (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) and pos.y() >= rect.center().y() class Window(QWidget): def __init__(self): super(Window, self).__init__() layout = QHBoxLayout() self.setLayout(layout) self.table_widget = TableWidgetDragRows() layout.addWidget(self.table_widget) # setup table widget self.table_widget.setColumnCount(2) self.table_widget.setHorizontalHeaderLabels([''Type'', ''Name'']) items = [(''Red'', ''Toyota''), (''Blue'', ''RV''), (''Green'', ''Beetle''), (''Silver'', ''Chevy''), (''Black'', ''BMW'')] self.table_widget.setRowCount(len(items)) for i, (color, model) in enumerate(items): self.table_widget.setItem(i, 0, QTableWidgetItem(color)) self.table_widget.setItem(i, 1, QTableWidgetItem(model)) self.resize(400, 400) self.show() if __name__ == ''__main__'': app = QApplication(sys.argv) window = Window() sys.exit(app.exec_())