PyQt4: How to reorder child widgets? - python

I want to implement a GUI program like the blueprint editor in the Unreal game engine with PyQt4. Here is an example of the blueprint editor:
First I create a simple container widget to place all the components(The rectangles). Then I use the QPainterPath widget to draw lines to connect the components. Since the users can place the components wherever they want by drag and drop, I choose absolute positioning in my container widget. Here is the sample code:
class Component(QtGui.QWidget):
"""A simple rectangle."""
def __init__(self, type_, parent=None):
super(Component, self).__init__(parent)
...
class BezierPath(QtGui.QWidget):
def __init__(self, start, end, parent=None):
super(BezierPath, self).__init__(parent)
self.setMinimumSize(300, 500)
self._start = start
self._end = end
self._path = self._generate_path(self._start, self._end)
self.setMinimumSize(
abs(start.x()-end.x()), abs(start.y()-end.y()))
def _generate_path(self, start, end):
path = QtGui.QPainterPath()
path.moveTo(start)
central_x = (start.x()+end.x())*0.5
path.cubicTo(
QtCore.QPointF(central_x, start.y()),
QtCore.QPointF(central_x, end.y()),
end)
return path
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
pen = QtGui.QPen()
pen.setWidth(3)
painter.setPen(pen)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.drawPath(self._path)
painter.end()
class Container(QtGui.QWidget):
def add_component(self, component_type, position):
component = Component(component_type, self)
component.move(position)
def add_connection(self, component_a, component_b):
bezier_path = BezierPath(component_a.pos(), component_b.pose(), self)
The problem is I want to lines show underneath the components, but the components are created first. Is there a way I can reorder the child widgets of the Container of should I use a better way to organize the components?

I found a solution for reordering the child widgets myself. I'll mark this answer as accepted for now. It's not a perfect solution so if there is a better answer I will accept it then. Anyway here is the solution:
In qt4 the widget has a method raise_(), here I quote:
to raises this widget to the top of the parent widget's stack. After
this call the widget will be visually in front of any overlapping
sibling widgets.
So if you want to reorder all your widgets, first you keep the references of all the child widgets in you own list or what container you choose. Reorder all the widgets in you own container, then call raise_() for each widget in reverse order.

Related

Create new components on an inherited window in pyqt5

I'm working on a small GUI made in PYQT5. I have a main window with a couple of buttons which open new windows. One of these windows has an embedded matplotlib plot and 2 buttons.
So, from this existing window called "PlotWindow" I want to create a new window called "DynamicPlotWindow" but add more elements (Comboboxes, buttons, methods, etc.). In other words, I want to reuse existing windows and put more components on them. I´m able to create new DynamicPlotWindow windows, but the new components added to it aren´t visible.
Based on this question: PyQt5 Making a subclass widgets the definition of both clases is as follows:
class PlotWindow(QMainWindow): #Matplotlib embeded + 2 buttons
def __init__(self, parent):
super(QMainWindow, self).__init__(parent)
self.width = 1000
self.height = 540
self.setGeometry(10, 10, self.width, self.height)
...
self.show()
...
class DynamicPlotWindow(PlotWindow):
def __init__(self, parent):
super(PlotWindow, self).__init__(parent)
self.btn = QPushButton("Test") # -> Not visible
self.btn.resize(120,30)
self.btn.move(600,800)
...
self.show()
My question is what am I doing wrong here? Is it possible to do it?
Best,
Your code has the following errors:
The botton is not a child of the window so it will not be shown, the solution is to pass it to self as parent
The window has a size of 1000x540 but you want to place the button in the position (600,800) that is clearly outside the height: 800> 540.
The solution is:
self.btn = QPushButton("Test", self)
self.btn.resize(120,30)
self.btn.move(600, 200) # change y coordinate

How to show Qt.Tool window with minimize/maximize windows controls?

