2014-10-07 15 views
9

GoalDrag and drop righe all'interno QTableWidget

Il mio obiettivo è di avere una QTableWidget in cui l'utente può trascinare/goccia file internamente. Cioè, l'utente può trascinare e rilasciare un'intera riga, spostandola verso l'alto o verso il basso nella tabella in una posizione diversa tra altre due righe. L'obiettivo è illustrato in questa figura:

the challenge

Quello che ho cercato, e cosa succede

volta ho popolato un QTableWidget con i dati, ho impostato le proprietà come segue:

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

Il codice simile rende QListWidget comportarsi bene: quando si sposta un elemento internamente, viene rilasciato tra due elementi dell'elenco e il resto di gli oggetti si ordinano in modo ragionevole, senza dati sovrascritti (in altre parole, la vista si comporta come nella figura sopra, ma è una lista).

Al contrario, in una tabella modificata con il codice sopra riportato, le cose non funzionano come pianificato. La figura seguente mostra cosa accade realmente:

crud

In parole: quando fila i gocciola, tale riga diventa vuoto nella tabella. Inoltre, se accidentalmente cadere riga i sulla riga j (invece che lo spazio tra due file), i dati di riga isostituisce i dati nella riga j. Cioè, in quello sfortunato caso, oltre alla riga i che diventa vuota, la riga j viene sovrascritta.

Nota Ho anche provato ad aggiungere table.setDragDropOverwriteMode(False) ma non ha modificato il comportamento.

Una via da seguire?

This bug report potrebbero includere una possibile soluzione in C++: sembra che reimplementate dropEvent per QTableWidget, ma io non sono sicuro di come la porta in modo pulito a Python.

Contenuti correlati:

+0

qt-project.org è morto, ora al bug https://bugreports.qt.io/browse/QTBUG-13873 – handle

risposta

8

Questo sembra comportamento predefinito molto bizzarro. Comunque, seguendo il codice nello bug report you linked to, ho portato con successo qualcosa a PyQt. Potrebbe, o potrebbe non essere robusto come quel codice, ma almeno sembra funzionare per il semplice caso di test che fornisci nei tuoi screenshot!

I potenziali problemi con l'implementazione di seguito sono:

  • La riga attualmente selezionata non segue il drag and drop (quindi se si sposta la terza fila, la terza fila rimane selezionato dopo la mossa) . Questo probabilmente non è troppo difficile da risolvere!

  • Potrebbe non funzionare per righe con righe figlio. Non sono nemmeno sicuro che un QTableWidgetItem possa avere figli, quindi forse va bene.

  • non ho testato con selezionando più righe, ma penso che dovrebbe funzionare

  • Per qualche ragione non ho dovuto rimuovere la riga che veniva spostato, nonostante l'inserimento di una nuova riga nella tavolo. Questo mi sembra molto strano. Sembra quasi come inserire una riga ovunque ma la fine non aumenta il rowCount() della tabella.

  • La mia implementazione di GetSelectedRowsFast è leggermente diversa dalla loro. Potrebbe non essere veloce e potrebbe avere qualche bug in esso (non controllo se gli oggetti sono abilitati o selezionabili) come hanno fatto. Questo sarebbe anche facile da risolvere, penso, ma è solo un problema se si disabilita una riga mentre è selezionata e qualcuno quindi esegue un drag/drop. In questa situazione, penso che la soluzione migliore potrebbe essere quella di deselezionare le righe in quanto disabilitate, ma dipende da cosa si sta facendo con esso, immagino!

Se si sta utilizzando questo codice in un ambiente di produzione, si sarebbe probabilmente vuole andare su di esso con un pettine a denti-pettine e assicurarsi che tutto aveva un senso. Ci sono probabilmente problemi con la mia porta PyQt, e probabilmente problemi con l'algoritmo C++ originale su cui si basava la mia porta. Tuttavia, serve come una prova che ciò che si desidera può essere raggiunto utilizzando un QTableWidget.

Codice:

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_()) 
+0

grandi cose, sicuramente imparando molto da questo bell'esempio! Ho scritto un follow-up per aiutarmi a provare a capire supportDropActions: http://stackoverflow.com/questions/26321386/pull-values-of-dropactions. – neuronet

+0

grazie mille. il tuo link al sito di qre bugreport era tutto ciò che cercavo. Avevo bisogno di C++ così sfortunatamente il tuo codice non era esattamente quello di cui avevo bisogno :) – MadddinTribleD

2

Così mi sono imbattuto in questo stesso problema recente e ho distillato il blocco di codice di cui sopra giù in qualcosa che credo abbia tutti lo stesso comportamento, ma è molto più succinta.

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 
+0

Non vicino al mio computer per controllare, ma sicuramente lo farò, e ti farò sapere come funziona. – neuronet

+0

Buon lavoro compagno! Funziona! – wondie

+0

Molto più semplice, e funziona alla grande! – Spencer

1

Dal momento che non ho trovato alcuna soluzione adeguata per l'utilizzo C++ usando google voglio aggiungere la mia:

#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)); 
     } 
    } 
} 
2

Ecco una versione riveduta del three-pineapples risposta che è stato progettato per PyQt5 e Python 3. Corregge anche multi-selezione drag-and-drop e riseleziona le righe dopo lo spostamento.

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_())