Add reorder functionality to CustomListModel class using QStyledItemDelegate
-
wrote on 23 Feb 2023, 17:13 last edited by Geuse
I found this example: https://dftalk.jp/?p=16745
I'm using Maya to develop my UI:s.
It looks very much what I'm after, but I can't for the life of me figure out how to implement the reordering of the items.
I've modified the code somewhat, but instead of putting the element in between two other in the list, it creates a new one at the bottom and only displays it with the name of the item.
I can read the code and see why this is happening. The problem is I can't figure out how to code it like I want it to behave.
I'm not getting the correct row value as I executeprint(row)
in the dropMimeData method.
I can see in the mimeData method that I am only adding the data name field, no other data. So how can I get the rest of them?
And I'm wondering if the approach is to copy the data and create a new item at the new position and then delete the old item, or is there a way to actually move the original item to its new location?
I'm not sure if I need to modify the CustomListDelegate class's dropEvent or not.Any help, or pointers are greatly appreciated.
import maya.OpenMayaUI as omui import os import sys from PySide2 import QtWidgets, QtGui, QtCore from shiboken2 import wrapInstance # スクリプトのフォルダ #CURRENT_PATH = os.path.dirname(__file__) CURRENT_PATH = os.path.normpath(r'D:\scripts\delegate_example') # オリジナルのRole DESCRIPTION_ROLE = QtCore.Qt.UserRole STATUS_ROLE = QtCore.Qt.UserRole + 1 THUMB_ROLE = QtCore.Qt.UserRole + 2 # Modelに入るデータ SAMPLE_DATA = [ {"name":"Ben", "description":"Support", "status":"OK", "color":[217,204,0], "thumbnail":"ben.png"}, {"name":"Chris", "description":"Modeling", "status":"NOT OK", "color":[127,197,195], "thumbnail":"chris.png"}, {"name":"Daniel", "description":"Manager", "status":"OK", "color":[217,204,166], "thumbnail":"daniel.png"}, {"name":"Natalie", "description":"Sales", "status":"NOT OK", "color":[237,111,112], "thumbnail":"natalie.png"}, {"name":"Mike", "description":"Animation", "status":"NOT OK", "color":[127,197,195], "thumbnail":"mike.png"} ] class CustomListModel(QtCore.QAbstractListModel): def __init__(self, parent=None, data=None): super(CustomListModel, self).__init__(parent) self.__items = data def rowCount(self, parent=QtCore.QModelIndex()): return len(self.__items) def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid(): return None if not 0 <= index.row() < len(self.__items): return None if role == QtCore.Qt.DisplayRole: return self.__items[index.row()]["name"] elif role == DESCRIPTION_ROLE: return self.__items[index.row()]["description"] elif role == STATUS_ROLE: return self.__items[index.row()]["status"] elif role == THUMB_ROLE: return self.__items[index.row()]["thumbnail"] elif role == QtCore.Qt.BackgroundRole: color = self.__items[index.row()]["color"] return QtGui.QColor(color[0], color[1], color[2]) else: return None def flags(self, index): defaultFlags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable if index.isValid(): return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | defaultFlags else: return QtCore.Qt.ItemIsDropEnabled | defaultFlags def mimeTypes(self): return ['text/plain'] def mimeData(self, indexes): mimeData = QtCore.QMimeData() encodedData = QtCore.QByteArray() stream = QtCore.QDataStream(encodedData, QtCore.QIODevice.WriteOnly) for index in indexes: if index.isValid(): itemData = self.__items[index.row()] stream.writeQString(itemData['name']) mimeData.setData('text/plain', encodedData) return mimeData def canDropMimeData(self, data, action, row, column, parent): if not data.hasText(): return False if action == QtCore.Qt.IgnoreAction: return True if column > 0: return False if row != -1: return True if parent.isValid(): return False return True def dropMimeData(self, data, action, row, column, parent): print(self.rowCount()) print(row) if not self.canDropMimeData(data, action, row, column, parent): return False if action == QtCore.Qt.IgnoreAction: return True if row == -1: row = self.rowCount() if column > 0: return False encodedData = data.data('text/plain') stream = QtCore.QDataStream(encodedData, QtCore.QIODevice.ReadOnly) newItems = [] rows = 0 while not stream.atEnd(): text = stream.readQString() newItems.append(text) rows += 1 self.beginInsertRows(QtCore.QModelIndex(), row, row + rows - 1) for text in newItems: print(text) self.__items.insert(row, {"name": text, "description": "", "status": "", "thumbnail": "", "color": [255, 255, 255]}) row += 1 self.endInsertRows() return True class CustomListDelegate(QtWidgets.QStyledItemDelegate): # Listの各アイテムの大きさ ITEM_SIZE_HINT = [300, 70] # 各アイテム内の表示に使う定数 MARGIN = 20 THUMB_AREA_WIDTH = 70 THUMB_WIDTH = 60 STATUS_AREA_WIDTH = 24 # Painterが使うブラシやペンの定義 BG_DEFAULT = QtGui.QBrush(QtGui.QColor(247,248,242)) BG_SELECTED = QtGui.QBrush(QtGui.QColor(255,255,204)) BORDER_DEFAULT = QtGui.QPen(QtGui.QColor(255,255,255), 0.5, QtCore.Qt.SolidLine) TEXT_BLACK_PEN = QtGui.QPen(QtGui.QColor(106,107,109), 0.5, QtCore.Qt.SolidLine) SHADOW_PEN = QtGui.QPen(QtGui.QColor(220,220,220, 100), 1, QtCore.Qt.SolidLine) FONT_H1 = QtGui.QFont("Corbel", 12, QtGui.QFont.Normal, True) FONT_H2 = QtGui.QFont("Corbel", 11, QtGui.QFont.Normal, True) def __init__(self, parent=None): super(CustomListDelegate, self).__init__(parent) self._pressed_index = None self._target_index = None self._target_rect = None def mousePressEvent(self, event, model, view): if event.button() == QtCore.Qt.LeftButton: self._pressed_index = view.indexAt(event.pos()) if self._pressed_index.isValid(): pixmap = QtGui.QPixmap.grabWidget(view, self._pressed_index.rect()) drag = QtGui.QDrag(view) mime_data = QtCore.QMimeData() mime_data.setData("application/x-qabstractitemmodeldatalist", QByteArray()) drag.setMimeData(mime_data) drag.setPixmap(pixmap) drag.setHotSpot(event.pos() - self._pressed_index.rect().topLeft()) drag.exec_() def mouseMoveEvent(self, event, model, view): if self._pressed_index.isValid(): if (event.pos() - self._pressed_index.rect().topLeft()).manhattanLength() > QtWidgets.QApplication.startDragDistance(): pixmap = QtGui.QPixmap.grabWidget(view, self._pressed_index.rect()) drag = QtGui.QDrag(view) mime_data = QtCore.QMimeData() mime_data.setData("application/x-qabstractitemmodeldatalist", QByteArray()) drag.setMimeData(mime_data) drag.setPixmap(pixmap) drag.setHotSpot(event.pos() - self._pressed_index.rect().topLeft()) drag.exec_() def dragEnterEvent(self, event, model, view): if event.mimeData().hasFormat("application/x-qabstractitemmodeldatalist"): event.accept() self._target_index = view.indexAt(event.pos()) self._target_rect = view.visualRect(self._target_index) return event.ignore() def dragMoveEvent(self, event, model, view): if self._target_index.isValid(): self._target_rect.setHeight(view.rowHeight(self._target_index)) if self._target_rect.contains(event.pos()): event.setDropAction(QtCore.Qt.MoveAction) event.accept() return event.ignore() def dropEvent(self, event, model, view): if event.mimeData().hasFormat("application/x-qabstractitemmodeldatalist") and self._target_index.isValid(): target_row = self._target_index.row() source_row = self._pressed_index.row() if target_row > source_row: target_row -= 1 item_data = model._CustomListModel__items.pop(source_row) model.beginInsertRows(QtCore.QModelIndex(), target_row, target_row) model._CustomListModel__items.insert(target_row, item_data) model.endInsertRows() self._pressed_index = None self._target_index = None event.accept() return event.ignore() # 背景描画メソッド paintメソッド内で呼ばれている def drawBackground(self, painter, rect, selected, color): if selected: baseColor = self.BG_SELECTED else: baseColor = self.BG_DEFAULT painter.setPen(self.BORDER_DEFAULT) painter.setBrush(baseColor) painter.drawRoundedRect(rect, 1, 1) painter.setPen(self.SHADOW_PEN) painter.drawLine(rect.bottomLeft().x(), rect.bottomLeft().y()+2, rect.bottomRight().x(), rect.bottomRight().y()+2) r = QtCore.QRect(rect.left(), rect.top(), self.THUMB_AREA_WIDTH, rect.height()) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QBrush(color)) painter.drawRoundedRect(r, 1, 1) # サムネイル描画メソッド paintメソッド内で呼ばれている def drawThumbnail(self, painter, rect, thumbnail): r = QtCore.QRect(rect.left() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, rect.top() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, self.THUMB_WIDTH, self.THUMB_WIDTH) thumbImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", thumbnail)).scaled(self.THUMB_WIDTH, self.THUMB_WIDTH) painter.drawPixmap(r, thumbImage) # 名前テキスト描画メソッド paintメソッド内で呼ばれている def drawName(self, painter, rect, name): painter.setFont(self.FONT_H1) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top(), rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, "Name : " + name) painter.setPen(self.TEXT_BLACK_PEN) painter.drawLine(r.bottomLeft(), r.bottomRight()) # ディスクリプションテキスト描画メソッド paintメソッド内で呼ばれている def drawDescription(self, painter, rect, description): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top() + rect.height() / 2, rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, description) # ステータス画像描画メソッド paintメソッド内で呼ばれている def drawStatus(self, painter, rect, status): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.right() - self.STATUS_AREA_WIDTH - self.MARGIN, rect.center().y() - self.STATUS_AREA_WIDTH / 2, self.STATUS_AREA_WIDTH, self.STATUS_AREA_WIDTH) statusIcon = "notok.png" if status == "OK": statusIcon = "ok.png" statusImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", statusIcon)) painter.drawPixmap(r, statusImage) # 描画メインメソッド def paint(self, painter, option, index): selected = False if option.state & QtWidgets.QStyle.State_Selected: selected = True name = index.data(QtCore.Qt.DisplayRole) description = index.data(DESCRIPTION_ROLE) status = index.data(STATUS_ROLE) color = index.data(QtCore.Qt.BackgroundRole) thumbnail = index.data(THUMB_ROLE) self.drawBackground(painter, option.rect, selected, color) self.drawThumbnail(painter, option.rect, thumbnail) self.drawName(painter, option.rect, name) self.drawDescription(painter, option.rect, description) self.drawStatus(painter, option.rect, status) # 各アイテムの大きさ def sizeHint(self, option, index): return QtCore.QSize(self.ITEM_SIZE_HINT[0], self.ITEM_SIZE_HINT[1]) #---------------------------------------------------------------------------- ## メインUI def maya_main_window(): main_window_ptr = omui.MQtUtil.mainWindow() return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) class GUI(QtWidgets.QMainWindow): def __init__(self, parent=maya_main_window()): super(GUI, self).__init__(parent) self.setWindowTitle('Main Window') self.resize(350, 600) self.setMaximumSize(525, 700) self.initUI() def initUI(self): # リストの設定 myListView = QtWidgets.QListView(self) myListView.setSpacing(10) myListView.setAutoFillBackground(False) url = os.path.join(CURRENT_PATH, "images", "bg.png").replace("\\", "/") myListView.setStyleSheet("background: url("+url+") center center;") myListView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) # modelを作る myListModel = CustomListModel(data = SAMPLE_DATA) # delegateを作る myListDelegate = CustomListDelegate() # listview myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) myListView.setDefaultDropAction(QtCore.Qt.MoveAction) myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragEnabled(True) myListView.setAcceptDrops(True) myListView.setDropIndicatorShown(True) # modelとdelegateをリストにセット myListView.setItemDelegate(myListDelegate) myListView.setModel(myListModel) self.setCentralWidget(myListView) # if __name__ == "__main__": try: test_dialog.close() test_dialog.deleteLater() except: pass test_dialog = GUI() test_dialog.show()
-
Hi,
Why are you implementing drag and drop in the delegate ? That's not its role and QListView already supports it.
-
Hi,
Why are you implementing drag and drop in the delegate ? That's not its role and QListView already supports it.
wrote on 23 Feb 2023, 21:20 last edited by Geuse@SGaist Thanks so much.
And to answer your question "why?" Because I don't know any better... =/
So I should remove all event methods from the CustomListDelegate class?Tried that and the UI works the same... :-) So it should just handle the paint methods then, correct?
So I suppose my only problem is to modify the dropMimeData then in CustomListModel.But how do I do that. As I wrote, I believe I need a way to get the row of where I'm dragging from to where I'm going.
And is there a way I can move an item or should I create a new one and copy the contents of the old one? -
Yes, remove all that stuff from the delegate.
There's a dedicated section about drag and drop in the model view programming chapter in Qt's documentation.
The chapter explains all the concepts regarding Qt's model view implementation.
-
Yes, remove all that stuff from the delegate.
There's a dedicated section about drag and drop in the model view programming chapter in Qt's documentation.
The chapter explains all the concepts regarding Qt's model view implementation.
-
I found this example: https://dftalk.jp/?p=16745
I'm using Maya to develop my UI:s.
It looks very much what I'm after, but I can't for the life of me figure out how to implement the reordering of the items.
I've modified the code somewhat, but instead of putting the element in between two other in the list, it creates a new one at the bottom and only displays it with the name of the item.
I can read the code and see why this is happening. The problem is I can't figure out how to code it like I want it to behave.
I'm not getting the correct row value as I executeprint(row)
in the dropMimeData method.
I can see in the mimeData method that I am only adding the data name field, no other data. So how can I get the rest of them?
And I'm wondering if the approach is to copy the data and create a new item at the new position and then delete the old item, or is there a way to actually move the original item to its new location?
I'm not sure if I need to modify the CustomListDelegate class's dropEvent or not.Any help, or pointers are greatly appreciated.
import maya.OpenMayaUI as omui import os import sys from PySide2 import QtWidgets, QtGui, QtCore from shiboken2 import wrapInstance # スクリプトのフォルダ #CURRENT_PATH = os.path.dirname(__file__) CURRENT_PATH = os.path.normpath(r'D:\scripts\delegate_example') # オリジナルのRole DESCRIPTION_ROLE = QtCore.Qt.UserRole STATUS_ROLE = QtCore.Qt.UserRole + 1 THUMB_ROLE = QtCore.Qt.UserRole + 2 # Modelに入るデータ SAMPLE_DATA = [ {"name":"Ben", "description":"Support", "status":"OK", "color":[217,204,0], "thumbnail":"ben.png"}, {"name":"Chris", "description":"Modeling", "status":"NOT OK", "color":[127,197,195], "thumbnail":"chris.png"}, {"name":"Daniel", "description":"Manager", "status":"OK", "color":[217,204,166], "thumbnail":"daniel.png"}, {"name":"Natalie", "description":"Sales", "status":"NOT OK", "color":[237,111,112], "thumbnail":"natalie.png"}, {"name":"Mike", "description":"Animation", "status":"NOT OK", "color":[127,197,195], "thumbnail":"mike.png"} ] class CustomListModel(QtCore.QAbstractListModel): def __init__(self, parent=None, data=None): super(CustomListModel, self).__init__(parent) self.__items = data def rowCount(self, parent=QtCore.QModelIndex()): return len(self.__items) def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid(): return None if not 0 <= index.row() < len(self.__items): return None if role == QtCore.Qt.DisplayRole: return self.__items[index.row()]["name"] elif role == DESCRIPTION_ROLE: return self.__items[index.row()]["description"] elif role == STATUS_ROLE: return self.__items[index.row()]["status"] elif role == THUMB_ROLE: return self.__items[index.row()]["thumbnail"] elif role == QtCore.Qt.BackgroundRole: color = self.__items[index.row()]["color"] return QtGui.QColor(color[0], color[1], color[2]) else: return None def flags(self, index): defaultFlags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable if index.isValid(): return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | defaultFlags else: return QtCore.Qt.ItemIsDropEnabled | defaultFlags def mimeTypes(self): return ['text/plain'] def mimeData(self, indexes): mimeData = QtCore.QMimeData() encodedData = QtCore.QByteArray() stream = QtCore.QDataStream(encodedData, QtCore.QIODevice.WriteOnly) for index in indexes: if index.isValid(): itemData = self.__items[index.row()] stream.writeQString(itemData['name']) mimeData.setData('text/plain', encodedData) return mimeData def canDropMimeData(self, data, action, row, column, parent): if not data.hasText(): return False if action == QtCore.Qt.IgnoreAction: return True if column > 0: return False if row != -1: return True if parent.isValid(): return False return True def dropMimeData(self, data, action, row, column, parent): print(self.rowCount()) print(row) if not self.canDropMimeData(data, action, row, column, parent): return False if action == QtCore.Qt.IgnoreAction: return True if row == -1: row = self.rowCount() if column > 0: return False encodedData = data.data('text/plain') stream = QtCore.QDataStream(encodedData, QtCore.QIODevice.ReadOnly) newItems = [] rows = 0 while not stream.atEnd(): text = stream.readQString() newItems.append(text) rows += 1 self.beginInsertRows(QtCore.QModelIndex(), row, row + rows - 1) for text in newItems: print(text) self.__items.insert(row, {"name": text, "description": "", "status": "", "thumbnail": "", "color": [255, 255, 255]}) row += 1 self.endInsertRows() return True class CustomListDelegate(QtWidgets.QStyledItemDelegate): # Listの各アイテムの大きさ ITEM_SIZE_HINT = [300, 70] # 各アイテム内の表示に使う定数 MARGIN = 20 THUMB_AREA_WIDTH = 70 THUMB_WIDTH = 60 STATUS_AREA_WIDTH = 24 # Painterが使うブラシやペンの定義 BG_DEFAULT = QtGui.QBrush(QtGui.QColor(247,248,242)) BG_SELECTED = QtGui.QBrush(QtGui.QColor(255,255,204)) BORDER_DEFAULT = QtGui.QPen(QtGui.QColor(255,255,255), 0.5, QtCore.Qt.SolidLine) TEXT_BLACK_PEN = QtGui.QPen(QtGui.QColor(106,107,109), 0.5, QtCore.Qt.SolidLine) SHADOW_PEN = QtGui.QPen(QtGui.QColor(220,220,220, 100), 1, QtCore.Qt.SolidLine) FONT_H1 = QtGui.QFont("Corbel", 12, QtGui.QFont.Normal, True) FONT_H2 = QtGui.QFont("Corbel", 11, QtGui.QFont.Normal, True) def __init__(self, parent=None): super(CustomListDelegate, self).__init__(parent) self._pressed_index = None self._target_index = None self._target_rect = None def mousePressEvent(self, event, model, view): if event.button() == QtCore.Qt.LeftButton: self._pressed_index = view.indexAt(event.pos()) if self._pressed_index.isValid(): pixmap = QtGui.QPixmap.grabWidget(view, self._pressed_index.rect()) drag = QtGui.QDrag(view) mime_data = QtCore.QMimeData() mime_data.setData("application/x-qabstractitemmodeldatalist", QByteArray()) drag.setMimeData(mime_data) drag.setPixmap(pixmap) drag.setHotSpot(event.pos() - self._pressed_index.rect().topLeft()) drag.exec_() def mouseMoveEvent(self, event, model, view): if self._pressed_index.isValid(): if (event.pos() - self._pressed_index.rect().topLeft()).manhattanLength() > QtWidgets.QApplication.startDragDistance(): pixmap = QtGui.QPixmap.grabWidget(view, self._pressed_index.rect()) drag = QtGui.QDrag(view) mime_data = QtCore.QMimeData() mime_data.setData("application/x-qabstractitemmodeldatalist", QByteArray()) drag.setMimeData(mime_data) drag.setPixmap(pixmap) drag.setHotSpot(event.pos() - self._pressed_index.rect().topLeft()) drag.exec_() def dragEnterEvent(self, event, model, view): if event.mimeData().hasFormat("application/x-qabstractitemmodeldatalist"): event.accept() self._target_index = view.indexAt(event.pos()) self._target_rect = view.visualRect(self._target_index) return event.ignore() def dragMoveEvent(self, event, model, view): if self._target_index.isValid(): self._target_rect.setHeight(view.rowHeight(self._target_index)) if self._target_rect.contains(event.pos()): event.setDropAction(QtCore.Qt.MoveAction) event.accept() return event.ignore() def dropEvent(self, event, model, view): if event.mimeData().hasFormat("application/x-qabstractitemmodeldatalist") and self._target_index.isValid(): target_row = self._target_index.row() source_row = self._pressed_index.row() if target_row > source_row: target_row -= 1 item_data = model._CustomListModel__items.pop(source_row) model.beginInsertRows(QtCore.QModelIndex(), target_row, target_row) model._CustomListModel__items.insert(target_row, item_data) model.endInsertRows() self._pressed_index = None self._target_index = None event.accept() return event.ignore() # 背景描画メソッド paintメソッド内で呼ばれている def drawBackground(self, painter, rect, selected, color): if selected: baseColor = self.BG_SELECTED else: baseColor = self.BG_DEFAULT painter.setPen(self.BORDER_DEFAULT) painter.setBrush(baseColor) painter.drawRoundedRect(rect, 1, 1) painter.setPen(self.SHADOW_PEN) painter.drawLine(rect.bottomLeft().x(), rect.bottomLeft().y()+2, rect.bottomRight().x(), rect.bottomRight().y()+2) r = QtCore.QRect(rect.left(), rect.top(), self.THUMB_AREA_WIDTH, rect.height()) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QBrush(color)) painter.drawRoundedRect(r, 1, 1) # サムネイル描画メソッド paintメソッド内で呼ばれている def drawThumbnail(self, painter, rect, thumbnail): r = QtCore.QRect(rect.left() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, rect.top() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, self.THUMB_WIDTH, self.THUMB_WIDTH) thumbImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", thumbnail)).scaled(self.THUMB_WIDTH, self.THUMB_WIDTH) painter.drawPixmap(r, thumbImage) # 名前テキスト描画メソッド paintメソッド内で呼ばれている def drawName(self, painter, rect, name): painter.setFont(self.FONT_H1) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top(), rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, "Name : " + name) painter.setPen(self.TEXT_BLACK_PEN) painter.drawLine(r.bottomLeft(), r.bottomRight()) # ディスクリプションテキスト描画メソッド paintメソッド内で呼ばれている def drawDescription(self, painter, rect, description): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top() + rect.height() / 2, rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, description) # ステータス画像描画メソッド paintメソッド内で呼ばれている def drawStatus(self, painter, rect, status): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.right() - self.STATUS_AREA_WIDTH - self.MARGIN, rect.center().y() - self.STATUS_AREA_WIDTH / 2, self.STATUS_AREA_WIDTH, self.STATUS_AREA_WIDTH) statusIcon = "notok.png" if status == "OK": statusIcon = "ok.png" statusImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", statusIcon)) painter.drawPixmap(r, statusImage) # 描画メインメソッド def paint(self, painter, option, index): selected = False if option.state & QtWidgets.QStyle.State_Selected: selected = True name = index.data(QtCore.Qt.DisplayRole) description = index.data(DESCRIPTION_ROLE) status = index.data(STATUS_ROLE) color = index.data(QtCore.Qt.BackgroundRole) thumbnail = index.data(THUMB_ROLE) self.drawBackground(painter, option.rect, selected, color) self.drawThumbnail(painter, option.rect, thumbnail) self.drawName(painter, option.rect, name) self.drawDescription(painter, option.rect, description) self.drawStatus(painter, option.rect, status) # 各アイテムの大きさ def sizeHint(self, option, index): return QtCore.QSize(self.ITEM_SIZE_HINT[0], self.ITEM_SIZE_HINT[1]) #---------------------------------------------------------------------------- ## メインUI def maya_main_window(): main_window_ptr = omui.MQtUtil.mainWindow() return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) class GUI(QtWidgets.QMainWindow): def __init__(self, parent=maya_main_window()): super(GUI, self).__init__(parent) self.setWindowTitle('Main Window') self.resize(350, 600) self.setMaximumSize(525, 700) self.initUI() def initUI(self): # リストの設定 myListView = QtWidgets.QListView(self) myListView.setSpacing(10) myListView.setAutoFillBackground(False) url = os.path.join(CURRENT_PATH, "images", "bg.png").replace("\\", "/") myListView.setStyleSheet("background: url("+url+") center center;") myListView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) # modelを作る myListModel = CustomListModel(data = SAMPLE_DATA) # delegateを作る myListDelegate = CustomListDelegate() # listview myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) myListView.setDefaultDropAction(QtCore.Qt.MoveAction) myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragEnabled(True) myListView.setAcceptDrops(True) myListView.setDropIndicatorShown(True) # modelとdelegateをリストにセット myListView.setItemDelegate(myListDelegate) myListView.setModel(myListModel) self.setCentralWidget(myListView) # if __name__ == "__main__": try: test_dialog.close() test_dialog.deleteLater() except: pass test_dialog = GUI() test_dialog.show()
wrote on 24 Feb 2023, 19:52 last edited byI've read and tried to understand how to do, but I still fail. I've found three threads on stack overflow that touches on this, but I still can't sort it out.
https://stackoverflow.com/questions/45448863/cant-seem-to-enable-drag-drop-in-qlistview
https://stackoverflow.com/questions/73544611/pyside-qlistview-drag-and-drop
From what I gather I need to implement four methods: data, setData, insertRows and removeRows.
removeRows works well, but not insertRows. I'm trying to debug it and print out the values, but obviously I'm not getting it right. My thinking is that the values in the argument list should return the row of where to insert a new element. So where I drop it should tell me what row that is. I'm not sure exactly what I need to do here. it's all so confusing. =/import maya.OpenMayaUI as omui import os import sys from PySide2 import QtWidgets, QtGui, QtCore from shiboken2 import wrapInstance # スクリプトのフォルダ #CURRENT_PATH = os.path.dirname(__file__) CURRENT_PATH = os.path.normpath(r'D:\01_personal_files\20_MAYA_CUSTOM_CONFIG\10_maya_custom_config_2022\14_MODULES_DAG\16_dag_wip') # オリジナルのRole DESCRIPTION_ROLE = QtCore.Qt.UserRole STATUS_ROLE = QtCore.Qt.UserRole + 1 THUMB_ROLE = QtCore.Qt.UserRole + 2 # Modelに入るデータ SAMPLE_DATA = [ {"name":"Ben", "description":"Support", "status":"OK", "color":[217,204,0], "thumbnail":"ben.png"}, {"name":"Chris", "description":"Modeling", "status":"NOT OK", "color":[127,197,195], "thumbnail":"chris.png"}, {"name":"Daniel", "description":"Manager", "status":"OK", "color":[217,204,166], "thumbnail":"daniel.png"}, {"name":"Natalie", "description":"Sales", "status":"NOT OK", "color":[237,111,112], "thumbnail":"natalie.png"}, {"name":"Mike", "description":"Animation", "status":"NOT OK", "color":[127,197,195], "thumbnail":"mike.png"} ] class CustomListModel(QtCore.QAbstractListModel): def __init__(self, parent=None, data=None): super(CustomListModel, self).__init__(parent) self.__items = data def rowCount(self, parent=QtCore.QModelIndex()): return len(self.__items) def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid(): return None if not 0 <= index.row() < len(self.__items): return None if role == QtCore.Qt.DisplayRole: return self.__items[index.row()]["name"] elif role == DESCRIPTION_ROLE: return self.__items[index.row()]["description"] elif role == STATUS_ROLE: return self.__items[index.row()]["status"] elif role == THUMB_ROLE: return self.__items[index.row()]["thumbnail"] elif role == QtCore.Qt.BackgroundRole: color = self.__items[index.row()]["color"] return QtGui.QColor(color[0], color[1], color[2]) else: return None def removeRows(self, row, count, parent=QtCore.QModelIndex()): self.beginRemoveRows(parent, row, row + count - 1) del self.__items[row:row + count] self.endRemoveRows() print(f'remove {row}') return True def insertRows(self, row, count, parent=QtCore.QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) self.endInsertRows() #print(f'number of items: {len(self.__items)}') print(f'move {row} to {row + count - 1}') return True def supportedDropActions(self): return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction; def flags(self, index): defaultFlags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable if index.isValid(): return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | defaultFlags else: return QtCore.Qt.ItemIsDropEnabled | defaultFlags class CustomListDelegate(QtWidgets.QStyledItemDelegate): # Listの各アイテムの大きさ ITEM_SIZE_HINT = [300, 70] # 各アイテム内の表示に使う定数 MARGIN = 20 THUMB_AREA_WIDTH = 70 THUMB_WIDTH = 60 STATUS_AREA_WIDTH = 24 # Painterが使うブラシやペンの定義 BG_DEFAULT = QtGui.QBrush(QtGui.QColor(247,248,242)) BG_SELECTED = QtGui.QBrush(QtGui.QColor(255,255,204)) BORDER_DEFAULT = QtGui.QPen(QtGui.QColor(255,255,255), 0.5, QtCore.Qt.SolidLine) TEXT_BLACK_PEN = QtGui.QPen(QtGui.QColor(106,107,109), 0.5, QtCore.Qt.SolidLine) SHADOW_PEN = QtGui.QPen(QtGui.QColor(220,220,220, 100), 1, QtCore.Qt.SolidLine) FONT_H1 = QtGui.QFont("Corbel", 12, QtGui.QFont.Normal, True) FONT_H2 = QtGui.QFont("Corbel", 11, QtGui.QFont.Normal, True) def __init__(self, parent=None): super(CustomListDelegate, self).__init__(parent) # 背景描画メソッド paintメソッド内で呼ばれている def drawBackground(self, painter, rect, selected, color): if selected: baseColor = self.BG_SELECTED else: baseColor = self.BG_DEFAULT painter.setPen(self.BORDER_DEFAULT) painter.setBrush(baseColor) painter.drawRoundedRect(rect, 1, 1) painter.setPen(self.SHADOW_PEN) painter.drawLine(rect.bottomLeft().x(), rect.bottomLeft().y()+2, rect.bottomRight().x(), rect.bottomRight().y()+2) r = QtCore.QRect(rect.left(), rect.top(), self.THUMB_AREA_WIDTH, rect.height()) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QBrush(color)) painter.drawRoundedRect(r, 1, 1) # サムネイル描画メソッド paintメソッド内で呼ばれている def drawThumbnail(self, painter, rect, thumbnail): r = QtCore.QRect(rect.left() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, rect.top() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, self.THUMB_WIDTH, self.THUMB_WIDTH) thumbImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", thumbnail)).scaled(self.THUMB_WIDTH, self.THUMB_WIDTH) painter.drawPixmap(r, thumbImage) # 名前テキスト描画メソッド paintメソッド内で呼ばれている def drawName(self, painter, rect, name): painter.setFont(self.FONT_H1) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top(), rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, "Name : " + name) painter.setPen(self.TEXT_BLACK_PEN) painter.drawLine(r.bottomLeft(), r.bottomRight()) # ディスクリプションテキスト描画メソッド paintメソッド内で呼ばれている def drawDescription(self, painter, rect, description): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top() + rect.height() / 2, rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, description) # ステータス画像描画メソッド paintメソッド内で呼ばれている def drawStatus(self, painter, rect, status): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.right() - self.STATUS_AREA_WIDTH - self.MARGIN, rect.center().y() - self.STATUS_AREA_WIDTH / 2, self.STATUS_AREA_WIDTH, self.STATUS_AREA_WIDTH) statusIcon = "notok.png" if status == "OK": statusIcon = "ok.png" statusImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", statusIcon)) painter.drawPixmap(r, statusImage) # 描画メインメソッド def paint(self, painter, option, index): selected = False if option.state & QtWidgets.QStyle.State_Selected: selected = True name = index.data(QtCore.Qt.DisplayRole) description = index.data(DESCRIPTION_ROLE) status = index.data(STATUS_ROLE) color = index.data(QtCore.Qt.BackgroundRole) thumbnail = index.data(THUMB_ROLE) self.drawBackground(painter, option.rect, selected, color) self.drawThumbnail(painter, option.rect, thumbnail) self.drawName(painter, option.rect, name) self.drawDescription(painter, option.rect, description) self.drawStatus(painter, option.rect, status) # 各アイテムの大きさ def sizeHint(self, option, index): return QtCore.QSize(self.ITEM_SIZE_HINT[0], self.ITEM_SIZE_HINT[1]) #---------------------------------------------------------------------------- ## メインUI def maya_main_window(): main_window_ptr = omui.MQtUtil.mainWindow() return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) class GUI(QtWidgets.QMainWindow): def __init__(self, parent=maya_main_window()): super(GUI, self).__init__(parent) self.setWindowTitle('Main Window') self.resize(350, 600) self.setMaximumSize(525, 700) self.initUI() def initUI(self): # リストの設定 myListView = QtWidgets.QListView(self) myListView.setSpacing(10) myListView.setAutoFillBackground(False) url = os.path.join(CURRENT_PATH, "images", "bg.png").replace("\\", "/") myListView.setStyleSheet("background: url("+url+") center center;") myListView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) # modelを作る myListModel = CustomListModel(data = SAMPLE_DATA) # delegateを作る myListDelegate = CustomListDelegate() # listview myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) myListView.setDefaultDropAction(QtCore.Qt.MoveAction) myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragEnabled(True) myListView.setAcceptDrops(True) myListView.setDropIndicatorShown(True) # modelとdelegateをリストにセット myListView.setItemDelegate(myListDelegate) myListView.setModel(myListModel) self.setCentralWidget(myListView) # if __name__ == "__main__": try: test_dialog.close() test_dialog.deleteLater() except: pass test_dialog = GUI() test_dialog.show()
-
I've read and tried to understand how to do, but I still fail. I've found three threads on stack overflow that touches on this, but I still can't sort it out.
https://stackoverflow.com/questions/45448863/cant-seem-to-enable-drag-drop-in-qlistview
https://stackoverflow.com/questions/73544611/pyside-qlistview-drag-and-drop
From what I gather I need to implement four methods: data, setData, insertRows and removeRows.
removeRows works well, but not insertRows. I'm trying to debug it and print out the values, but obviously I'm not getting it right. My thinking is that the values in the argument list should return the row of where to insert a new element. So where I drop it should tell me what row that is. I'm not sure exactly what I need to do here. it's all so confusing. =/import maya.OpenMayaUI as omui import os import sys from PySide2 import QtWidgets, QtGui, QtCore from shiboken2 import wrapInstance # スクリプトのフォルダ #CURRENT_PATH = os.path.dirname(__file__) CURRENT_PATH = os.path.normpath(r'D:\01_personal_files\20_MAYA_CUSTOM_CONFIG\10_maya_custom_config_2022\14_MODULES_DAG\16_dag_wip') # オリジナルのRole DESCRIPTION_ROLE = QtCore.Qt.UserRole STATUS_ROLE = QtCore.Qt.UserRole + 1 THUMB_ROLE = QtCore.Qt.UserRole + 2 # Modelに入るデータ SAMPLE_DATA = [ {"name":"Ben", "description":"Support", "status":"OK", "color":[217,204,0], "thumbnail":"ben.png"}, {"name":"Chris", "description":"Modeling", "status":"NOT OK", "color":[127,197,195], "thumbnail":"chris.png"}, {"name":"Daniel", "description":"Manager", "status":"OK", "color":[217,204,166], "thumbnail":"daniel.png"}, {"name":"Natalie", "description":"Sales", "status":"NOT OK", "color":[237,111,112], "thumbnail":"natalie.png"}, {"name":"Mike", "description":"Animation", "status":"NOT OK", "color":[127,197,195], "thumbnail":"mike.png"} ] class CustomListModel(QtCore.QAbstractListModel): def __init__(self, parent=None, data=None): super(CustomListModel, self).__init__(parent) self.__items = data def rowCount(self, parent=QtCore.QModelIndex()): return len(self.__items) def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid(): return None if not 0 <= index.row() < len(self.__items): return None if role == QtCore.Qt.DisplayRole: return self.__items[index.row()]["name"] elif role == DESCRIPTION_ROLE: return self.__items[index.row()]["description"] elif role == STATUS_ROLE: return self.__items[index.row()]["status"] elif role == THUMB_ROLE: return self.__items[index.row()]["thumbnail"] elif role == QtCore.Qt.BackgroundRole: color = self.__items[index.row()]["color"] return QtGui.QColor(color[0], color[1], color[2]) else: return None def removeRows(self, row, count, parent=QtCore.QModelIndex()): self.beginRemoveRows(parent, row, row + count - 1) del self.__items[row:row + count] self.endRemoveRows() print(f'remove {row}') return True def insertRows(self, row, count, parent=QtCore.QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) self.endInsertRows() #print(f'number of items: {len(self.__items)}') print(f'move {row} to {row + count - 1}') return True def supportedDropActions(self): return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction; def flags(self, index): defaultFlags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable if index.isValid(): return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | defaultFlags else: return QtCore.Qt.ItemIsDropEnabled | defaultFlags class CustomListDelegate(QtWidgets.QStyledItemDelegate): # Listの各アイテムの大きさ ITEM_SIZE_HINT = [300, 70] # 各アイテム内の表示に使う定数 MARGIN = 20 THUMB_AREA_WIDTH = 70 THUMB_WIDTH = 60 STATUS_AREA_WIDTH = 24 # Painterが使うブラシやペンの定義 BG_DEFAULT = QtGui.QBrush(QtGui.QColor(247,248,242)) BG_SELECTED = QtGui.QBrush(QtGui.QColor(255,255,204)) BORDER_DEFAULT = QtGui.QPen(QtGui.QColor(255,255,255), 0.5, QtCore.Qt.SolidLine) TEXT_BLACK_PEN = QtGui.QPen(QtGui.QColor(106,107,109), 0.5, QtCore.Qt.SolidLine) SHADOW_PEN = QtGui.QPen(QtGui.QColor(220,220,220, 100), 1, QtCore.Qt.SolidLine) FONT_H1 = QtGui.QFont("Corbel", 12, QtGui.QFont.Normal, True) FONT_H2 = QtGui.QFont("Corbel", 11, QtGui.QFont.Normal, True) def __init__(self, parent=None): super(CustomListDelegate, self).__init__(parent) # 背景描画メソッド paintメソッド内で呼ばれている def drawBackground(self, painter, rect, selected, color): if selected: baseColor = self.BG_SELECTED else: baseColor = self.BG_DEFAULT painter.setPen(self.BORDER_DEFAULT) painter.setBrush(baseColor) painter.drawRoundedRect(rect, 1, 1) painter.setPen(self.SHADOW_PEN) painter.drawLine(rect.bottomLeft().x(), rect.bottomLeft().y()+2, rect.bottomRight().x(), rect.bottomRight().y()+2) r = QtCore.QRect(rect.left(), rect.top(), self.THUMB_AREA_WIDTH, rect.height()) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QBrush(color)) painter.drawRoundedRect(r, 1, 1) # サムネイル描画メソッド paintメソッド内で呼ばれている def drawThumbnail(self, painter, rect, thumbnail): r = QtCore.QRect(rect.left() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, rect.top() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, self.THUMB_WIDTH, self.THUMB_WIDTH) thumbImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", thumbnail)).scaled(self.THUMB_WIDTH, self.THUMB_WIDTH) painter.drawPixmap(r, thumbImage) # 名前テキスト描画メソッド paintメソッド内で呼ばれている def drawName(self, painter, rect, name): painter.setFont(self.FONT_H1) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top(), rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, "Name : " + name) painter.setPen(self.TEXT_BLACK_PEN) painter.drawLine(r.bottomLeft(), r.bottomRight()) # ディスクリプションテキスト描画メソッド paintメソッド内で呼ばれている def drawDescription(self, painter, rect, description): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top() + rect.height() / 2, rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, description) # ステータス画像描画メソッド paintメソッド内で呼ばれている def drawStatus(self, painter, rect, status): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.right() - self.STATUS_AREA_WIDTH - self.MARGIN, rect.center().y() - self.STATUS_AREA_WIDTH / 2, self.STATUS_AREA_WIDTH, self.STATUS_AREA_WIDTH) statusIcon = "notok.png" if status == "OK": statusIcon = "ok.png" statusImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", statusIcon)) painter.drawPixmap(r, statusImage) # 描画メインメソッド def paint(self, painter, option, index): selected = False if option.state & QtWidgets.QStyle.State_Selected: selected = True name = index.data(QtCore.Qt.DisplayRole) description = index.data(DESCRIPTION_ROLE) status = index.data(STATUS_ROLE) color = index.data(QtCore.Qt.BackgroundRole) thumbnail = index.data(THUMB_ROLE) self.drawBackground(painter, option.rect, selected, color) self.drawThumbnail(painter, option.rect, thumbnail) self.drawName(painter, option.rect, name) self.drawDescription(painter, option.rect, description) self.drawStatus(painter, option.rect, status) # 各アイテムの大きさ def sizeHint(self, option, index): return QtCore.QSize(self.ITEM_SIZE_HINT[0], self.ITEM_SIZE_HINT[1]) #---------------------------------------------------------------------------- ## メインUI def maya_main_window(): main_window_ptr = omui.MQtUtil.mainWindow() return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) class GUI(QtWidgets.QMainWindow): def __init__(self, parent=maya_main_window()): super(GUI, self).__init__(parent) self.setWindowTitle('Main Window') self.resize(350, 600) self.setMaximumSize(525, 700) self.initUI() def initUI(self): # リストの設定 myListView = QtWidgets.QListView(self) myListView.setSpacing(10) myListView.setAutoFillBackground(False) url = os.path.join(CURRENT_PATH, "images", "bg.png").replace("\\", "/") myListView.setStyleSheet("background: url("+url+") center center;") myListView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) # modelを作る myListModel = CustomListModel(data = SAMPLE_DATA) # delegateを作る myListDelegate = CustomListDelegate() # listview myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) myListView.setDefaultDropAction(QtCore.Qt.MoveAction) myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragEnabled(True) myListView.setAcceptDrops(True) myListView.setDropIndicatorShown(True) # modelとdelegateをリストにセット myListView.setItemDelegate(myListDelegate) myListView.setModel(myListModel) self.setCentralWidget(myListView) # if __name__ == "__main__": try: test_dialog.close() test_dialog.deleteLater() except: pass test_dialog = GUI() test_dialog.show()
wrote on 25 Feb 2023, 22:58 last edited byI've been reading the docs and I don't really get what I need to do.
For instance, since in this example I'm using a custom model with for my data. I'm just not sure of all the things I need to implement myself. dragging works, and obviosly I can delete the dragged object.
I just can't seem to get to the point where I move it in place.
Is data encoded and decoded from the model when I drag and drop in the same ListView, say for a moving operation in this case? It would help tremendously if I could just learn what methods I need to implement myself. Because I'm really stuck now...Ideally I'd like it to look like this:
https://reactscript.com/wp-content/uploads/2019/01/Drag-Drop-List-Component-react-movable.gifNot sure if it's possible. But even so, just to get drag-and-drop-move to work would be a dream.
-
I've been reading the docs and I don't really get what I need to do.
For instance, since in this example I'm using a custom model with for my data. I'm just not sure of all the things I need to implement myself. dragging works, and obviosly I can delete the dragged object.
I just can't seem to get to the point where I move it in place.
Is data encoded and decoded from the model when I drag and drop in the same ListView, say for a moving operation in this case? It would help tremendously if I could just learn what methods I need to implement myself. Because I'm really stuck now...Ideally I'd like it to look like this:
https://reactscript.com/wp-content/uploads/2019/01/Drag-Drop-List-Component-react-movable.gifNot sure if it's possible. But even so, just to get drag-and-drop-move to work would be a dream.
-
wrote on 26 Feb 2023, 22:17 last edited by
@SGaist Thank you.
I tried changing to that and I understand that should be what to use, but that alone doesn't make it work.
I would need a real example to see howclass CustomListModel(QtCore.QAbstractListModel):
should look and work.
I feel like I'm just getting confused because I don't know or understand exactly what I'm doing wrong or what needs to be done. -
@SGaist Thank you.
I tried changing to that and I understand that should be what to use, but that alone doesn't make it work.
I would need a real example to see howclass CustomListModel(QtCore.QAbstractListModel):
should look and work.
I feel like I'm just getting confused because I don't know or understand exactly what I'm doing wrong or what needs to be done.wrote on 26 Feb 2023, 23:06 last edited by@Geuse
So far as I can see, you already havedel self.__items[row:row + count]
in
removeRows()
, you just need to do the insertion ininsertRows()
. Then drag&drop reordering (QAbstractItemView.InternalMove
) is done by Qt calling both of these methods. -
@Geuse
So far as I can see, you already havedel self.__items[row:row + count]
in
removeRows()
, you just need to do the insertion ininsertRows()
. Then drag&drop reordering (QAbstractItemView.InternalMove
) is done by Qt calling both of these methods.wrote on 27 Feb 2023, 00:31 last edited by@JonB said in Add reorder functionality to CustomListModel class using QStyledItemDelegate:
insertRows()
Ok, thank you, it's just that my insertion doesn't work. I'm trying to debug it, but from the print out I'm doing, it seems I'm not getting the correct rows that I can use to put the elemtn in. I get the same row I'm dragging from,
From the method:def insertRows(self, row, count, parent=QtCore.QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) self.endInsertRows() print(f'move {row} to {row + count - 1}')
As an example, I'm dragging from element 0, The printout says "move 0 to 0".
_
And does it work like this: when you remove an element, you dump the data temporarly and then when you insert, you create a new element and grab that data. This is the purpose of the mime data?I removed the implementations of mimeTypes, mimeData, canDropMimeData, dropMimeData.
But I need to implement them myself? -
wrote on 27 Feb 2023, 02:23 last edited by
If the goal is to have working code rather than learning how to implement a model, I think OP will be better off using QStandardItemModel. In C++, a custom model has the potential advantage of superior performance. With python, I'm skeptical.
QListView view; QStandardItemModel model; for(int i = 0; i < 10; i++) { QStandardItem *item = new QStandardItem(QString::number(i)); item->setDropEnabled(false); model.appendRow(item); } view.setModel(&model); view.setDragDropMode(QAbstractItemView::DragDropMode::InternalMove); view.show();
-
@JonB said in Add reorder functionality to CustomListModel class using QStyledItemDelegate:
insertRows()
Ok, thank you, it's just that my insertion doesn't work. I'm trying to debug it, but from the print out I'm doing, it seems I'm not getting the correct rows that I can use to put the elemtn in. I get the same row I'm dragging from,
From the method:def insertRows(self, row, count, parent=QtCore.QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) self.endInsertRows() print(f'move {row} to {row + count - 1}')
As an example, I'm dragging from element 0, The printout says "move 0 to 0".
_
And does it work like this: when you remove an element, you dump the data temporarly and then when you insert, you create a new element and grab that data. This is the purpose of the mime data?I removed the implementations of mimeTypes, mimeData, canDropMimeData, dropMimeData.
But I need to implement them myself?wrote on 27 Feb 2023, 08:16 last edited by JonB@Geuse
But yourinsertRows()
is empty betweenbeginInsertRows()
andendInsertRows()
. It does not do anything about inserting any rows there, and it should/must do. Just as you implementdel self.__items[row:row + count]
in betweenbeginRemoveRows()
andendRemoveRows()
. So you need the "opposite" of that, something likeself._items.insert(row, count)
. You need to insertcount
new rows staring atrow
, the rows should be "blank" (as in, a list/array with the right number of columns but blank/default values in each column). I don't think Python actually offers to insert more than one element at a time into a list, so you'll need to do it in afor i in range(count)
loop.You should not be thinking about "moving" or "copying" or "mime data". Nor is it your job to "dump/restore" whatever data is already there. The Qt
QListView
caller looks after that, your job is just to respond to the calls to remove and insert correctly.Having said the above. If it's all getting too much you might follow @jeremy_k's suggestion and use a
QStandardItemModel
for this, which will already have the correctremove
/insertRows()
implemented for you. -
@Geuse
But yourinsertRows()
is empty betweenbeginInsertRows()
andendInsertRows()
. It does not do anything about inserting any rows there, and it should/must do. Just as you implementdel self.__items[row:row + count]
in betweenbeginRemoveRows()
andendRemoveRows()
. So you need the "opposite" of that, something likeself._items.insert(row, count)
. You need to insertcount
new rows staring atrow
, the rows should be "blank" (as in, a list/array with the right number of columns but blank/default values in each column). I don't think Python actually offers to insert more than one element at a time into a list, so you'll need to do it in afor i in range(count)
loop.You should not be thinking about "moving" or "copying" or "mime data". Nor is it your job to "dump/restore" whatever data is already there. The Qt
QListView
caller looks after that, your job is just to respond to the calls to remove and insert correctly.Having said the above. If it's all getting too much you might follow @jeremy_k's suggestion and use a
QStandardItemModel
for this, which will already have the correctremove
/insertRows()
implemented for you.wrote on 27 Feb 2023, 11:07 last edited by@jeremy_k Thank you, I didn't know about that. I will look into it.
@JonB Yes, I removed my "insertion code" because it wasn't working. So I thought I should do a print out of the values instead and maybe that's where I made my mistake? the values for count and row that I print out seems to be incorrect. I mean, the values are not the one I anticipated so I'm getting confused.
I will try your suggestion. And since I'm a beginner I din't know about QStandardItemModel and it is to be used instead of QAbstractListModel?
Thank you so much.
-
@jeremy_k Thank you, I didn't know about that. I will look into it.
@JonB Yes, I removed my "insertion code" because it wasn't working. So I thought I should do a print out of the values instead and maybe that's where I made my mistake? the values for count and row that I print out seems to be incorrect. I mean, the values are not the one I anticipated so I'm getting confused.
I will try your suggestion. And since I'm a beginner I din't know about QStandardItemModel and it is to be used instead of QAbstractListModel?
Thank you so much.
wrote on 27 Feb 2023, 15:17 last edited byOk, I kinda see how this works. Maybe....?
It seems thatinsertRows
is run before theremoveRows
.
The problem I'm facing is that I don't know how to get the index of the item I want to insert from the row or count argument in the method wheninsertRows
is run. It just spits out seemingly random values. I'm sure they aren't random, but not the values I'm anticipating. I can't make sense of it so how can I tell the method to create a new row in between two elements?The
removeRows
returns the expected row number that I can use to delete that item from the list with. -
Ok, I kinda see how this works. Maybe....?
It seems thatinsertRows
is run before theremoveRows
.
The problem I'm facing is that I don't know how to get the index of the item I want to insert from the row or count argument in the method wheninsertRows
is run. It just spits out seemingly random values. I'm sure they aren't random, but not the values I'm anticipating. I can't make sense of it so how can I tell the method to create a new row in between two elements?The
removeRows
returns the expected row number that I can use to delete that item from the list with.wrote on 27 Feb 2023, 15:33 last edited by@Geuse
We are going round in circles. You keep saying/asking the same thing and the answer remains the same. Just implementinsertRows()
given its parameters. Something like:self.beginInsertRows(parent, row, row + count - 1) for i in range(count): self.__items.insert(row + i, [ 0, "", None, ... ]) self.endInsertRows()
Stop worrying about when it's being called, just make it do what it's asked to do.
-
@Geuse
We are going round in circles. You keep saying/asking the same thing and the answer remains the same. Just implementinsertRows()
given its parameters. Something like:self.beginInsertRows(parent, row, row + count - 1) for i in range(count): self.__items.insert(row + i, [ 0, "", None, ... ]) self.endInsertRows()
Stop worrying about when it's being called, just make it do what it's asked to do.
wrote on 27 Feb 2023, 16:45 last edited by@JonB Yes, I just fail to understand how it all works. I'm very grateful for your patience.
And if I understand you correctly,
this:
self.__items.insert(row + i, [ 0, "", None, ... ])
obviously doesn't work in my example. It says
# TypeError: join() argument must be str, bytes, or os.PathLike object, not 'NoneType'
I can't seem to understand how I should alter that to make it work in my case. Mostly because I don't understand this
[ 0, "", None, ... ]
.My data looks like this
{"name":"Mike", "description":"Animation", "status":"NOT OK", "color":[127,197,195], "thumbnail":"mike.png"}
I can do something like this:
def insertRows(self, row, count, parent=QtCore.QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) for i in range(count): self.__items.insert(row + i, {"name":"", "description":"", "status":"", "color":[217,204,0], "thumbnail":""}) self.endInsertRows() return True
and it will create a new row with this data, but at arbitrary rows each time. sometimes at the end, sometimes in the middle. And that confuses me.
But I gather I should not put in my own data like this. -
@JonB Yes, I just fail to understand how it all works. I'm very grateful for your patience.
And if I understand you correctly,
this:
self.__items.insert(row + i, [ 0, "", None, ... ])
obviously doesn't work in my example. It says
# TypeError: join() argument must be str, bytes, or os.PathLike object, not 'NoneType'
I can't seem to understand how I should alter that to make it work in my case. Mostly because I don't understand this
[ 0, "", None, ... ]
.My data looks like this
{"name":"Mike", "description":"Animation", "status":"NOT OK", "color":[127,197,195], "thumbnail":"mike.png"}
I can do something like this:
def insertRows(self, row, count, parent=QtCore.QModelIndex()): self.beginInsertRows(parent, row, row + count - 1) for i in range(count): self.__items.insert(row + i, {"name":"", "description":"", "status":"", "color":[217,204,0], "thumbnail":""}) self.endInsertRows() return True
and it will create a new row with this data, but at arbitrary rows each time. sometimes at the end, sometimes in the middle. And that confuses me.
But I gather I should not put in my own data like this.@Geuse what do you mean by arbitrary ?
-
@SGaist I understand it can't be arbitrary, but my item isn't always being inserted where I drop it, between the two items in the list. very seldom is it inserted correctly. Most of the time it is added to the bottom.
wrote on 27 Feb 2023, 20:20 last edited by GeuseI got a suggestion from ChatGPT and it kind of works better now. I'm not neglecting the help or effort all of you are providing. I'm just trying things to see if anything makes it act less weird. I understand I have a special use case and that I'm using Maya(though code wise it shouldn't be an issue).
But I removed both theinsertRows
andremoveRows
and addedmoveRows
instead and it works better actually. Though it doesn't insert them correct each time. There has to be something funky in how it calculates the height of the element?
And I can't drag from below and insert above, then it crashes.
I just thought I should post the code hereimport maya.OpenMayaUI as omui import os import sys from PySide2 import QtWidgets, QtGui, QtCore from shiboken2 import wrapInstance # スクリプトのフォルダ #CURRENT_PATH = os.path.dirname(__file__) CURRENT_PATH = os.path.normpath(r'D:\01_personal_files\20_MAYA_CUSTOM_CONFIG\10_maya_custom_config_2022\14_MODULES_DAG\16_dag_wip') # オリジナルのRole DESCRIPTION_ROLE = QtCore.Qt.UserRole STATUS_ROLE = QtCore.Qt.UserRole + 1 THUMB_ROLE = QtCore.Qt.UserRole + 2 # Modelに入るデータ SAMPLE_DATA = [ {"name":"Ben", "description":"Support", "status":"OK", "color":[217,204,0], "thumbnail":"ben.png"}, {"name":"Chris", "description":"Modeling", "status":"NOT OK", "color":[127,197,195], "thumbnail":"chris.png"}, {"name":"Daniel", "description":"Manager", "status":"OK", "color":[217,204,166], "thumbnail":"daniel.png"}, {"name":"Natalie", "description":"Sales", "status":"NOT OK", "color":[237,111,112], "thumbnail":"natalie.png"}, {"name":"Mike", "description":"Animation", "status":"NOT OK", "color":[127,197,195], "thumbnail":"mike.png"} ] class CustomListModel(QtCore.QAbstractListModel): def __init__(self, parent=None, data=None): super(CustomListModel, self).__init__(parent) self.__items = data def rowCount(self, parent=QtCore.QModelIndex()): return len(self.__items) def data(self, index, role=QtCore.Qt.DisplayRole): if not index.isValid(): return None if not 0 <= index.row() < len(self.__items): return None if role == QtCore.Qt.DisplayRole: return self.__items[index.row()]["name"] elif role == DESCRIPTION_ROLE: return self.__items[index.row()]["description"] elif role == STATUS_ROLE: return self.__items[index.row()]["status"] elif role == THUMB_ROLE: return self.__items[index.row()]["thumbnail"] elif role == QtCore.Qt.BackgroundRole: color = self.__items[index.row()]["color"] return QtGui.QColor(color[0], color[1], color[2]) else: return None def moveRows(self, sourceParent, sourceRow, count, destinationParent, destinationChild): if sourceParent != destinationParent: return False self.beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild) for i in range(count): if destinationChild > sourceRow: self.__items.insert(destinationChild - count + i, self.__items.pop(sourceRow)) else: self.__items.insert(destinationChild + i, self.__items.pop(sourceRow)) self.endMoveRows() return True def supportedDropActions(self): return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction; def flags(self, index): defaultFlags = QtCore.QAbstractListModel.flags(self, index) if index.isValid(): return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | defaultFlags else: return QtCore.Qt.ItemIsDropEnabled | defaultFlags class CustomListDelegate(QtWidgets.QStyledItemDelegate): # Listの各アイテムの大きさ ITEM_SIZE_HINT = [300, 70] # 各アイテム内の表示に使う定数 MARGIN = 20 THUMB_AREA_WIDTH = 70 THUMB_WIDTH = 60 STATUS_AREA_WIDTH = 24 # Painterが使うブラシやペンの定義 BG_DEFAULT = QtGui.QBrush(QtGui.QColor(247,248,242)) BG_SELECTED = QtGui.QBrush(QtGui.QColor(255,255,204)) BORDER_DEFAULT = QtGui.QPen(QtGui.QColor(255,255,255), 0.5, QtCore.Qt.SolidLine) TEXT_BLACK_PEN = QtGui.QPen(QtGui.QColor(106,107,109), 0.5, QtCore.Qt.SolidLine) SHADOW_PEN = QtGui.QPen(QtGui.QColor(220,220,220, 100), 1, QtCore.Qt.SolidLine) FONT_H1 = QtGui.QFont("Corbel", 12, QtGui.QFont.Normal, True) FONT_H2 = QtGui.QFont("Corbel", 11, QtGui.QFont.Normal, True) def __init__(self, parent=None): super(CustomListDelegate, self).__init__(parent) # 背景描画メソッド paintメソッド内で呼ばれている def drawBackground(self, painter, rect, selected, color): if selected: baseColor = self.BG_SELECTED else: baseColor = self.BG_DEFAULT painter.setPen(self.BORDER_DEFAULT) painter.setBrush(baseColor) painter.drawRoundedRect(rect, 1, 1) painter.setPen(self.SHADOW_PEN) painter.drawLine(rect.bottomLeft().x(), rect.bottomLeft().y()+2, rect.bottomRight().x(), rect.bottomRight().y()+2) r = QtCore.QRect(rect.left(), rect.top(), self.THUMB_AREA_WIDTH, rect.height()) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QBrush(color)) painter.drawRoundedRect(r, 1, 1) # サムネイル描画メソッド paintメソッド内で呼ばれている def drawThumbnail(self, painter, rect, thumbnail): r = QtCore.QRect(rect.left() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, rect.top() + (self.THUMB_AREA_WIDTH - self.THUMB_WIDTH) / 2, self.THUMB_WIDTH, self.THUMB_WIDTH) thumbImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", thumbnail)).scaled(self.THUMB_WIDTH, self.THUMB_WIDTH) painter.drawPixmap(r, thumbImage) # 名前テキスト描画メソッド paintメソッド内で呼ばれている def drawName(self, painter, rect, name): painter.setFont(self.FONT_H1) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top(), rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, "Name : " + name) painter.setPen(self.TEXT_BLACK_PEN) painter.drawLine(r.bottomLeft(), r.bottomRight()) # ディスクリプションテキスト描画メソッド paintメソッド内で呼ばれている def drawDescription(self, painter, rect, description): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.left() + self.THUMB_AREA_WIDTH + self.MARGIN, rect.top() + rect.height() / 2, rect.width() - self.THUMB_AREA_WIDTH - self.STATUS_AREA_WIDTH - self.MARGIN * 3, rect.height() / 2) painter.drawText(r, QtCore.Qt.AlignVCenter|QtCore.Qt.AlignLeft, description) # ステータス画像描画メソッド paintメソッド内で呼ばれている def drawStatus(self, painter, rect, status): painter.setFont(self.FONT_H2) painter.setPen(self.TEXT_BLACK_PEN) r = QtCore.QRect(rect.right() - self.STATUS_AREA_WIDTH - self.MARGIN, rect.center().y() - self.STATUS_AREA_WIDTH / 2, self.STATUS_AREA_WIDTH, self.STATUS_AREA_WIDTH) statusIcon = "notok.png" if status == "OK": statusIcon = "ok.png" statusImage = QtGui.QPixmap(os.path.join(CURRENT_PATH, "images", statusIcon)) painter.drawPixmap(r, statusImage) # 描画メインメソッド def paint(self, painter, option, index): selected = False if option.state & QtWidgets.QStyle.State_Selected: selected = True name = index.data(QtCore.Qt.DisplayRole) description = index.data(DESCRIPTION_ROLE) status = index.data(STATUS_ROLE) color = index.data(QtCore.Qt.BackgroundRole) thumbnail = index.data(THUMB_ROLE) self.drawBackground(painter, option.rect, selected, color) self.drawThumbnail(painter, option.rect, thumbnail) self.drawName(painter, option.rect, name) self.drawDescription(painter, option.rect, description) self.drawStatus(painter, option.rect, status) # 各アイテムの大きさ def sizeHint(self, option, index): return QtCore.QSize(self.ITEM_SIZE_HINT[0], self.ITEM_SIZE_HINT[1]) #---------------------------------------------------------------------------- ## メインUI def maya_main_window(): main_window_ptr = omui.MQtUtil.mainWindow() return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) class GUI(QtWidgets.QMainWindow): def __init__(self, parent=maya_main_window()): super(GUI, self).__init__(parent) self.setWindowTitle('Main Window') self.resize(350, 600) self.setMaximumSize(525, 700) self.initUI() def initUI(self): # リストの設定 myListView = QtWidgets.QListView(self) myListView.setSpacing(10) myListView.setAutoFillBackground(False) url = os.path.join(CURRENT_PATH, "images", "bg.png").replace("\\", "/") myListView.setStyleSheet("background: url("+url+") center center;") myListView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) # modelを作る myListModel = CustomListModel(data = SAMPLE_DATA) # delegateを作る myListDelegate = CustomListDelegate() # listview myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove) myListView.setDefaultDropAction(QtCore.Qt.MoveAction) myListView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) myListView.setDragEnabled(True) myListView.setAcceptDrops(True) myListView.setDropIndicatorShown(True) # modelとdelegateをリストにセット myListView.setItemDelegate(myListDelegate) myListView.setModel(myListModel) # self.setCentralWidget(myListView) # if __name__ == "__main__": try: test_dialog.close() test_dialog.deleteLater() except: pass test_dialog = GUI() test_dialog.show()
1/22