I have...
class ToolWindow(QtWidgets.QMainWindow):
"""Generic window to be used as non-modal tool
Usage:
tool_win = ToolWindow()
layout = QtWidgets.QHBoxLayout()
button = QtWidgets.QPushButton('hello')
layout.addWidget(button)
tool_win.setup(layout)
button.released.connect(lambda: print('hello'))
tool_win.show()
"""
def __init__(self):
super(ToolWindow, self).__init__()
def setup(self, layout,
window_title='Untitled', object_name=None, tool=True):
"""Setup tool window"""
if tool:
self.setWindowFlags(QtCore.Qt.Tool)
self.widget = QtWidgets.QWidget()
self.widget.setLayout(layout)
self.setCentralWidget(self.widget)
self.setWindowTitle(window_title)
def closeEvent(self, event):
"""Delete object when closed"""
self.deleteLater()
However, I wish to add the typical maximize and minimize window controls to the window. I've attempted to add the following to the ToolWindow class without success (the tool window still doesn't show the maximize/minimize window controls):
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMinMaxButtonsHint)
Is it possible to add these controls to a tool window?
Alternatively, can I create a non-modal window but which always sits atop my parent application and which shows the maximize/minimize window controls?
Please note, I don't want this tool window staying on top of ALL windows on my system. I only want it to always stay on top of my application.
You should be able to just use the QMainWindow class without any flags. As long as the tool window is a child of the primary application window, it will stay on top of it (but not windows from other applications, like it would if you set the "Window Stays On Top" flag).
You'll need to change your __init__ to accept parent arguments
def __init__(self, parent):
super(ToolWindow, self).__init__(parent)
If you have multiple Tool Windows and you want them to stay on top in a specific order, you can call my_tool_window.raise_() to bring it to the top of the z-order.
Qt ships with a window flags example. You may want to check that out to see how the different flags affect the window display and behavior.

PyQt: give parent when creating a widget?

Let's assume I want to create a dialog box, a child of my main program:
from PyQt4 import QtGui, QtCore
class WizardJournal(QtGui.QDialog):
def __init__(self, parent):
super(WizardJournal, self).__init__(parent)
self.parent = parent
self.initUI()
def initUI(self):
self.parent.wizard = QtGui.QWidget()
self.ok_button = QtGui.QPushButton("OK", self)
self.vbox_global = QtGui.QVBoxLayout(self)
self.vbox_global.addWidget(self.ok_button)
self.paret.wizard.setLayout(self.vbox_global)
self.parent.wizard.show()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
parent = QtGui.QWidget()
obj = WizardJournal(parent)
sys.exit(app.exec_())
This dialog box will be opened and closed by my main program. What is better regarding memory consumption:
self.ok_button = QtGui.QPushButton("OK", self)
self.ok_button = QtGui.QPushButton("OK")
Basically, I would like to know if I should mention the parent widget when I create a widget. When I will close this dialog box, will the OK button be released from memory if I didn't mention the parent widget when I created it ?
Given the way your example is currently structured, neither the dialog nor any of its child widgets will be deleted when it is closed.
You can see this by changing the end of the example to look like this:
app.exec_()
print('\n'.join(repr(w) for w in app.allWidgets()))
which will give output like this (once the dialog is closed):
<__main__.WizardJournal object at 0x7fcd850f65e8>
<PyQt4.QtGui.QPushButton object at 0x7fcd850f6708>
<PyQt4.QtGui.QWidget object at 0x7fcd850f6558>
<PyQt4.QtGui.QDesktopWidget object at 0x7fcd850f6828>
<PyQt4.QtGui.QWidget object at 0x7fcd850f6678>
In PyQt, you have to be aware that there may be two kinds of reference held for a object: one on the Python side (the PyQt wrapper object) and one on the C++ side (the underlying Qt object). So to fully delete an object, you need to remove all of these references.
In general, Qt does not delete objects unless you explictly tell it to do so. This is something you need to be aware of when creating dialogs with a parent, because it is very easy to produce a memory leak otherwise. It is common to see code written like this:
def openDialog(self):
dialog = MyDialog(self)
dialog.show()
Which looks harmless at first glance - but the method will create a new dialog every time it is called, and Qt will end up holding on to every single one of them (because of the parent reference on the C++ side). One way to avoid this is to re-write the method so that it only keeps a reference on the Python side:
def openDialog(self):
self.dialog = MyDialog()
self.dialog.show()
But what to do about a modal dialog, which must have a parent? In that case, you could initialise the dialog class like this:
class MyDialog(QtGui.QDialog):
def __init__(self, parent):
super(MyDialog, self).__init__(parent)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
And now Qt will automatically delete the dialog when it is closed, and also recursively delete all of its child objects as well. This will just leave behind an empty PyQt wrapper object, which will (eventually) be removed by the Python garbage-collector.
So for your particular example, I think I would re-write it to look something like this:
import sys
from PyQt4 import QtGui, QtCore
class WizardJournal(QtGui.QDialog):
def __init__(self, parent):
super(WizardJournal, self).__init__(parent)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.initUI()
def initUI(self):
self.ok_button = QtGui.QPushButton("OK", self)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.ok_button)
self.show()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
parent = QtGui.QWidget()
obj = WizardJournal(parent)
app.exec_()
print('\n'.join(repr(w) for w in app.allWidgets()))
The dialog class is now completely self-contained, and there is only one external python reference to the instance of it. (If you need to access the parent widget from within the dialog class, you can use self.parent()).
PS: when widgets are added to a layout, they will be automatically re-parented to whatever top-level widget eventually contains the layout. So, strictly speaking, it is not necessary to explicitly set a parent for such widgets in your code.

