web-dev-qa-db-ja.com

QTableWidget内で行をドラッグアンドドロップします

目標

私の目標は、ユーザーが行を内部でドラッグアンドドロップできるQTableWidgetを作成することです。つまり、ユーザーは1つの行全体をドラッグアンドドロップして、テーブル内で他の2つの行の間の別の場所に上下に移動できます。目標は次の図に示されています。

the challenge

私が試したことと何が起こるか

QTableWidgetにデータを入力したら、そのプロパティを次のように設定します。

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

同様のコードにより、QListWidgetは適切に動作します。アイテムを内部で移動すると、リストの2つの要素の間にドロップされ、残りのアイテムは、データが上書きされることなく(他のつまり、ビューは上の図のように機能しますが、リストです)。

対照的に、上記のコードで変更されたテーブルでは、計画どおりに機能しません。次の図は、実際に何が起こるかを示しています。

crud

つまり、行iが削除されると、その行はテーブル内で空白になります。さらに、誤って行iを行j(2つの行の間のスペースではなく)にドロップした場合、行からのデータi置換行のデータj。つまり、その不幸なケースでは、行iが空白になることに加えて、行jが上書きされます。

table.setDragDropOverwriteMode(False)も追加しようとしましたが、動作は変わりませんでした。

前進する方法は?

このバグレポート C++で考えられる解決策が含まれている可能性があります:dropEventQTableWidgetに再実装したようですが、Pythonにクリーンに移植する方法がわかりません。

関連コンテンツ:

11
eric

これは非常に奇妙なデフォルトの動作のようです。とにかく、 あなたがリンクしたバグレポート のコードに従って、私は何かをPyQtに正常に移植しました。そのコードほど堅牢な場合とそうでない場合がありますが、少なくともスクリーンショットで提供する単純なテストケースでは機能するようです。

以下の実装で発生する可能性のある問題は次のとおりです。

  • 現在選択されている行はドラッグアンドドロップに従いません(したがって、3番目の行を移動すると、移動後も3番目の行が選択されたままになります)。これはおそらく修正するのはそれほど難しいことではありません!

  • 子行のある行では機能しない場合があります。 QTableWidgetItemに子を持たせることができるかどうかさえわからないので、おそらく問題ありません。

  • 複数の行を選択してテストしたことはありませんが、機能するはずです。

  • 何らかの理由で、テーブルに新しい行を挿入したにもかかわらず、移動されていた行を削除する必要はありませんでした。これは私には非常に奇妙に思えます。どこかに行を挿入しているように見えますが、最後ではテーブルのrowCount()は増加しません。

  • 私のGetSelectedRowsFastの実装は、彼らの実装とは少し異なります。高速ではない可能性があり、バグが発生する可能性があります(アイテムが有効になっているか選択可能かどうかは確認していません)。これも簡単に修正できると思いますが、選択中に行を無効にして、誰かがドラッグアンドドロップを実行した場合にのみ問題になります。この状況では、無効になっている行の選択を解除する方が良い解決策かもしれないと思いますが、それはあなたがそれを使って何をしているかによると思います!

このコードを実稼働環境で使用している場合は、細かい櫛でコードを調べて、すべてが理にかなっていることを確認することをお勧めします。おそらく私のPyQtポートに問題があり、おそらく私のポートが基づいていた元のc ++アルゴリズムに問題があります。ただし、これは、QTableWidgetを使用して目的を達成できることの証明として機能します。

更新:PyQt5には 以下の追加の回答 があり、上記の懸念のいくつかも修正されていることに注意してください。あなたはそれをチェックしたいかもしれません!

コード:

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

これは、PyQt5およびPython 3用に設計された three-pineapples answerの改訂版です。また、複数選択のドラッグアンドドロップを修正し、行を再選択します引っ越し後。

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_())
5
Scott Maxwell

そのため、最近この同じ問題に遭遇し、上記のコードブロックを、すべて同じ動作をしていると思うものに絞り込みましたが、はるかに簡潔です。

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
4
Jesse C

グーグルを使用してC++を使用するための適切な解決策が見つからなかったので、私は私のものを追加したいと思います:

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