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:
- Reordenando elementos en un QTreeWidget con Arrastrar y soltar en PyQt
- QT: arrastrar y soltar filas internas en QTableView, que cambia el orden de las filas en QTableModel
- http://www.qtcentre.org/threads/35113-QTableWidget-dropping-between-the-rows-shall-insert-the-item
- qt: pyqt: QTreeView interno arrastra y suelta casi funcionando ... el elemento arrastrado desaparece
- Cómo arrastrar y soltar filas dentro de QTableWidget
- QListWidget arrastra y suelta elementos que desaparecen de la lista en Symbian
- QTableWidget Arrastrar interno dejar toda la fila
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_())