Smooth lazy loading and unloading of custom IndexWidget in QListView

I am writing an application that makes use of a custom QWidget in place of regular listitems or delegates in PyQt. I have followed the answer in Render QWidget in paint() method of QWidgetDelegate for a QListView -- among others -- to implement a QTableModel with custom widgets. The resulting sample code is at the bottom of this question. There are some problems with the implementation that I do not know how to solve:
Unloading items when they are not being displayed. I plan to build my application for a list that will have thousands of entries, and I cannot keep that many widgets in memory.
Loading items that are not yet in view or at least loading them asynchronously. Widgets take a moment to render, and the example code below has some obvious lag when scrolling through the list.
When scrolling the list in the implementation below, each newly loaded button when loading shows up at the top left corner of the QListView for a split second before bouncing into position. How can that be avoided?
--
import sys
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt
class TestListModel(QtCore.QAbstractListModel):
def __init__(self, parent=None):
QtCore.QAbstractListModel.__init__(self, parent)
self.list = parent
def rowCount(self, index):
return 1000
def data(self, index, role):
if role == Qt.DisplayRole:
if not self.list.indexWidget(index):
button = QtGui.QPushButton("This is item #%s" % index.row())
self.list.setIndexWidget(index, button)
return QtCore.QVariant()
if role == Qt.SizeHintRole:
return QtCore.QSize(100, 50)
def columnCount(self, index):
pass
def main():
app = QtGui.QApplication(sys.argv)
window = QtGui.QWidget()
list = QtGui.QListView()
model = TestListModel(list)
list.setModel(model)
list.setVerticalScrollMode(QtGui.QAbstractItemView.ScrollPerPixel)
layout = QtGui.QVBoxLayout(window)
layout.addWidget(list)
window.setLayout(layout)
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
You can use a proxy model to avoid to load all widgets. The proxy model can calculate the row count with the heights of the viewport and the widget. He can calculate the index of the items with the scrollbar value.
It is a shaky solution but it should work.
If you modify your data() method with:
button = QtGui.QPushButton("This is item #%s" % index.row())
self.list.setIndexWidget(index, button)
button.setVisible(False)
The items will not display until they are moved at their positions (it works for me).
QTableView only requests data to the model for items in its viewport, so the size of your data doesn't really affect the speed. Since you already subclassed QAbstractListModel you could reimplement it to return only a small set of rows when it's initialized, and modify its canFetchMore method to return True if the total amount of records hasn't been displayed. Although, with the size of your data, you might want to consider creating a database and using QSqlQueryModel or QSqlTableModel instead, both of them do lazy loading in groups of 256.
To get a smoother load of items you could connect to the valueChanged signal of your QTableView.verticalScrollBar() and depending on the difference between it's value and maximum have something like:
while xCondition:
if self.model.canFetchMore():
self.model.fetchMore()
Using setIndexWidget is slowing up your application considerably. You could use a QItemDelegate and customize it's paint method to display a button with something like:
class MyItemDelegate(QtGui.QItemDelegate):
def __init__(self, parent=None):
super(MyItemDelegate, self).__init__(parent)
def paint(self, painter, option, index):
text = index.model().data(index, QtCore.Qt.DisplayRole).toString()
pushButton = QtGui.QPushButton()
pushButton.setText(text)
pushButton.setGeometry(option.rect)
painter.save()
painter.translate(option.rect.x(), option.rect.y())
pushButton.render(painter)
painter.restore()
And setting it up with:
myView.setItemDelegateForColumn(columnNumber, myItemDelegate)

