目標
私の目標は、ユーザーが行を内部でドラッグアンドドロップできるQTableWidget
を作成することです。つまり、ユーザーは1つの行全体をドラッグアンドドロップして、テーブル内で他の2つの行の間の別の場所に上下に移動できます。目標は次の図に示されています。
私が試したことと何が起こるか
QTableWidget
にデータを入力したら、そのプロパティを次のように設定します。
_table.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
#select one row at a time
table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
_
同様のコードにより、QListWidget
は適切に動作します。アイテムを内部で移動すると、リストの2つの要素の間にドロップされ、残りのアイテムは、データが上書きされることなく(他のつまり、ビューは上の図のように機能しますが、リストです)。
対照的に、上記のコードで変更されたテーブルでは、計画どおりに機能しません。次の図は、実際に何が起こるかを示しています。
つまり、行iが削除されると、その行はテーブル内で空白になります。さらに、誤って行iを行j(2つの行の間のスペースではなく)にドロップした場合、行からのデータi置換行のデータj。つまり、その不幸なケースでは、行iが空白になることに加えて、行jが上書きされます。
table.setDragDropOverwriteMode(False)
も追加しようとしましたが、動作は変わりませんでした。
前進する方法は?
このバグレポート C++で考えられる解決策が含まれている可能性があります:dropEvent
をQTableWidget
に再実装したようですが、Pythonにクリーンに移植する方法がわかりません。
関連コンテンツ:
これは非常に奇妙なデフォルトの動作のようです。とにかく、 あなたがリンクしたバグレポート のコードに従って、私は何かを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_())
これは、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_())
そのため、最近この同じ問題に遭遇し、上記のコードブロックを、すべて同じ動作をしていると思うものに絞り込みましたが、はるかに簡潔です。
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
グーグルを使用して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));
}
}
}