QT, Python, QTreeView, custom widget, setData - lose reference after drag and drop - python

This question is similar to the one in this this topic Preserve QStandardItem subclasses in drag and drop but with issue that I cant find a good solution for. That topic partially helps but fail on more complex task.
When I create an item in QTreeView I put that item in my array but when I use drag&Drop the item gets deleted and I no longer have access to it. I know that its because drag and drop copies the item and not moves it so I should use setData. I cant setData to be an object because even then the object gets copied and I lose reference to it.
Here is an example
itemsArray = self.addNewRow
def addNewRow(self)
'''some code with more items'''
itemHolder = QStandardItem("ProgressBarItem")
widget = QProgressBar()
itemHolder.setData(widget)
inx = self.model.rowCount()
self.model.setItem(inx, 0, itemIcon)
self.model.setItem(inx, 1, itemName)
self.model.setItem(inx, 2, itemHolder)
ix = self.model.index(inx,2,QModelIndex())
self.treeView.setIndexWidget(ix, widget)
return [itemHolder, itemA, itemB, itemC]
#Simplified functionality
data = [xxx,xxx,xxx]
for items in itemsArray:
items[0].data().setPercentage(data[0])
items[1].data().setText(data[1])
items[2].data().setChecked(data[2])
The code above works if I won't move the widget. The second I drag/drop I lose reference I lose updates on all my items and I get crash.
RuntimeError: wrapped C/C++ object of type QProgressBar has been deleted
The way I can think of of fixing this problem is to loop over entire treeview recursively over each row/child and on name match update item.... Problem is that I will be refreshing treeview every 0.5 second and have 500+ rows with 5-15 items each. Meaning... I don't think that will be very fast/efficient... if I want to loop over 5 000 items every 0.5 second...
Can some one suggest how I could solve this problem? Perhaps I can edit dropEvent so it does not copy/paste item but rather move item.... This way I would not lose my object in array

Qt can only serialize objects that can be stored in a QVariant, so it's no surprise that this won't work with a QWidget. But even if it could serialize widgets, I still don't think it would work, because index-widgets belong to the view, not the model.
Anyway, I think you will have to keep references to the widgets separately, and only store a simple key in the model items. Then once the items are dropped, you can retrieve the widgets and reset them in the view.
Here's a working demo script:
from PyQt4 import QtCore, QtGui
class TreeView(QtGui.QTreeView):
def __init__(self, *args, **kwargs):
super(TreeView, self).__init__(*args, **kwargs)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setAllColumnsShowFocus(True)
self.setModel(QtGui.QStandardItemModel(self))
self._widgets = {}
self._dropping = False
self._droprange = range(0)
def dropEvent(self, event):
self._dropping = True
super(TreeView, self).dropEvent(event)
for row in self._droprange:
item = self.model().item(row, 2)
self.setIndexWidget(item.index(), self._widgets[item.data()])
self._droprange = range(0)
self._dropping = False
def rowsInserted(self, parent, start, end):
super(TreeView, self).rowsInserted(parent, start, end)
if self._dropping:
self._droprange = range(start, end + 1)
def addNewRow(self, name):
model = self.model()
itemIcon = QtGui.QStandardItem()
pixmap = QtGui.QPixmap(16, 16)
pixmap.fill(QtGui.QColor(name))
itemIcon.setIcon(QtGui.QIcon(pixmap))
itemName = QtGui.QStandardItem(name.title())
itemHolder = QtGui.QStandardItem('ProgressBarItem')
widget = QtGui.QProgressBar()
widget.setValue(5 * (model.rowCount() + 1))
key = id(widget)
self._widgets[key] = widget
itemHolder.setData(key)
model.appendRow([itemIcon, itemName, itemHolder])
self.setIndexWidget(model.indexFromItem(itemHolder), widget)
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.treeView = TreeView()
for name in 'red yellow green purple blue orange'.split():
self.treeView.addNewRow(name)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.treeView)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 150, 600, 400)
window.show()
sys.exit(app.exec_())

Related

Drag and Drop a Widget in QTreeWidget