How to detect mouse click on images displayed in GUI created using PySide

Firstly, I'm new to Python, Qt and PySide so forgive me if this question seems too simple.
What I'm trying to do is to display a bunch of photos in a grid in a GUI constructed using PySide API. Further, when a user clicks on a photo, I want to be able to display the information corresponding to that photo. Additionally, I would like the container/widget used for displaying the photo to allow for the photo to be changed e.g. I should be able to replace any photo in the grid without causing the entire grid of photos to be created from scratch again.
Initially I tried to use QLabel to display a QPixmap but I realized (whether mistakenly or not) that I have no way to detect mouse clicks on the label. After some searching, I got the impression that I should subclass QLabel (or some other relevant class) and somehow override QWidget's(QLabel's parent class) mousePressEvent() to enable mouse click detection. Problem is I'm not sure how to do that or whether there is any alternative widget I can use to contain my photos other than the QLabel without having to go through subclass customization.
Can anyone suggest a more suitable container other than QLabel to display photos while allowing me to detect mouse clicks on the photo or provide some code snippet for subclassing QLabel to enable it to detect mouse clicks?
Thanks in advance for any replies.
I've added an example of how to emit a signal and connect to another slot. Also the docs are very helpful
from PySide.QtCore import *
from PySide.QtGui import *
import sys
class Main(QWidget):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
layout = QHBoxLayout(self)
picture = PictureLabel("pic.png", self)
picture.pictureClicked.connect(self.anotherSlot)
layout.addWidget(picture)
layout.addWidget(QLabel("click on the picture"))
def anotherSlot(self, passed):
print passed
print "now I'm in Main.anotherSlot"
class PictureLabel(QLabel):
pictureClicked = Signal(str) # can be other types (list, dict, object...)
def __init__(self, image, parent=None):
super(PictureLabel, self).__init__(parent)
self.setPixmap(image)
def mousePressEvent(self, event):
print "from PictureLabel.mousePressEvent"
self.pictureClicked.emit("emit the signal")
a = QApplication([])
m = Main()
m.show()
sys.exit(a.exec_())
Even if the question has been answered, i want to provide an other way that can be used in different situations (see below) :
from PySide.QtCore import *
from PySide.QtGui import *
import sys
class Main(QWidget):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
layout = QHBoxLayout(self)
picture = QLabel()
picture.setPixmap("pic.png")
layout.addWidget(picture)
layout.addWidget(QLabel("click on the picture"))
makeClickable(picture)
QObject.connect(picture, SIGNAL("clicked()"), self.anotherSlot)
def anotherSlot(self):
print("AnotherSlot has been called")
def makeClickable(widget):
def SendClickSignal(widget, evnt):
widget.emit(SIGNAL('clicked()'))
widget.mousePressEvent = lambda evnt: SendClickSignal(widget, evnt)
a = QApplication([])
m = Main()
m.show()
sys.exit(a.exec_())
This way doesn't imply subclassing QLabel so it can be used to add logic to a widget made with QtDeigner.
Pros :
Can be used over QTdesigner compiled files
Can be applied to any kind of widget (you might need to include a super call to the overrided function to ensure widget's normal behavior)
The same logic can be used to send other signals
Cons :
You have to use the QObject syntax to connect signals and slots

Categories