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);
I have a situation where i want to add 3 buttons in a QTableWidget.
I could able to add a single button using below code.
self.tableWidget = QtGui.QTableWidget()
saveButtonItem = QtGui.QPushButton('Save')
self.tableWidget.setCellWidget(0,4,saveButtonItem)
But i want to know how to add multiple (lets say 3) buttons. I Mean Along with Save Button i want to add other 2 buttons like Edit, Delete in the same column (Actions)
You can simply create your own widget, containing the three buttons, e.g. via subclassing QWidget:
class EditButtonsWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(EditButtonsWidget,self).__init__(parent)
# add your buttons
layout = QtGui.QHBoxLayout()
# adjust spacings to your needs
layout.setContentsMargins(0,0,0,0)
layout.setSpacing(0)
# add your buttons
layout.addWidget(QtGui.QPushButton('Save'))
layout.addWidget(QtGui.QPushButton('Edit'))
layout.addWidget(QtGui.QPushButton('Delete'))
self.setLayout(layout)
And then, set this widget as the cellwidget:
self.tableWidget.setCellWidget(0,4, EditButtonsWidget())
You use a layout widget to add your widgets to, then add the layout widget to the cell.
There are a couple of different ones you can use.
http://doc.qt.io/qt-4.8/layout.html
self.tableWidget = QtGui.QTableWidget()
layout = QtGui.QHBoxLayout()
saveButtonItem = QtGui.QPushButton('Save')
editButtonItem = QtGui.QPushButton('Edit')
layout.addWidget(saveButtonItem)
layout.addWidget(editButtonItem)
cellWidget = QtGui.QWidget()
cellWidget.setLayout(layout)
self.tableWidget.setCellWidget(0, 4, cellWidget)
How to make a list of line editors without many variables? (smth like self.line_1 = QLineEdit(self), self.line_2 = QLineEdit(self), ... , self.line_9000 = QLineEdit(self))
For example, I want to create this
window with ability to get access to each element.
A simple cycle does not provide access to each element, only last. How I can do this?
One way is to make widgets as you said - cycle,
and you can access to the widget with using layout.itemAtPosition
it would go like this :
layout = QVBoxLayout()
for i in range(list_length):
line_edit = QLineEdit(self)
layout.addWidget(line_edit)
to access the widget :
def access_widget(int):
item = layout.itemAtPosition(int)
line_edit = item.widget()
return line_edit
now you can access to the designated QLineEdit.
layout = QFormLayout()
self.alphabet_line_edits = dict.fromkeys(['а', 'б', 'в', 'г'])
for letter in self.alphabet_line_edits:
line_edit = QLineEdit()
layout.addRow(letter, line_edit)
self.alphabet_line_edits[letter] = line_edit
def button_clicked(self):
print(self.alphabet_line_edit['б'].text())
In my PyQT window, I have a table containing QComboBox in one column. How can the QComboBox later be changed to the regular QTableWidgetItem to display some text?
I tried the following but the QComboBox was not replaced by text from QTableWidgetItem.
myTable= QTableWidget()
myTable.setRowCount(6)
myTable.setColumnCount(2)
myTable.setHorizontalHeaderLabels(QString("Name;Age;").split(";"))
myTable.horizontalHeader().setResizeMode(QHeaderView.Stretch)
# Populate with QComboBox in column 1
for i, name in enumerate(nameList):
myTable.setItem(i, 0, QTableWidgetItem(name ))
ageCombo = QComboBox()
for option in ageComboOptions:
ageCombo.addItem(option)
myTable.setCellWidget(i, 1, ageCombo)
# Change column 1 to QTableWidgetItem
for i, name in enumerate(nameList):
myTable.setItem(i, 1, QTableWidgetItem(name))
The short answer is that if you just removeCellWidget you'll get what you want. Example code below.
But in more detail:
The "Item" as set by setItem and the "Widget" as set by setCellWidget are different - they play different roles. The item carries the data for the cell: in the model view architecture it's in the model. The widget is doing the display: it's in the view. So, when you set the cell widget you might still expect it to use an item in the model behind it. However, the QTableWidget provides a simplified API to the full model view architecture as used in QT (e.g. see the QTableView and QAbstractitemModel). It provides its own default model which you access via an item for each cell. Then, when you replace the widget on a cell it dispenses with any item at all and just allows you to control the widget directly. Remove the widget and it goes back to using the item.
Here's a working example:
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
self.myTable= QtGui.QTableWidget()
self.myTable.setRowCount(1)
self.myTable.setColumnCount(2)
item1 = QtGui.QTableWidgetItem("a")
self.myTable.setItem(0, 0, item1)
item2 = QtGui.QTableWidgetItem("b")
self.myTable.setItem(0, 1, item2)
self.setCentralWidget(self.myTable)
menubar = QtGui.QMenuBar(self)
self.setMenuBar(menubar)
menu = QtGui.QMenu(menubar)
menu.setTitle("Test")
action = QtGui.QAction(self)
action.setText("Test 1")
action.triggered.connect(self.test1)
menu.addAction(action)
action = QtGui.QAction(self)
action.setText("Test 2")
action.triggered.connect(self.test2)
menu.addAction(action)
menubar.addAction(menu.menuAction())
self.show()
def test1(self):
self.myTable.removeCellWidget(0, 1)
def test2(self):
combo = QtGui.QComboBox()
combo.addItem("c")
combo.addItem("d")
self.myTable.setCellWidget(0, 1, combo)
def main():
app = QtGui.QApplication(sys.argv)
mw = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
My objective is to give the user QComboBoxes depending on the number he/she selected on the QSpinbox.
So, it will be something like this:
User will select a number on QSpinbox,after that, by using the signal valueChanged(int) i want to invoke another function that will create QComboBoxes for user to enter the data.
As soon as user change to another value. It will automatically increase / decrease the QComboBox numbers depending on the QSpinbox value.
So i came up with something like this:
def myfunction(x):
labellist = []
combolist = []
layoutlist = []
layout = QtGui.QVBoxLayout()
for i in range(x):
labellist.append(QtGui.QLabel('this is label'+str(i))
combolist.append(QtGui.QComboBox())
layoutlist.append(QtGui.QHBoxLayout())
layoutlist[i].addWidget(labellist[i])
layoutlist[i].addWidget(combolist[i])
layout.addLayout(layoutlist[i])
self.connect(number,QtCore.SIGNAL("valueChanged(int)"),myfunction)
Even though it create ComboBoxes depending on the number user selected on spin box, when user increase the number (eg. 3 to 4), it doesn't remove the old 3comboBoxes, instead it become 7 comboboxes all together.
How do i fix this? Or Is there a better way to achieve the similar result?
You could delete and recreate all the comboboxes everytime your spinbox value changes. It may not be the most efficient, but it's quite simple.
Just have a list with references to the labels/comboxes as an attribute. In your slot, call deleteLater() on each item, then delete the reference by setting your list to []. Finally, recreate the items, add them to your layout and repopulate your list.
Also, you should have a look at New Style Signals and Slots. They are nicer than the ugly C++ style connect.
class DynamicComboBoxes(QtGui.QWidget):
def __init__(self, parent=None):
super(DynamicComboBoxes, self).__init__(parent)
vbox = QtGui.QVBoxLayout(self)
spinbox = QtGui.QSpinBox(self)
spinbox.setRange(0,10)
spinbox.valueChanged.connect(self.onChangeValue)
vbox.addWidget(spinbox)
self.grid = QtGui.QGridLayout()
self.itemlist = []
vbox.addLayout(self.grid)
vbox.addStretch(1)
def onChangeValue(self, val):
for label, combobox in self.itemlist:
label.deleteLater()
combobox.deleteLater()
self.itemlist = []
for i in range(val):
label = QtGui.QLabel('This is Label {}'.format(i))
combobox = QtGui.QComboBox()
self.grid.addWidget(label, i, 0)
self.grid.addWidget(combobox, i, 1)
self.itemlist.append([label, combobox])