I am trying to write a PyQt5 widget which will be a basic preset manager inside an application. I am trying to use the QTreeWidget to display the structure of each type. I have this weird issue, where I see an extra QTreeWidget with a column named "1" outside my QTreeWidget. I have attached a picture (sorry, I could not take a screenshots) and the code. I have also noticed that this does not happen if I don't use classes.
Here's my test code showing the problem:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class MyTree(QtWidgets.QTreeWidget):
def __init__(self, parent = None):
super(self.__class__, self).__init__(parent)
boxLayout = QtWidgets.QVBoxLayout()
treeWidget = QtWidgets.QTreeWidget()
treeWidget.setHeaderLabels(['Preset Name'])
treeWidget.setColumnCount(1)
items = []
for i in range(10):
items.append(QtWidgets.QTreeWidgetItem(["item {0}".format(i)]))
treeWidget.insertTopLevelItems(0, items)
boxLayout.addWidget(treeWidget)
self.setLayout(boxLayout)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MyTree()
w.show()
sys.exit(app.exec_())
That's what I see:
My best guess that it's the layout that's causing it, because if I just create the QTreeWidget like so:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
app = QtWidgets.QApplication(sys.argv)
treeWidget = QtWidgets.QTreeWidget()
treeWidget.setHeaderLabels(['Preset Name'])
treeWidget.setColumnCount(1)
items = []
for i in range(10):
items.append(QtWidgets.QTreeWidgetItem(["item {0}".format(i)]))
treeWidget.insertTopLevelItems(0, items)
treeWidget.show()
sys.exit(app.exec_())
That's what I see:
Any ideas how to get it showing like the second image, from a class?
Any help is greatly appreciated
The error is simple: Your window (MyTree) is an empty QTreeWidget that has another QTreeWidget inside, so "1" belongs to the empty QTreeWidget.
There are 2 possible solutions:
No user as base class to QTreeWidget
class MyTree(QtWidgets.QWidget):
def __init__(self, parent=None):
super(self.__class__, self).__init__(parent)
treeWidget = QtWidgets.QTreeWidget()
treeWidget.setHeaderLabels(["Preset Name"])
treeWidget.setColumnCount(1)
items = []
for i in range(10):
items.append(QtWidgets.QTreeWidgetItem(["item {0}".format(i)]))
treeWidget.insertTopLevelItems(0, items)
boxLayout = QtWidgets.QVBoxLayout(self)
boxLayout.addWidget(treeWidget)
Do not nest a new QTreeWidget
class MyTree(QtWidgets.QTreeWidget):
def __init__(self, parent=None):
super(self.__class__, self).__init__(parent)
self.setHeaderLabels(["Preset Name"])
self.setColumnCount(1)
items = []
for i in range(10):
items.append(QtWidgets.QTreeWidgetItem(["item {0}".format(i)]))
self.insertTopLevelItems(0, items)
Related
I have a simple app with two QListWidgets
I want to:
drag and drop between them
rearrange the order within them.
The problem: When I attempt to rearrange the order within one of the QListWidgets, the QListItem disappears.
Here is a small example (I'm using python3)
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QApplication, QWidget, QLabel
from PyQt5.QtCore import Qt
class DragWidget(QtWidgets.QListWidget) :
def __init__(self,parent,total=None) :
super(DragWidget,self).__init__(parent)
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Minimum)
#Want horizontal listwidgets.
self.setFlow(QtWidgets.QListView.Flow.LeftToRight)
#Here's the attempt to configure dragging.
self.setDragEnabled(True)
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
self.setDropIndicatorShown(True)
self.setDefaultDropAction(Qt.MoveAction)
self.viewport().setAcceptDrops(True)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.setResizeMode(QtWidgets.QListView.ResizeMode.Adjust)
self.setSpacing(2)
self.setFixedHeight(50)
#An attempt to overload the dragEnterEvent
def dragEnterEvent(self,event) :
#Use the InternalMove if the source = the drop site
if (event.source() is self):
self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
else :
#And regular ol' DragDrop if not.
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
super().dragEnterEvent(event)
class DragDemo(QtWidgets.QDialog) :
def __init__(self,parent=None,*args,**kwargs) :
super().__init__()
layout = QtWidgets.QGridLayout()
groupbox = QtWidgets.QGroupBox("Display Columns")
groupbox.setLayout(layout)
showlist = DragWidget(groupbox)
options = ['type','name','timestamp']
itemlist = []
for option in options :
item = QtWidgets.QListWidgetItem(option,showlist)
itemlist.append(item)
layout.addWidget(showlist,0,0)
hidelist = DragWidget(groupbox)
layout.addWidget(hidelist,1,0)
vlayout = QtWidgets.QVBoxLayout()
vlayout.addWidget(groupbox)
self.setLayout(vlayout)
self.show()
app = QApplication(sys.argv)
demo = DragDemo()
demo.show()
sys.exit(app.exec_())
Who out there can tell me what I'm doing wrong?
Any, and all help is appreciated.
I've got a QTreeView populated by a QStandardItemModel with the TreeView set up so it only allows internal Drag and Drop (InternalMove).
I am trying to detect whenever the user makes such an internal move and would like to extract the item being dragged as well as the start and end location.
QStandardItemModel provides the "rowsMoved" signal which is supposed to emit precisely what I am looking for: parent, start, end, destination, row.
The problem: This signal never gets called when moving around items. Why is this?
Other signals like rowsInserted() or rowsRemoved() work just fine, but rowsMoved() does not.
In the following minimal example, the print() in onMove() should be called when the user moves around items, but it doesn't.
import random
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
model = QtGui.QStandardItemModel(self)
view = QtWidgets.QTreeView()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(view)
view.setModel(model)
root_item = QtGui.QStandardItem("Root")
model.appendRow(root_item)
self.populate(root_item, 3)
view.expandAll()
view.setDragEnabled(True)
view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
view.setDefaultDropAction(QtCore.Qt.MoveAction)
view.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
model.rowsMoved.connect(self.onMove)
def onMove(self, parent, start, end, destination, row):
print("parent",parent,"start",start,"end",end,"destination",destination,"row",row)
def populate(self, root_item, level):
for i in range(random.randint(2, 4)):
it = QtGui.QStandardItem("item {}".format(i))
it.setCheckable(False)
root_item.appendRow(it)
next_level = level - 1
if next_level > 0:
self.populate(it, next_level)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
The documentation states:
Components connected to this signal use it to adapt to changes
in the model's dimensions. It can only be emitted by the QAbstractItemModel
implementation, and cannot be explicitly emitted in subclass code.
Considering that this signal is not defined by the QStandardItemModel class, any instance of this class won't emit such signal.
The only signals you can connect to, as you mentioned, are rowsInserted() are rowsRemoved() because they are redefined in this class. Note that they are marked as internal in the source file.
If you need information about the item being dragged, I suggest that you create a custom class which inherit from QtWidgets.QTreeView and override the dragEnterEvent and dropEvent methods.
Here is an example on how to retrieve the item with its whole hierarchy being dragged and dropped:
import random
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
def getHierarchy(index, hierarchy=None):
hierarchy = hierarchy or []
if not index.isValid():
return hierarchy
hierarchy.insert(0, (index.row(), index.column()))
return getHierarchy(index.parent(), hierarchy)
class MyTreeView(QtWidgets.QTreeView):
def dropEvent(self, e):
super().dropEvent(e)
currentIndex = e.source().currentIndex()
print('dropEvent.source current index:', getHierarchy(currentIndex))
def dragEnterEvent(self, e):
super().dragEnterEvent(e)
currentIndex = e.source().currentIndex()
print('dragEnterEvent.source current index:', getHierarchy(currentIndex))
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
model = QtGui.QStandardItemModel(self)
view = MyTreeView()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(view)
view.setModel(model)
root_item = QtGui.QStandardItem("Root")
model.appendRow(root_item)
self.populate(root_item, 3)
view.expandAll()
view.setDragEnabled(True)
view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
view.setDefaultDropAction(QtCore.Qt.MoveAction)
view.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
view.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
def populate(self, root_item, level):
for i in range(random.randint(2, 4)):
it = QtGui.QStandardItem("item {}".format(i))
it.setCheckable(False)
root_item.appendRow(it)
next_level = level - 1
if next_level > 0:
self.populate(it, next_level)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
I suggest to look at this repository which deals with a similar problem as yours: https://github.com/d1vanov/PyQt5-reorderable-list-model
I'm putting together a UI - users provide some information, and code executes. I've got some checkboxes. I want to enable/disable some of the checkboxes based on the status of other checkboxes. As an example, my UI has a checkbox which lets a user specify if they wanted a file to print, and a checkbox that let a user specify if they want it to print in colour. If the 'print' checkbox isn't ticked, I want to clear and disable the 'colour' checkbox: it doesn't make any sense to let someone say they want to print in colour if they aren't printing.
I can see how to do this with signals/slots, but I'm pretty new to Qt, so I'm wondering if there's a cleaner way to do this. Looking at ButtonGroups was my first port of call, but I couldn't see any way to make it work.
What I have looks something like this. I want to emphasize - this does exactly what I want it to do - I'm just not sure that it's the best way to do it, and I'd like not to hate myself if I come back to the code in a few months with more knowledge. I'd be entirely unsurprised if there were built-in functionality that accomplished my goals.
self.first_checkbox = QtWidgets.QCheckBox()
self.second_checkbox = QtWidgets.QCheckBox()
self.first_checkbox.stateChanged.connect(self._handleCheckboxStateChanged)
#QtCore.Slot()
def _handleCheckboxStateChange(self):
if self.first_checkbox.isChecked():
self.second_checkbox.setEnabled(True)
else:
self.second_checkbox.setEnabled(False)
self.second_checkbox.setChecked(False)
Your method is correct, my answer just tries to show other equivalent methods:
1.
from PySide2 import QtCore, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.first_checkbox = QtWidgets.QCheckBox("Print")
self.second_checkbox = QtWidgets.QCheckBox("color")
self.first_checkbox.stateChanged.connect(
lambda state: self.second_checkbox.setDisabled(
state != QtCore.Qt.Checked
)
)
self.first_checkbox.stateChanged.connect(
lambda state: self.second_checkbox.setCheckState(
QtCore.Qt.Unchecked
)
if not state
else None
)
self.second_checkbox.setDisabled(True)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.first_checkbox)
lay.addWidget(self.second_checkbox)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
2.
from PySide2 import QtCore, QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.first_checkbox = QtWidgets.QCheckBox("Print")
self.second_checkbox = QtWidgets.QCheckBox("color")
self.first_checkbox.stateChanged.connect(
self._handleCheckboxStateChange
)
self.second_checkbox.setDisabled(True)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.first_checkbox)
lay.addWidget(self.second_checkbox)
#QtCore.Slot(QtCore.Qt.CheckState)
def _handleCheckboxStateChange(self, state):
self.second_checkbox.setEnabled(state == QtCore.Qt.Checked)
if state != QtCore.Qt.Checked:
self.second_checkbox.setCheckState(QtCore.Qt.Unchecked)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
I would like to dynamically create then manipulate lots of widgets. My idea is to store widgets in a dict (mywidgets) and to trigger them with signals stored in another dict (mysignals). Both dict shared same keys defined in a list (names), dicts are initialized with for loops.
When I connect signals to slots, I'm currently facing an AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect'.
I have tried to disable signal/slot connections: the GUI looks good, QLineEdit are well stored in mywidgets. Types of mysignals items are correct: class 'PyQt5.QtCore.pyqtSignal'.
Can you, please, explain me where the issue come from ?
Thanks.
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QPushButton, QVBoxLayout
from PyQt5.QtCore import pyqtSlot, pyqtSignal
class App(QWidget):
names = ["foo","bar"]
mysignals = {} # Store several signals in a dict
for name in names:
mysignals[name] = pyqtSignal(str)
def __init__(self):
super().__init__()
# Create Widgets
self.btn_go = QPushButton("Go") #Simple push button
self.mywidgets = {} #Store several QLineEdit in a dict
for name in self.names:
self.mywidgets[name] = QLineEdit()
# Connect signals
self.btn_go.clicked.connect(self.on_click) #Connect push button
for name in self.names:
print(type(self.mysignals[name]))
self.mysignals[name].connect(self.mywidgets[name].setText) #Connect several signals
# Configure layout
layout = QVBoxLayout()
layout.addWidget(self.btn_go)
for name in self.names:
layout.addWidget(self.mywidgets[name])
self.setLayout(layout)
# Show widget
self.show()
#pyqtSlot()
def on_click(self):
data = {"foo":"Python3","bar":"PyQt5"}
for key,value in data.items():
self.mysignals[key].emit(value)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
The expected result is to display respectivelly Python3 and PyQt5 in mywidgets["foo"] and mywidgets["bar"] QLineEdit widgets when push button is clicked.
As the docs point out:
A signal (specifically an unbound signal) is a class attribute. When a
signal is referenced as an attribute of an instance of the class then
PyQt5 automatically binds the instance to the signal in order to
create a bound signal. This is the same mechanism that Python itself
uses to create bound methods from class functions.
A signal is declared as an attribute of the class but when referenced through self a bind is made with the object, that is, the declared signal is different from the instantiated signal:
from PyQt5 import QtCore
class Foo(QtCore.QObject):
fooSignal = QtCore.pyqtSignal()
print("declared:", fooSignal)
def __init__(self, parent=None):
super(Foo, self).__init__(parent)
print("instantiated:", self.fooSignal)
if __name__ == '__main__':
import sys
app = QtCore.QCoreApplication(sys.argv)
obj = Foo()
Output:
declared: <unbound PYQT_SIGNAL )>
instantiated: <bound PYQT_SIGNAL fooSignal of Foo object at 0x7f4beb998288>
That is the reason for the error you get, so if you want to use the signals you must obtain it using the object so we can inspect the attributes and get the signals:
from PyQt5 import QtCore, QtGui, QtWidgets
class Widget(QtWidgets.QWidget):
foo = QtCore.pyqtSignal(str)
bar = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self.fill_signals()
self.names = ["foo", "bar"]
self.btn_go = QtWidgets.QPushButton("Go")
self.mywidgets = {}
for name in self.names:
self.mywidgets[name] = QtWidgets.QLineEdit()
signal = self.mysignals.get(name)
if signal is not None:
signal.connect(self.mywidgets[name].setText)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.btn_go)
for name in self.names:
layout.addWidget(self.mywidgets[name])
self.btn_go.clicked.connect(self.testing)
def fill_signals(self):
self.mysignals = dict()
for p in dir(self):
attr = getattr(self, p)
if isinstance(attr, QtCore.pyqtBoundSignal):
self.mysignals[p] = attr
def testing(self):
self.foo.emit("foo")
self.bar.emit("bar")
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Sorry, I think you have complicated the algorithm to obtain the expected result. Try it:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLineEdit, QPushButton, QVBoxLayout
from PyQt5.QtCore import pyqtSlot, pyqtSignal
class App(QWidget):
def __init__(self, names):
super().__init__()
self.names = names
layout = QVBoxLayout()
# Create Widgets
self.btn_go = QPushButton("Go") # Simple push button
self.btn_go.clicked.connect(self.on_click) # Connect push button
layout.addWidget(self.btn_go)
self.mywidgets = {} # Store several QLineEdit in a dict
for name in self.names:
self.mywidgets[name] = QLineEdit()
layout.addWidget(self.mywidgets[name])
self.setLayout(layout)
#pyqtSlot()
def on_click(self):
data = {"foo":"Python3", "bar":"PyQt5"}
for key, value in data.items():
self.mywidgets[key].setText(value)
if __name__ == '__main__':
app = QApplication(sys.argv)
names = ["foo", "bar"]
ex = App(names)
ex.show()
sys.exit(app.exec_())
I'm trying to set up unit tests for my PyQt app, and I ran into an unexpected situation.
Basically, the type of item returned from layout.itemAt() is different from the item that was added to the layout, and I'm curious why.
Here's my example code:
from PyQt5 import QtWidgets
class MainWindow(QtWidgets.QDialog):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.layout = QtWidgets.QVBoxLayout()
self.table = QtWidgets.QTableWidget()
self.layout.addWidget(self.table)
self.setLayout(self.layout)
print(type(self.table))
print(type(self.layout.itemAt(0)))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
form = MainWindow()
form.show()
app.exec_()
When this is run, type(self.table) returns <class 'PyQt5.QtWidgets.QTableWidget'> as expected, but type(self.layout.itemAt(0)) -- which is still the table item -- returns <class 'PyQt5.QtWidgets.QWidgetItem'>, and I can't figue out why they would be different.
The classes that inherit QLayout as QBoxLayout, QGridLayout, QFormLayout and QStackedLayout have as their main element the QLayoutItem(in your case QWidgetItem) which is a class that can handle the geometry of other QLayouts and QWidgets, and that is the object that is being returned with the method itemAt().
If you want to get the widget you must use the widget() method of the QLayoutItem:
from PyQt5 import QtWidgets
class MainWindow(QtWidgets.QDialog):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.layout = QtWidgets.QVBoxLayout()
self.table = QtWidgets.QTableWidget()
self.layout.addWidget(self.table)
self.setLayout(self.layout)
assert(self.table == self.layout.itemAt(0).widget())
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
form = MainWindow()
form.show()
sys.exit(app.exec_())
That object type is an intermediate between the widget and the layout that is created by the layout when you add an item (see here). Use self.layout.itemAt(0).widget() to get the original widget!