I have a QTreeWidget where I want to move around the items. This works fine with the cities (see example) but not with the buttons.
First Question: What do I need to do to make the buttons moveable like the cities?
Second Question: If I move the cities I get a copy, but I want to move the city only (delete from original place)
class Example(QTreeWidget):
def __init__(self):
super().__init__()
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setWindowTitle('Drag and Drop Button')
self.setGeometry(300, 300, 550, 450)
self.cities = QTreeWidgetItem(self)
self.cities.setText(0,"Cities")
self.setDragDropMode(self.InternalMove)
osloItem = QTreeWidgetItem(self.cities)
osloItem.setText(0,"Oslo")
bergenItem = QTreeWidgetItem(self.cities)
bergenItem.setText(0,"Bergen")
stavangerItem = QTreeWidgetItem(self.cities)
stavangerItem.setText(0,"Stavanger")
button1 = QPushButton('Button1',self)
button2 = QPushButton("Button2",self)
label = QLabel("dragHandle")
container = QWidget()
containerLayout = QHBoxLayout()
container.setLayout(containerLayout)
containerLayout.addWidget(label)
containerLayout.addWidget(button1)
containerLayout.addWidget(button2)
b1 = QTreeWidgetItem(self.cities)
self.setItemWidget(b1,0,container)
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
if __name__ == '__main__':
main()
An important aspect about index widgets (including persistent editors) is that the view takes complete ownership on the widget, and whenever index is removed for any reason, the widget associated to that widget gets automatically destroyed. This also happens when calling again setIndexWidget() with another widget for the same index.
There is absolutely no way to prevent that, the destruction is done internally (by calling deleteLater()), and reparenting the widget won't change anything.
The only way to "preserve" the widget is to set a "fake" container as the index widget, create a layout for it, and add the actual widget to it.
Then, the problem comes when using drag&drop, because item views always use serialization of items, even when the InternalMove flag is set.
This means that when an item is moved, the original index gets removed (and the widget along with it, including widgets for any child item).
The solution, then, is to "capture" the drop operation before it's performed, reparent the contents of the container with a similar copy, proceed with the base implementation and then restore the widgets for the new target indexes.
Since we are dealing with tree models, this automatically calls for recursive functions, both for reparenting and restoration.
In the following code I've created an implementation that should be compatible for all standard views (QTreeView, QTableView, QListView) and their higher level widgets. I didn't consider QColumnView, as it's a quite peculiar view and rarely has such requirement.
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Handle(QWidget):
'''
A custom widget that shows a handle for item dragging whenever
the editor doesn't support it
'''
def __init__(self):
super().__init__()
self.setFixedWidth(self.style().pixelMetric(
QStyle.PM_ToolBarHandleExtent))
def paintEvent(self, event):
qp = QPainter(self)
opt = QStyleOption()
opt.initFrom(self)
style = self.style()
opt.state |= style.State_Horizontal
style.drawPrimitive(style.PE_IndicatorToolBarHandle, opt, qp)
class Example(QTreeWidget):
def __init__(self):
super().__init__()
self.setColumnCount(2)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setWindowTitle('Drag and Drop Button')
self.cities = QTreeWidgetItem(self)
self.cities.setText(0, 'Cities')
self.setDragDropMode(self.InternalMove)
osloItem = QTreeWidgetItem(self.cities)
osloItem.setText(0,'Oslo')
bergenItem = QTreeWidgetItem(self.cities)
bergenItem.setText(0,'Bergen')
stavangerItem = QTreeWidgetItem(self.cities)
stavangerItem.setText(0, 'Stavanger')
button1 = QPushButton('Button1', self)
button2 = QPushButton('Button2', self)
label = QLabel('dragHandle')
container = QWidget()
containerLayout = QGridLayout()
container.setLayout(containerLayout)
containerLayout.addWidget(label)
containerLayout.addWidget(button1, 0, 1)
containerLayout.addWidget(button2, 0, 2)
b1 = QTreeWidgetItem(self.cities)
self.setItemWidget(b1, 0, container)
anotherItem = QTreeWidgetItem(self, ['Whatever'])
anotherChild = QTreeWidgetItem(anotherItem, ['Another child'])
grandChild = QTreeWidgetItem(anotherChild, ['a', 'b'])
self.setItemWidget(grandChild, 1, QPushButton('Whatever'))
self.expandAll()
height = self.header().sizeHint().height() + self.frameWidth() * 2
index = self.model().index(0, 0)
while index.isValid():
height += self.rowHeight(index)
index = self.indexBelow(index)
self.resize(500, height + 100)
def setItemWidget(self, item, column, widget, addHandle=False):
if widget and addHandle or widget.layout() is None:
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(Handle())
layout.addWidget(widget)
widget = container
super().setItemWidget(item, column, widget)
def createNewContainer(self, index):
'''
create a copy of the container and its layout, then reparent all
child widgets by adding them to the new layout
'''
oldWidget = self.indexWidget(index)
if oldWidget is None:
return
oldLayout = oldWidget.layout()
if oldLayout is None:
return
newContainer = oldWidget.__class__()
newLayout = oldLayout.__class__(newContainer)
newLayout.setContentsMargins(oldLayout.contentsMargins())
newLayout.setSpacing(oldLayout.spacing())
if isinstance(oldLayout, QGridLayout):
newLayout.setHorizontalSpacing(
oldLayout.horizontalSpacing())
newLayout.setVerticalSpacing(
oldLayout.verticalSpacing())
for r in range(oldLayout.rowCount()):
newLayout.setRowStretch(r,
oldLayout.rowStretch(r))
newLayout.setRowMinimumHeight(r,
oldLayout.rowMinimumHeight(r))
for c in range(oldLayout.columnCount()):
newLayout.setColumnStretch(c,
oldLayout.columnStretch(c))
newLayout.setColumnMinimumWidth(c,
oldLayout.columnMinimumWidth(c))
items = []
for i in range(oldLayout.count()):
layoutItem = oldLayout.itemAt(i)
if not layoutItem:
continue
if layoutItem.widget():
item = layoutItem.widget()
elif layoutItem.layout():
item = layoutItem.layout()
elif layoutItem.spacerItem():
item = layoutItem.spacerItem()
if isinstance(oldLayout, QBoxLayout):
items.append((item, oldLayout.stretch(i), layoutItem.alignment()))
else:
items.append((item, ) + oldLayout.getItemPosition(i))
for item, *args in items:
if isinstance(item, QWidget):
newLayout.addWidget(item, *args)
elif isinstance(item, QLayout):
newLayout.addLayout(item, *args)
else:
if isinstance(newLayout, QBoxLayout):
newLayout.addSpacerItem(item)
else:
newLayout.addItem(item, *args)
return newContainer
def getNewIndexWidgets(self, parent, row):
'''
A recursive function that returns a nested list of widgets and those of
child indexes, by creating new parent containers in the meantime to
avoid their destruction
'''
model = self.model()
rowItems = []
for column in range(model.columnCount()):
index = model.index(row, column, parent)
childItems = []
if column == 0:
for childRow in range(model.rowCount(index)):
childItems.append(self.getNewIndexWidgets(index, childRow))
rowItems.append((
self.createNewContainer(index),
childItems
))
return rowItems
def restoreIndexWidgets(self, containers, parent, startRow=0, startCol=0):
'''
Restore index widgets based on the previously created nested list of
widgets, based on the new parent and drop row (and column for tables)
'''
model = self.model()
for row, rowItems in enumerate(containers, startRow):
for column, (widget, childItems) in enumerate(rowItems, startCol):
index = model.index(row, column, parent)
if widget:
self.setIndexWidget(index, widget)
self.restoreIndexWidgets(childItems, index)
def dropEvent(self, event):
'''
Assume that the selected index is the source of the drag and drop
operation, then create a nested list of possible index widget that
are reparented *before* the drop is applied to avoid their destruction
and then restores them based on the drop index
'''
containers = []
if event.source() == self:
dropTarget = QPersistentModelIndex(self.indexAt(event.pos()))
dropPos = self.dropIndicatorPosition()
selection = self.selectedIndexes()
if selection and len(set(i.row() for i in selection)) == 1:
index = selection[0]
containers.append(
self.getNewIndexWidgets(index.parent(), index.row()))
super().dropEvent(event)
if containers:
model = self.model()
startCol = 0
if dropPos == self.OnViewport:
parent = QModelIndex()
dropRow = model.rowCount() - 1
else:
if dropPos == self.OnItem:
if isinstance(self, QTreeView):
# tree views move items as *children* of the drop
# target when the action is *on* an item
parent = model.index(
dropTarget.row(), dropTarget.column(),
dropTarget.parent())
dropRow = model.rowCount(parent) - 1
else:
# QTableView and QListView use the drop index as target
# for the operation, so the parent index is actually
# the root index of the view
parent = self.rootIndex()
dropRow = model.rowCount(dropTarget.parent()) - 1
dropRow = dropTarget.row()
startCol = dropTarget.column() - index.column()
else:
if isinstance(self, QTreeView):
parent = dropTarget.parent()
else:
parent = self.rootIndex()
if dropPos == self.AboveItem:
dropRow = dropTarget.row() - 1
else:
dropRow = dropTarget.row() + 1
# try to restore the widgets based on the above
self.restoreIndexWidgets(containers, parent, dropRow, startCol)
# ensure that all geometries are updated after the drop operation
self.updateGeometries()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
Notes:
while the above is done for QTreeWidget, it can be easily ported for generic usage of other views; the only difference is the optional override of setItemWidget (which would be setIndexWidget() for basic views or setCellWidget() for QTableWidget, and with the relative argument differences;
item views (and "item widget views") and models might have different behaviors on drag&drop operations, and depending on their settings (starting with dragDropMode); always consider the default behavior of those classes, and also the defaultDropAction;
please study all the code above with extreme care: it's a generic example, drag and drop operations are quite complex, and you might need custom behavior; I strongly suggest to patiently and carefully read that code to understand its aspects whenever you need more advanced control over d&d, especially when dealing with tree structures;
remember that index widgets, while useful, are often problematic; most of the times, using a custom delegate might be a better choice, even for interactive widgets such as buttons or complex widgets; remember that the base source of item views is the model data, including eventual "widgets" (or even editors and their contents);

PyQt6 deleting custom widget with nested class causes the program to crash

I am using Python 3.9.5.
I have encountered some serious problem in my project and here is a minimum reproducible example code, along with some descriptions.
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class Editor(QTextEdit):
doubleClicked = pyqtSignal(QTextEdit)
def __init__(self):
super().__init__()
self.setReadOnly(True)
def mouseDoubleClickEvent(self, e: QMouseEvent) -> None:
self.doubleClicked.emit(self)
class textcell(QGroupBox):
def __init__(self, text):
super().__init__()
self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
self.label = QLabel(text)
self.apply = makebutton('Apply')
self.apply.hide()
self.editor = Editor()
self.editor.doubleClicked.connect(lambda: self.editor.setReadOnly(False))
self.editor.doubleClicked.connect(self.apply.show)
self.hbox = QHBoxLayout()
self.hbox.addSpacerItem(spacer)
self.hbox.addWidget(self.apply)
self.vbox = QVBoxLayout()
self.vbox.addWidget(self.label)
self.vbox.addWidget(self.editor)
self.vbox.addLayout(self.hbox)
self.setLayout(self.vbox)
self.apply.clicked.connect(self.on_ApplyClick)
def on_ApplyClick(self):
self.editor.setReadOnly(True)
self.apply.hide()
def makebutton(text):
button = QPushButton()
button.setFixedSize(60, 20)
button.setText(text)
return button
class songpage(QGroupBox):
def __init__(self, texts):
super().__init__()
self.init(texts)
self.setCheckable(True)
self.setChecked(False)
def init(self, texts):
self.vbox = QVBoxLayout()
artist = textcell('Artist')
artist.editor.setText(texts[0])
album = textcell('Album')
album.editor.setText(texts[1])
title = textcell('Title')
title.editor.setText(texts[2])
self.vbox.addWidget(artist)
self.vbox.addWidget(album)
self.vbox.addWidget(title)
self.setLayout(self.vbox)
spacer = QSpacerItem(0, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
class Ui_MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(405, 720)
self.setWindowTitle('example')
frame = self.frameGeometry()
center = self.screen().availableGeometry().center()
frame.moveCenter(center)
self.move(frame.topLeft())
self.centralwidget = QWidget(self)
vbox = QVBoxLayout(self.centralwidget)
hbox = QHBoxLayout()
add = makebutton('Add')
delete = makebutton('Delete')
hbox.addWidget(add)
hbox.addSpacerItem(spacer)
hbox.addWidget(delete)
vbox.addLayout(hbox)
self.scrollArea = QScrollArea(self.centralwidget)
self.scrollArea.setWidgetResizable(True)
self.scrollAreaWidgetContents = QWidget()
self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.verticalLayout = QVBoxLayout(self.scrollAreaWidgetContents)
self.verticalLayout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.scrollArea.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
vbox.addWidget(self.scrollArea)
self.setCentralWidget(self.centralwidget)
add.clicked.connect(self.addObj)
delete.clicked.connect(self.deleteObj)
def addObj(self):
Obj = songpage(('AAA', 'BBB', 'CCC'))
self.verticalLayout.addWidget(Obj)
def deleteObj(self):
item = self.verticalLayout.itemAt(0)
widget = item.widget()
self.verticalLayout.removeItem(item)
self.verticalLayout.removeWidget(widget)
app = QApplication([])
window = Ui_MainWindow()
window.show()
app.exec()
The problem is very simple, if I click add button, the widget will be added and everything works fine, if I double click on a QTextEdit, its apply button will show and it will change from read only to editable.
After I click an apply button, the button will hide and the corresponding QTextEdit will be read only again.
I have finally managed to add a doubleClicked signal to QTextEdit.
And, the problem, is if I click delete button, instead of deleting the thing as expected, it crashes the whole application.
I am sorry the minimum reproducible example is so long, but I have only managed to reproduce the issue with all the code and I really don't know what went wrong.
So how to fix it?
Well, I have figured it out, it was caused by the spacer item that I add to every one of horizontal layouts where fixed-sized buttons are used.
All such layouts are using the same spacer item, exactly the same spacer item, not just identical.
From what I have observed, the spacer items are all references to the same object, they are all echos of the same object which lives at a fixed memory address.
I honestly don't understand how such thing works, that the same object can not only be added to multiple layouts and present in all the layouts concurrently, but also be added to the same layout multiple times, yet it always remains the original object, not duplicates of itself.
I thought when I added the same spacer item to multiple layouts, I didn't add the original spacer item, instead duplicates of the original item which are identical but are at different memory addresses, and clearly that isn't how Python works.
So when I remove a widget, everything inside it, everything inside its layout is deleted, and the spacer item is deleted.
Because all the spacer items in all the layouts are references to the original spacer item, when I delete one of the layouts, the original spacer item is deleted as well, and the spacer item is deleted from all other layouts, yet its shadows remain, and the item isn't properly removed from all the other layouts, the layouts contain references to an object that no longer exists, thus the application crashed.
By removing the definition of the spacer item and replacing adding the spacer item with .addStretch(), the bug is fixed.

How to save selected element from combobox as variable in PyQt5?

I'm interested in how to save a selected value from my combobox as variable, so when I press e.g. B then I want it to be saved as SelectedValueCBox = selected value, which would be B in this case.
Thank you for your help
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
class App(QMainWindow):
def __init__(self):
super().__init__()
self.title = "PyQt5 - StockWindow"
self.left = 0
self.top = 0
self.width = 200
self.height = 300
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.tab_widget = MyTabWidget(self)
self.setCentralWidget(self.tab_widget)
self.show()
class MyTabWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
#self.layout = QGridLayout(self)
self.tabs = QTabWidget()
self.tab1 = QWidget()
self.tabs.resize(300, 200)
self.tabs.addTab(self.tab1, "Stock-Picker")
self.tab1.layout = QGridLayout(self)
button = QToolButton()
self.tab1.layout.addWidget(button, 1,1,1,1)
d = {'AEX':['A','B','C'], 'ATX':['D','E','F'], 'BEL20':['G','H','I'], 'BIST100':['J','K','L']}
def callback_factory(k, v):
return lambda: button.setText('{0}_{1}'.format(k, v))
menu = QMenu()
self.tab1.layout.addWidget(menu, 1,1,1,1)
for k, vals in d.items():
sub_menu = menu.addMenu(k)
for v in vals:
action = sub_menu.addAction(str(v))
action.triggered.connect(callback_factory(k, v))
button.setMenu(menu)
self.tab1.setLayout(self.tab1.layout)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
Since you're already returning a lambda for the connection, the solution is to use a function instead.
class MyTabWidget(QWidget):
def __init__(self, parent):
# ...
def callback_factory(k, v):
def func():
self.selectedValueCBox = v
button.setText('{0}_{1}'.format(k, v))
return func
# ...
self.selectedValueCBox = None
Note that your code also has many issues.
First of all, you should not add the menu to the layout: not only it doesn't make any sense (the menu should pop up, while adding it to a layout makes it "embed" into the widget, and that's not good), but it also creates graphical issues especially because you added the menu to the same grid "slot" (1, 1, 1, 1) which is already occupied by the button.
Creating a layout with a widget as argument automatically sets the layout to that widget. While in your case it doesn't create a big issue (since you've already set the layout) you should not create self.tab1.layout with self. Also, since you've already set the QVBoxLayout (due to the parent argument), there's no need to call setLayout() again.
A widget container makes sense if you're actually going to add more than one widget. You're only adding a QTabWidget to its layout, so it's almost useless, and you should just subclass from QTabWidget instead.
Calling resize on a widget that is going to be added on a layout is useless, as the layout will take care of the resizing and the previous resize call will be completely ignored. resize() makes only sense for top level widgets (windows) or the rare case of widgets not managed by a layout.
self.layout() is an existing property of all QWidgets, you should not overwrite it. The same with self.width() and self.height() you used in the App class.
App should refer to an application class, but you're using it for a QMainWindow. They are radically different types of classes.
Finally, you have no combobox in your code. A combobox is widget that is completely different from a drop down menu like the one you're using. I suggest you to be more careful with the terminology in the future, otherwise your question would result very confusing, preventing people to actually being able to help you.

