I'm trying to implement a function to move nodes by dragging in a single tree (QStandardItemModel, PyQt5, Python). My nodes are classes created by multiple inheritance like class Node(A, QStandardItem). When I drag and drop this node, only properties from QStandardItem parent class are moved, everything from the class A is lost.
Here is minimal working example:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import (Qt, QModelIndex, QMimeData, QByteArray)
from PyQt5.QtWidgets import (QApplication, QMainWindow, QAbstractItemView, QPushButton, QVBoxLayout, QWidget)
from PyQt5.QtGui import QStandardItemModel, QStandardItem
class A:
def __init__(self, *args, **kwargs):
self.symbol = None
super().__init__(*args, **kwargs) # forwards all unused arguments
class Node(A, QStandardItem):
def __init__(self, symbol, *args, **kwargs):
super().__init__(*args, **kwargs)
self.symbol = symbol
self.setText("Node " + str(self.symbol))
class DragDropTreeModel(QStandardItemModel):
def __init__(self, parent=None):
super(DragDropTreeModel, self).__init__(parent)
def supportedDropActions(self):
return Qt.MoveAction
def flags(self, index):
defaultFlags = QStandardItemModel.flags(self, index)
if index.isValid():
return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags
else:
return Qt.ItemIsDropEnabled | defaultFlags```
class DemoDragDrop(QWidget):
def __init__(self, parent=None):
super(DemoDragDrop, self).__init__(parent)
self.setWindowTitle('drag&drop in PyQt5')
self.resize(480, 320)
self.initUi()
def initUi(self):
self.vLayout = QVBoxLayout(self)
self.TreeView = QtWidgets.QTreeView(self)
self.TreeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.TreeView.setDragEnabled(True)
self.TreeView.setAcceptDrops(True)
self.TreeView.setDropIndicatorShown(True)
self.ddm = DragDropTreeModel()
self.TreeView.setDragDropMode(QAbstractItemView.InternalMove)
self.TreeView.setDefaultDropAction(Qt.MoveAction)
self.TreeView.setDragDropOverwriteMode(False)
self.root_node = Node('root')
self.ddm.appendRow(self.root_node)
node_a = Node('a')
self.root_node.appendRow(node_a)
node_b = Node('b')
self.root_node.appendRow(node_b)
node_c = Node('c')
self.root_node.appendRow(node_c)
self.TreeView.setModel(self.ddm)
self.printButton = QPushButton("Print")
self.vLayout.addWidget(self.TreeView)
self.vLayout.addWidget(self.printButton)
self.printButton.clicked.connect(self.printModelProp)
def printModelProp(self):
cur_ind = self.TreeView.currentIndex()
obj = self.ddm.itemFromIndex(cur_ind)
obj: Node
print(obj.symbol)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle('fusion')
window = DemoDragDrop()
window.show()
sys.exit(app.exec_())
In this example, select the node from the tree and click "Print" button - it will print to the console, 'a' for "Node a", 'b' for "Node b" and so on. Then move one node, select it and push "Print" again. The application will crash with the error AttributeError: 'QStandardItem' object has no attribute 'symbol'.
Then I tried to move a node manually by overriding methods mimeData and dropMimeData. I saved row and column indexes in mimeData and tried to get this node from the index in dropMimeData to move it. But this doesn't work because the index has changed meanwhile.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import (Qt, QModelIndex, QMimeData, QByteArray)
from PyQt5.QtWidgets import (QApplication, QMainWindow, QAbstractItemView, QPushButton, QVBoxLayout, QWidget)
from PyQt5.QtGui import QStandardItemModel, QStandardItem
class A:
def __init__(self, *args, **kwargs):
self.symbol = None
super().__init__(*args, **kwargs) # forwards all unused arguments
class Node(A, QStandardItem):
def __init__(self, symbol, *args, **kwargs):
super().__init__(*args, **kwargs)
self.symbol = symbol
self.setText("Node " + str(self.symbol))
class DragDropTreeModel(QStandardItemModel):
def __init__(self, parent=None):
super(DragDropTreeModel, self).__init__(parent)
def supportedDropActions(self):
return Qt.MoveAction
def flags(self, index):
defaultFlags = QStandardItemModel.flags(self, index)
if index.isValid():
return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags
else:
return Qt.ItemIsDropEnabled | defaultFlags
def mimeData(self, indexes) -> QtCore.QMimeData:
m_data = super().mimeData(indexes)
if (m_data):
r = indexes[0].row()
c = indexes[0].column()
obj = self.itemFromIndex(indexes[0])
print(f"row:{r}, column:{c}, type:{type(obj)}, ind:{indexes[0]}")
m_data.setData('row', QByteArray.number(indexes[0].row()))
m_data.setData('col', QByteArray.number(indexes[0].column()))
return m_data
def dropMimeData(self, data: QtCore.QMimeData, action: QtCore.Qt.DropAction, row: int, column: int,
parent: QtCore.QModelIndex) -> bool:
if data is None or action != QtCore.Qt.MoveAction:
return False
_row = data.data('row').toInt()[0]
_col = data.data('col').toInt()[0]
old_index = self.index(_row, _col)
current_index = parent
old_item = self.takeItem(old_index.row(), old_index.column())
parent_item = self.itemFromIndex(parent)
parent_item.appendRow(old_item)
return True
class DemoDragDrop(QWidget):
def __init__(self, parent=None):
super(DemoDragDrop, self).__init__(parent)
self.setWindowTitle('drag&drop in PyQt5')
self.resize(480, 320)
self.initUi()
def initUi(self):
self.vLayout = QVBoxLayout(self)
self.TreeView = QtWidgets.QTreeView(self)
self.TreeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.TreeView.setDragEnabled(True)
self.TreeView.setAcceptDrops(True)
self.TreeView.setDropIndicatorShown(True)
self.ddm = DragDropTreeModel()
self.TreeView.setDragDropMode(QAbstractItemView.InternalMove)
self.TreeView.setDefaultDropAction(Qt.MoveAction)
self.TreeView.setDragDropOverwriteMode(False)
self.root_node = Node('root')
self.ddm.appendRow(self.root_node)
node_a = Node('a')
self.root_node.appendRow(node_a)
node_b = Node('b')
self.root_node.appendRow(node_b)
node_c = Node('c')
self.root_node.appendRow(node_c)
self.TreeView.setModel(self.ddm)
self.printButton = QPushButton("Print")
self.vLayout.addWidget(self.TreeView)
self.vLayout.addWidget(self.printButton)
self.printButton.clicked.connect(self.printModelProp)
def printModelProp(self):
cur_ind = self.TreeView.currentIndex()
obj = self.ddm.itemFromIndex(cur_ind)
obj: Node
print(obj.symbol)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle('fusion')
window = DemoDragDrop()
window.show()
sys.exit(app.exec_())
In this example the tree will break.
I wonder is there a way to move the node without destroying it. It seems to me a wrong way to recreate the object (in mimeData() and dropMimeData()) when it's only needed to change the index.
So, the questions are: how to implement this move correctly, and is it possible without destroying the node (it can be a member of some list for example)?
Since nobody replied, I post an answer to my own question (maybe it's not perfect but it works).
First, I found that my design doesn't fit Qt's drag-n-drop design. One way is not to subclass an item using multiple inheritance (like class Node(A, QStandardItem)) but rather to use composition and keep class A within QStandardItem using roles. Then no need to override any methods.
In my case nodes are using multiple inheritance so I did override dropEvent method and move row (extract it from the model and insert in the new place). It's very simple example just for demonstration, so no checks what kind of object is dropping and so on.
If somebody has better idea - you're welcome to comment or write your own solution
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import (Qt, QModelIndex, QMimeData, QByteArray)
from PyQt5.QtWidgets import (QApplication, QMainWindow, QAbstractItemView, QPushButton, QVBoxLayout, QWidget)
from PyQt5.QtGui import QStandardItemModel, QStandardItem
class A:
def __init__(self, *args, **kwargs):
self.symbol = None
super().__init__(*args, **kwargs) # forwards all unused arguments
class Node(A, QStandardItem):
def __init__(self, symbol, *args, **kwargs):
super().__init__(*args, **kwargs)
self.symbol = symbol
self.setData(symbol, QtCore.Qt.UserRole)
self.setText("Node " + str(self.symbol))
def get_sym(self):
return self.data(QtCore.Qt.UserRole)
class DragDropTreeModel(QStandardItemModel):
def __init__(self, parent=None):
super(DragDropTreeModel, self).__init__(parent)
def supportedDropActions(self):
return Qt.MoveAction
def flags(self, index):
defaultFlags = QStandardItemModel.flags(self, index)
if index.isValid():
return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags
else:
return Qt.ItemIsDropEnabled | defaultFlags
class MyTreeView(QtWidgets.QTreeView):
def __init__(self, parent=None):
super(MyTreeView, self).__init__(parent)
self.setAcceptDrops(True)
self.setDragEnabled(True)
def dropEvent(self, event):
index = self.indexAt(event.pos())
model = self.model()
dest_node = model.itemFromIndex(index)
if dest_node is None:
return
source_index = self.currentIndex()
source_node = model.itemFromIndex(source_index)
source_node: Node
sourse_parent = source_node.parent()
taken_row = sourse_parent.takeRow(source_index.row())
dest_parent = dest_node
if dest_node != sourse_parent:
dest_parent = dest_node.parent()
if dest_parent is None:
dest_parent = dest_node
dest_parent.insertRow(index.row(), taken_row)
class DemoDragDrop(QWidget):
def __init__(self, parent=None):
super(DemoDragDrop, self).__init__(parent)
self.setWindowTitle('drag&drop in PyQt5')
self.resize(480, 320)
self.initUi()
def initUi(self):
self.vLayout = QVBoxLayout(self)
self.TreeView = MyTreeView(self)# QtWidgets.QTreeView(self)#
self.TreeView.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.TreeView.setDragEnabled(True)
self.TreeView.setAcceptDrops(True)
self.TreeView.setDropIndicatorShown(True)
self.ddm = DragDropTreeModel()
self.TreeView.setDragDropMode(QAbstractItemView.InternalMove)
self.TreeView.setDefaultDropAction(Qt.MoveAction)
self.TreeView.setDragDropOverwriteMode(False)
self.root_node = Node('root')
self.ddm.appendRow(self.root_node)
node_1 = Node('1')
self.root_node.appendRow(node_1)
node_2 = Node('2')
self.root_node.appendRow(node_2)
node_d = Node('d')
node_2.appendRow(node_d)
node_a = Node('a')
font = QtGui.QFont()
font.setBold(True)
node_a.setFont(font)
node_1.appendRow(node_a)
node_b = Node('b')
node_1.appendRow(node_b)
node_c = Node('c')
node_1.appendRow(node_c)
self.TreeView.setModel(self.ddm)
self.printButton = QPushButton("Print")
self.vLayout.addWidget(self.TreeView)
self.vLayout.addWidget(self.printButton)
self.printButton.clicked.connect(self.printModelProp)
def drop(self):
print('drop')
def printModelProp(self):
cur_ind = self.TreeView.currentIndex()
obj = self.ddm.itemFromIndex(cur_ind)
obj: Node
print(obj.symbol)
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle('fusion')
window = DemoDragDrop()
window.show()
sys.exit(app.exec_())
Related
I am using QCompleter to implement auto-completion on a QLineEdit widget:
from PySide2 import QtGui
from PySide2.QtCore import Qt
from PySide2.QtGui import QStandardItem
from PySide2.QtWidgets import QCompleter, QWidget, QLineEdit, QFormLayout, QApplication
class SuggestionPlaceModel(QtGui.QStandardItemModel):
def __init__(self, parent=None):
super(SuggestionPlaceModel, self).__init__(parent)
def search(self, text):
self.clear()
data = [{'text': f"{text} {i}"} for i in range(10)]
for i, row in enumerate(data):
item = QStandardItem(row['text'])
self.appendRow(item)
class Completer(QCompleter):
def splitPath(self, path):
self.model().search(path)
return super(Completer, self).splitPath(path)
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._model = SuggestionPlaceModel(self)
completer = Completer(self)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setModel(self._model)
lineedit = QLineEdit()
lineedit.setCompleter(completer)
lay = QFormLayout(self)
lay.addRow("Location: ", lineedit)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Here is a result:
QUESTION: How can I customize the SuggestionPlaceModel class so that the search result can include icons, horizontal separaters, different fonts, different font sizes, etc like this?
A possible solution is to use a custom delegate where the icon is set, in the case of html you can use a QLabel that supports rich text.
import random
from PySide2 import QtCore, QtGui, QtWidgets
class Delegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super(Delegate, self).initStyleOption(option, index)
option.text = ""
def paint(self, painter, option, index):
if isinstance(option.widget, QtWidgets.QAbstractItemView):
option.widget.openPersistentEditor(index)
super(Delegate, self).paint(painter, option, index)
def createEditor(self, parent, option, index):
editor = QtWidgets.QLabel(index.data(), parent)
editor.setContentsMargins(0, 0, 0, 0)
editor.setText(index.data(QtCore.Qt.UserRole))
return editor
class SuggestionPlaceModel(QtGui.QStandardItemModel):
def search(self, text):
self.clear()
for i in range(10):
html = f"{text}-{i} <b>Stack</b> <i>Overflow</i>"
plain = QtGui.QTextDocumentFragment.fromHtml(html).toPlainText()
pixmap = QtGui.QPixmap(128, 128)
pixmap.fill(QtGui.QColor(*random.sample(range(255), 4)))
item = QtGui.QStandardItem(plain)
item.setData(html, QtCore.Qt.UserRole)
item.setIcon(QtGui.QIcon(pixmap))
self.appendRow(item)
class Completer(QtWidgets.QCompleter):
def splitPath(self, path):
self.model().search(path)
return super(Completer, self).splitPath(path)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._model = SuggestionPlaceModel(self)
completer = Completer(self)
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
completer.setModel(self._model)
delegate = Delegate(completer.popup())
completer.popup().setItemDelegate(delegate)
lineedit = QtWidgets.QLineEdit()
lineedit.setCompleter(completer)
lay = QtWidgets.QFormLayout(self)
lay.addRow("Location: ", lineedit)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
I'm trying to wrap my head around some ModelView on PyQT5.
I have a QListView, which can display data stored in an QAbstractListModel.
But i'd like to have each line of my QListView displaying a complex Widget created in QDesigner.
I've created a widget with a QLabel, a spacer, and a QPushButton.
I'd like to have each elements of my QListView using this widget to display my model data
Here is the basic code with a simple ModelView QListView
import typing
from PyQt5 import QtCore
from PyQt5.QtCore import QModelIndex, Qt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QListView
class MyListModel(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super().__init__(parent)
self.data_list = []
def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
if role == Qt.DisplayRole:
return self.data_list[index.row()]
def rowCount(self, parent: QModelIndex = ...) -> int:
return len(self.data_list)
data = ["emotion", "unliving", "brutally", "torch", "donut", "comet"]
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
layout = QWidget()
layout.setLayout(QVBoxLayout())
list_view = QListView()
model = MyListModel()
model.data_list = data
list_view.setModel(model)
layout.layout().addWidget(list_view)
layout.resize(640, 480)
layout.show()
sys.exit(app.exec_())
I've tried to create a QStyledItemDelegate, and set the delegate on the model, but i can't make it work.
class LineDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.line = Ui_Form()
self.line.setupUi(parent)
def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:
# Not sure what to put here
The doc seems to said that a Delegate should be used to edit data inside the view, not having a complex widget view.
https://doc.qt.io/qtforpython/overviews/model-view-programming.html#a-simple-delegate
Is there anything possible to have what i want ?
I'm really not sure how to approch this.
You must use a delegate so that you create an editor for each item, and then use the openPersistentEditor method to open the editors, it is not necessary to override the paint method.
import typing
from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt
from PyQt5.QtWidgets import (
QApplication,
QListView,
QStyledItemDelegate,
QVBoxLayout,
QWidget,
)
class Form(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Form()
self.ui.setupUi(self)
class MyListModel(QAbstractListModel):
def __init__(self, parent=None):
super().__init__(parent)
self._data_list = []
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> typing.Any:
if role == Qt.DisplayRole:
return self._data_list[index.row()]
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self._data_list)
#property
def data_list(self):
return self._data_list
#data_list.setter
def data_list(self, data_list):
self.beginResetModel()
self._data_list = data_list.copy()
self.endResetModel()
class Delegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
return Form(parent)
"""def setEditorData(self, editor, index):
editor.label.setText(index.data())"""
data = ["emotion", "unliving", "brutally", "torch", "donut", "comet"]
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
list_view = QListView()
model = MyListModel()
model.data_list = data
list_view.setModel(model)
delegate = Delegate(list_view)
list_view.setItemDelegate(delegate)
for i in range(model.rowCount()):
index = model.index(i, 0)
list_view.openPersistentEditor(index)
container = QWidget()
layout = QVBoxLayout(container)
layout.addWidget(list_view)
container.resize(640, 480)
container.show()
sys.exit(app.exec_())
I'm making a custom QTreeView with QFileSystem model, and I have a MouseMoveEvent set up to print the path of the item that is hovered over.
I'm way down the rabbit hole and doing all kinds of weird things to make this work.
Here is the latest minimal reproducible code:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class MainWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
self.resize(500, 300)
self.layout = QVBoxLayout()
self.setLayout(self.layout)
self.myList = CustomTreeWidget()
self.myList.model.setRootPath("/Volumes/Home/User/Desktop/testsrc")
self.myList.setObjectName("/Volumes/Home/User/Desktop/testsrc")
self.layout.addWidget(self.myList)
class CustomTreeWidget(QTreeView):
def __init__(self):
super().__init__()
self.model = QFileSystemModel()
self.model.setFilter(QDir.NoDotAndDotDot | QDir.Files)
self.setModel(self.model)
self.setAlternatingRowColors(True)
self.setDragDropMode(QAbstractItemView.DragDrop)
self.setIndentation(0)
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.setMouseTracking(True)
self.model.directoryLoaded.connect(self._runwhenloaded)
def _runwhenloaded(self):
self.setRootIndex(self.model.index(self.objectName()))
self.model.setRootPath(self.objectName())
def mouseMoveEvent(self, event):
prev = ""
if self.selectedIndexes():
prev = self.selectedIndexes()[0]
x = event.x()
y = event.y()
self.setSelection(QRect(x, y, 1, 1), QItemSelectionModel.ClearAndSelect)
self.setCurrentIndex(self.selectedIndexes()[0])
print(self.model.filePath(self.currentIndex()))
if prev:
self.setCurrentIndex(prev)
# pos = QCursor.pos()
# indexat = self.indexAt(pos).row() # why -1?
# print(indexat) # why -1?
# print(self.indexAt(pos).row())
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Obviously this example is not proper at all, as it destroys multiple selections and scrolls to the previously selected item whenever the mouse moves, and just a hack in general.
I've gone through many iterations and read everything I could put my hands on but I can't figure it out.
The closest answer seems to be HERE, but it's in C and I don't understand it.
So the question is: How to print the file path when the mouse hovers over an item in this QTreeView?
A possible solution is to create an event filter that tracks the hover events and, according to that information, emits a signal that has the QModelIndex:
import sys
from PyQt5.QtCore import (
pyqtSignal,
pyqtSlot,
Qt,
QDir,
QEvent,
QModelIndex,
QObject,
QPersistentModelIndex,
QStandardPaths,
)
from PyQt5.QtWidgets import (
QAbstractItemView,
QApplication,
QFileSystemModel,
QMainWindow,
QTreeView,
)
from PyQt5 import sip
class HoverViewHelper(QObject):
hovered = pyqtSignal(QModelIndex)
def __init__(self, view):
super().__init__(view)
self._current_index = QPersistentModelIndex()
if not isinstance(view, QAbstractItemView):
raise TypeError(f"The {view} must be of type QAbstractItemView")
self._view = view
self.view.viewport().setAttribute(Qt.WA_Hover)
self.view.viewport().installEventFilter(self)
#property
def view(self):
return self._view
def eventFilter(self, obj, event):
if sip.isdeleted(self.view):
return True
if self.view.viewport() is obj:
if event.type() in (QEvent.HoverMove, QEvent.HoverEnter):
p = event.pos()
index = self.view.indexAt(p)
self._update_index(index)
elif event.type() == QEvent.HoverLeave:
if self._current_index.isValid():
self._update_index(QModelIndex())
return super().eventFilter(obj, event)
def _update_index(self, index):
pindex = QPersistentModelIndex(index)
if pindex != self._current_index:
self._current_index = pindex
self.hovered.emit(QModelIndex(self._current_index))
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.model = QFileSystemModel()
self.model.setRootPath(QDir.rootPath())
self.view = QTreeView()
self.view.setModel(self.model)
path = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation)
self.view.setRootIndex(self.model.index(path))
self.setCentralWidget(self.view)
helper = HoverViewHelper(self.view)
helper.hovered.connect(self.handle_hovered)
#pyqtSlot(QModelIndex)
def handle_hovered(self, index):
if not index.isValid():
return
path = self.model.filePath(index)
print(f"path: {path}")
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.resize(640, 480)
w.show()
app.exec_()
if __name__ == "__main__":
main()
i did create a qtableview in which column 1 has images and successfully made ImageDelegate that handles showing images in cells but the problem is with showing them with original ratio they appear to be stretched and maybe a bit smaller
image example:
the sample database used: https://gofile.io/d/LKNdsi
the code:
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import (QApplication, QComboBox, QDialog,
QDialogButtonBox, QFormLayout, QGridLayout, QGroupBox, QHBoxLayout,
QLabel, QLineEdit, QMenu, QMenuBar, QPushButton, QSpinBox, QTextEdit,
QVBoxLayout, QCheckBox, QTableWidgetItem, QAbstractItemView, QTableWidget, QTableView, QStyledItemDelegate)
import sqlite3
class Main(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
conn = sqlite3.connect("math.db")
conn.text_factory = bytes
self.cur = conn.cursor()
data = self.cur.execute("select * from problems;").fetchall();conn.close()
self.dims = (lambda x: (len(x), len(x[0])))(data) #(rows number, col number)
self.model = TableModel([[j.decode("utf-8", "ignore") if u!=1 else j for u, j in enumerate(i)] for i in data])
self.table.setModel(self.model)
self.table.setItemDelegateForColumn(1, ImageDelegate(self))
#self.table.resizeColumnsToContents()
#self.table.resizeRowsToContents()
self.setCentralWidget(self.table)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
class ImageDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
## [ Here is the part that needs something added or fixed ]
pixmap = QtGui.QPixmap()
pixmap.loadFromData(index.data(), "jpg")
painter.drawPixmap(option.rect, pixmap)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
main = Main()
main.resize(600,600)
main.show()
app.exec_()
and here is a snippet code i used to use with QtableWidget that fixed this issue for me in old days but now with QtableView things appears to be different somehow.
snippt class:
class ScaledPixmapLabel(QtWidgets.QLabel):
def __init__(self):
super().__init__()
self.setScaledContents(True)
def paintEvent(self, event):
if self.pixmap():
pm = self.pixmap()
originalRatio = pm.width() / pm.height()
currentRatio = self.width() / self.height()
if originalRatio != currentRatio:
qp = QtGui.QPainter(self)
pm = self.pixmap().scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
rect = QtCore.QRect(0, 0, pm.width(), pm.height())
rect.moveCenter(self.rect().center())
qp.drawPixmap(rect, pm)
return
super().paintEvent(event)
You have to make the size of column "1" and all its rows depend on the content, that is, on the image. On the other hand, instead of making a personalized painting, I established it as an icon. Also implement that the icon is placed in the center:
from PyQt5 import QtWidgets, QtGui, QtCore
import sqlite3
class ImageDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super(ImageDelegate, self).initStyleOption(option, index)
pixmap = QtGui.QPixmap()
pixmap.loadFromData(index.data(), "jpg")
if not pixmap.isNull():
option.features |= QtWidgets.QStyleOptionViewItem.HasDecoration
option.icon = QtGui.QIcon(pixmap)
option.decorationSize = pixmap.size() / pixmap.devicePixelRatio()
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
return self._data[index.row()][index.column()]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
class ImageCenterProxyStyle(QtWidgets.QProxyStyle):
def subElementRect(self, subElement, option, widget=None):
r = super(ImageCenterProxyStyle, self).subElementRect(
subElement, option, widget
)
if subElement == QtWidgets.QStyle.SE_ItemViewItemDecoration:
r.moveCenter(option.rect.center())
return r
class Main(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
conn = sqlite3.connect("math.db")
conn.text_factory = bytes
self.cur = conn.cursor()
data = self.cur.execute("select * from problems;").fetchall()
conn.close()
self.dims = (lambda x: (len(x), len(x[0])))(data) # (rows number, col number)
self.model = TableModel(
[
[j.decode("utf-8", "ignore") if u != 1 else j for u, j in enumerate(i)]
for i in data
]
)
self.table.setModel(self.model)
self.table.setItemDelegateForColumn(1, ImageDelegate(self))
self.table.resizeRowsToContents()
self.table.horizontalHeader().setSectionResizeMode(
1, QtWidgets.QHeaderView.ResizeToContents
)
proxy = ImageCenterProxyStyle(self.table.style())
self.table.setStyle(proxy)
self.setCentralWidget(self.table)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main = Main()
main.resize(600, 600)
main.show()
app.exec_()
This code will only show same image in the same row. How can i pass different path for image in ImageDelegate? Thanks
class testQT4(QtGui.QTableView):
def __init__(self, parent=None):
QtGui.QTableView.__init__(self, parent)
self.setItemDelegateForColumn(1, ImageDelegate(parent))
#table header
header = [ 'ID','image']
tabledata = [[1,2],[3,4]]
#create table model
self.model = MyTableModel(tabledata, header, self)
#set table model
self.setModel(self.model)
class ImageDelegate(QtGui.QStyledItemDelegate):
def __init__(self, parent):
print dir(self)
QtGui.QStyledItemDelegate.__init__(self, parent)
def paint(self, painter, option, index):
painter.fillRect(option.rect, QtGui.QColor(191,222,185))
# path = "path\to\my\image.jpg"
self.path = "image.bmp"
image = QtGui.QImage(str(self.path))
pixmap = QtGui.QPixmap.fromImage(image)
pixmap.scaled(50, 40, QtCore.Qt.KeepAspectRatio)
painter.drawPixmap(option.rect, pixmap)
In the paint method of the delegate you have access to the model via index.model(). You could then query your model for the data (image) you want to show. For example by using the Qt.UserRole for the data function of the model.
Another solution, which might even be easier, is that the data function of the model can return one of QIcon, QPixmap, QImage and QColor for the Qt.DecorationRole. In this case no Delegate is necessary.
As an example the following code will put in Icon in the (only) field of the table:
from PyQt4 import QtGui, QtCore
import PyQt4.uic
# using QtDesigner to just put a TableView in a Widget
Form, Base = PyQt4.uic.loadUiType(r'TableView.ui')
class TableModel( QtCore.QAbstractTableModel ):
def __init__(self, parent=None):
super(TableModel,self).__init__(parent)
def rowCount(self, parent=QtCore.QModelIndex()):
return 1
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if index.isValid():
if role==QtCore.Qt.DecorationRole:
return QtGui.QIcon("ChipScope.png")
return None
class TableViewUi(Form, Base ):
def __init__(self, parent=None):
Form.__init__(self)
Base.__init__(self,parent)
def setupUi(self, parent):
Form.setupUi(self,parent)
model = TableModel()
self.tableView.setModel(model)
if __name__=="__main__":
app = QtGui.QApplication(sys.argv)
MainWindow = QtGui.QMainWindow()
ui = TableViewUi()
ui.setupUi(ui)
MainWindow.setCentralWidget(ui)
MainWindow.show()
sys.exit(app.exec_())