Force QTabBar tabs to stay as small as possible and ignore sizeHint

I'm trying to have a + button added to a QTabBar. There's a great solution from years ago, with a slight issue that it doesn't work with PySide2. The problem is caused by the tabs auto resizing to fill the sizeHint, which in this case isn't wanted as the extra space is needed. Is there a way I can disable this behaviour?
I've tried QTabBar.setExpanding(False), but according to this answer, the property is mostly ignored:
The bad news is that QTabWidget effectively ignores that property, because it always forces its tabs to be the minimum size (even if you set your own tab-bar).
The difference being in PySide2, it forces the tabs to be the preferred size, where I'd like the old behaviour of minimum size.
Edit: Example with minimal code. The sizeHint width stretches the tab across the full width, whereas in older Qt versions it doesn't do that. I can't really override tabSizeHint since I don't know what the original tab size should be.
import sys
from PySide2 import QtCore, QtWidgets
class TabBar(QtWidgets.QTabBar):
def sizeHint(self):
return QtCore.QSize(100000, super().sizeHint().height())
class Test(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
tabWidget = QtWidgets.QTabWidget()
tabWidget.setTabBar(TabBar())
layout.addWidget(tabWidget)
tabWidget.addTab(QtWidgets.QWidget(), 'this shouldnt be stretched')
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
I think there may be an easy solution to your problem (see below). Where the linked partial solution calculated absolute positioning for the '+' button, the real intent with Qt is always to let the layout engine do it's thing rather than trying to tell it specific sizes and positions. QTabWidget is basically a pre-built amalgamation of layouts and widgets, and sometimes you just have to skip the pre-built and build your own.
example of building a custom TabWidget with extra things across the TabBar:
import sys
from PySide2 import QtWidgets
from random import randint
class TabWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
#layout for entire widget
vbox = QtWidgets.QVBoxLayout(self)
#top bar:
hbox = QtWidgets.QHBoxLayout()
vbox.addLayout(hbox)
self.tab_bar = QtWidgets.QTabBar()
self.tab_bar.setMovable(True)
hbox.addWidget(self.tab_bar)
spacer = QtWidgets.QSpacerItem(0,0,QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
hbox.addSpacerItem(spacer)
add_tab = QtWidgets.QPushButton('+')
hbox.addWidget(add_tab)
#tab content area:
self.widget_stack = QtWidgets.QStackedLayout()
vbox.addLayout(self.widget_stack)
self.widgets = {}
#connect events
add_tab.clicked.connect(self.add_tab)
self.tab_bar.currentChanged.connect(self.currentChanged)
def add_tab(self):
tab_text = 'tab' + str(randint(0,100))
tab_index = self.tab_bar.addTab(tab_text)
widget = QtWidgets.QLabel(tab_text)
self.tab_bar.setTabData(tab_index, widget)
self.widget_stack.addWidget(widget)
self.tab_bar.setCurrentIndex(tab_index)
def currentChanged(self, i):
if i >= 0:
self.widget_stack.setCurrentWidget(self.tab_bar.tabData(i))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = TabWidget()
test.show()
sys.exit(app.exec_())
All that said, I think the pre-built QTabWidget.setCornerWidget may be exactly what you're looking for (set a QPushButton to the upper-right widget). The example I wrote should much easier to customize, but also much more effort to re-implement all the same functionality. You will have to re-implement some of the signal logic to create / delete / select / rearrange tabs on your own. I only demonstrated simple implementation, which probably isn't bulletproof to all situations.
Using the code from Aaron as a base to start on, I managed to implement all the functionality required to work with my existing script:
from PySide2 import QtCore, QtWidgets
class TabBar(QtWidgets.QTabBar):
def minimumSizeHint(self):
"""Allow the tab bar to shrink as much as needed."""
minimumSizeHint = super(TabBar, self).minimumSizeHint()
return QtCore.QSize(0, minimumSizeHint.height())
class TabWidgetPlus(QtWidgets.QWidget):
tabOpenRequested = QtCore.Signal()
tabCountChanged = QtCore.Signal(int)
def __init__(self, parent=None):
self._addingTab = False
super(TabWidgetPlus, self).__init__(parent=parent)
# Main layout
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
# Bar layout
self._tabBarLayout = QtWidgets.QHBoxLayout()
self._tabBarLayout.setContentsMargins(0, 0, 0, 0)
self._tabBarLayout.setSpacing(0)
layout.addLayout(self._tabBarLayout)
self._tabBar = TabBar()
self._tabBarLayout.addWidget(self._tabBar)
for method in (
'isMovable', 'setMovable',
'tabsClosable', 'setTabsClosable',
'tabIcon', 'setTabIcon',
'tabText', 'setTabText',
'currentIndex', 'setCurrentIndex',
'currentChanged', 'tabCloseRequested',
):
setattr(self, method, getattr(self._tabBar, method))
self._plusButton = QtWidgets.QPushButton('+')
self._tabBarLayout.addWidget(self._plusButton) # TODO: Find location to insert
self._plusButton.setFixedWidth(20)
self._tabBarLayout.addStretch()
# Content area
self._contentArea = QtWidgets.QStackedLayout()
layout.addLayout(self._contentArea)
# Signals
self.currentChanged.connect(self._currentChanged)
self._plusButton.clicked.connect(self.tabOpenRequested.emit)
# Final setup
self.installEventFilter(self)
#QtCore.Slot(int)
def _currentChanged(self, i):
"""Update the widget."""
if i >= 0 and not self._addingTab:
self._contentArea.setCurrentWidget(self.tabBar().tabData(i))
def eventFilter(self, obj, event):
"""Intercept events until the correct height is set."""
if event.type() == QtCore.QEvent.Show:
self.plusButton().setFixedHeight(self._tabBar.geometry().height())
self.removeEventFilter(self)
return False
def tabBarLayout(self):
return self._tabBarLayout
def tabBar(self):
return self._tabBar
def plusButton(self):
return self._plusButton
def tabAt(self, point):
"""Get the tab at a given point.
This takes any layout margins into account.
"""
offset = self.layout().contentsMargins().top() + self.tabBarLayout().contentsMargins().top()
return self.tabBar().tabAt(point - QtCore.QPoint(0, offset))
def addTab(self, widget, name=''):
"""Add a new tab.
Returns:
Tab index as an int.
"""
self._addingTab = True
tabBar = self.tabBar()
try:
index = tabBar.addTab(name)
tabBar.setTabData(index, widget)
self._contentArea.addWidget(widget)
finally:
self._addingTab = False
return index
def insertTab(self, index, widget, name=''):
"""Inserts a new tab.
If index is out of range, a new tab is appended.
Returns:
Tab index as an int.
"""
self._addingTab = True
tabBar = self.tabBar()
try:
index = tabBar.insertTab(index, name)
tabBar.setTabData(index, widget)
self._contentArea.insertWidget(index, widget)
finally:
self._addingTab = False
return index
def removeTab(self, index):
"""Remove a tab."""
tabBar = self.tabBar()
self._contentArea.removeWidget(tabBar.tabData(index))
tabBar.removeTab(index)
if __name__ == '__main__':
import sys
import random
app = QtWidgets.QApplication(sys.argv)
test = TabWidgetPlus()
test.addTab(QtWidgets.QPushButton(), 'yeah')
test.insertTab(0, QtWidgets.QCheckBox(), 'what')
test.insertTab(1, QtWidgets.QRadioButton(), 'no')
test.removeTab(1)
test.setMovable(True)
test.setTabsClosable(True)
def tabTest():
name = 'Tab ' + str(random.randint(0, 100))
index = test.addTab(QtWidgets.QLabel(name), name)
test.setCurrentIndex(index)
test.tabOpenRequested.connect(tabTest)
test.tabCloseRequested.connect(lambda index: test.removeTab(index))
test.show()
sys.exit(app.exec_())
The one difference is if you're using tabWidget.tabBar().tabAt(point), this is no longer guaranteed to be correct as it doesn't take any margins into account. I set the margins to 0 so this shouldn't be an issue, but I also included those corrections in TabWidgetPlus.tabAt.
I only copied a few methods from QTabBar to QTabWidget as some may need extra testing.

How to catch mouse over event of QTableWidget item in pyqt?

what I want to do is to change the color of a QTableWidget item, when I hover with the mouse over the item of my QTableWidget.
Firstly, the table widget needs to have mouse-tracking switched on to get the hover events.
Secondly, we need to find some signals that tell us when the mouse enters and leaves the table cells, so that the background colours can be changed at the right times.
The QTableWidget class has the cellEntered / itemEntered signals, but there is nothing for when the mouse leaves a cell. So, we will need to create some custom signals to do that.
The TableWidget class in the demo script below sets up the necessary cellExited / itemExited signals, and then shows how everything can be hooked up to change the item background when hovering with the mouse:
from PyQt4 import QtGui, QtCore
class TableWidget(QtGui.QTableWidget):
cellExited = QtCore.pyqtSignal(int, int)
itemExited = QtCore.pyqtSignal(QtGui.QTableWidgetItem)
def __init__(self, rows, columns, parent=None):
QtGui.QTableWidget.__init__(self, rows, columns, parent)
self._last_index = QtCore.QPersistentModelIndex()
self.viewport().installEventFilter(self)
def eventFilter(self, widget, event):
if widget is self.viewport():
index = self._last_index
if event.type() == QtCore.QEvent.MouseMove:
index = self.indexAt(event.pos())
elif event.type() == QtCore.QEvent.Leave:
index = QtCore.QModelIndex()
if index != self._last_index:
row = self._last_index.row()
column = self._last_index.column()
item = self.item(row, column)
if item is not None:
self.itemExited.emit(item)
self.cellExited.emit(row, column)
self._last_index = QtCore.QPersistentModelIndex(index)
return QtGui.QTableWidget.eventFilter(self, widget, event)
class Window(QtGui.QWidget):
def __init__(self, rows, columns):
QtGui.QWidget.__init__(self)
self.table = TableWidget(rows, columns, self)
for column in range(columns):
for row in range(rows):
item = QtGui.QTableWidgetItem('Text%d' % row)
self.table.setItem(row, column, item)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.table)
self.table.setMouseTracking(True)
self.table.itemEntered.connect(self.handleItemEntered)
self.table.itemExited.connect(self.handleItemExited)
def handleItemEntered(self, item):
item.setBackground(QtGui.QColor('moccasin'))
def handleItemExited(self, item):
item.setBackground(QtGui.QTableWidgetItem().background())
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window(6, 3)
window.setGeometry(500, 300, 350, 250)
window.show()
sys.exit(app.exec_())
You can achieve your goal pretty easily using the proper signals as proved by the following simple code:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class TableViewer(QMainWindow):
def __init__(self, parent=None):
super(TableViewer, self).__init__(parent)
self.table = QTableWidget(3, 3)
for row in range (0,3):
for column in range(0,3):
item = QTableWidgetItem("This is cell {} {}".format(row+1, column+1))
self.table.setItem(row, column, item)
self.setCentralWidget(self.table)
self.table.setMouseTracking(True)
self.current_hover = [0, 0]
self.table.cellEntered.connect(self.cellHover)
def cellHover(self, row, column):
item = self.table.item(row, column)
old_item = self.table.item(self.current_hover[0], self.current_hover[1])
if self.current_hover != [row,column]:
old_item.setBackground(QBrush(QColor('white')))
item.setBackground(QBrush(QColor('yellow')))
self.current_hover = [row, column]
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
tv = TableViewer()
tv.show()
sys.exit(app.exec_())
You may be interested in other signals too, especially itemEntered. However, if you want total control over the editing and display of items then using delegates (via the QTableWidget.setItemDelegate method) is strongly recommended.
UPDATE:
sorry, I had forgotten the second part of the problem i.e. what happens when the mouse exits a cell. Even then the problem can be solved easily without using events. See the updated code, please.
There are no events based on QTableWidgetItem, but you can do this:
reimplement the mouseMoveEvent() of QTableWidget, you can get the mouse position;
use itemAt() method to get the item under your mouse cursor;
customize your item;
This may simalute what you want.
I know this is old but wanted to update a couple of parts to it as I came across this page looking for a similar solution. This has a couple of parts to it, one is similar to the above but avoids the NoneType error if the cell is empty. Additionally, it will change the color of the highlighted cell, but also update a tooltip for the cell to display the contents of the cell in a tooltip. Nice if you have cells with runoffs 123...
Sure it could be cleaned up a bit, but works for PyQt5. Cheers!
def cellHover(self, row, column):
item = self.My_Table1.item(row, column)
old_item = self.My_Table1.item(self.current_hover[0], self.current_hover[1])
if item is not None:
if self.current_hover != [row,column]:
text = item.text()
if text is not None:
self.My_Table1.setToolTip(text)
item.setBackground(QBrush(QColor('#bbd9f7')))
old_item.setBackground(QBrush(QColor('white')))
self.current_hover = [row, column]

Categories

Resources