QGraphicsItemGroup.removeFromGroup -- child items not correctly reparented to Scene - python

I'm trying to remove a QGraphicsItem from a QGraphicsItemGroup. When calling removeFromGroup, the item is removed (of course). However, it's then no longer visible in the Scene. I have to call Scene.addItem(item) in order for it to appear again. This is apparently something you shouldn't do (I'm given a warning for doing that). But I can't seem to find another workaround.
Here's a minimal example:
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.view = QGraphicsView()
self.scene = QGraphicsScene()
self.view.setScene(self.scene)
self.setCentralWidget(self.view)
def add_group(scene):
group = QGraphicsItemGroup()
text = QGraphicsTextItem()
text.setPlainText("I'm visible")
group.addToGroup(text)
scene.addItem(group)
# After this, text is no longer in group. However, it is no longer visible.
group.removeFromGroup(text)
assert not text in group.childItems()
# But text is still in scene.
assert text.scene() == scene
# this works (i.e. text becomes visible again). However, it also produces a
# warning: QGraphicsScene::addItem: item has already been added to this scene.
# The docs also advice against it.
scene.addItem(text)
# According to the docs, I thought this might work, but it gives me a TypeError.
# text.setParentItem(0)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainWindow()
add_group(main.scene)
main.show()
sys.exit(app.exec_())
Tips and hints are very welcome.

The QGraphicsTextItem can never be parented to a scene, because it's parent must be a QGraphicsItem (which QGraphicsScene does not inherit).
When the QGraphicsTextItem is created, it's parent is None. Its parent is set to group when it is added to it (QGraphicsItemGroup is a subclass of QGraphicsItem), then set back to None when it's removed from group.
Calling scene.addItem() is actually a NO-OP. Qt checks whether scene is the same as text.scene(), and if it is, it prints the warning and returns without doing anything.
The fact that it seems to "work" in some circumstances, is just an artifact of python's garbage-collecting mechanism.
If your test is re-cast in a more realistic way, the QGraphicsTextItem remains visible after removal from the group:
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.view = QGraphicsView(self)
self.scene = QGraphicsScene(self.view)
self.view.setScene(self.scene)
self.setCentralWidget(self.view)
self.group = QGraphicsItemGroup()
self.text = QGraphicsTextItem()
self.text.setPlainText("I'm visible")
self.group.addToGroup(self.text)
self.scene.addItem(self.group)
self.group.removeFromGroup(self.text)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())

The problem is that text is deleted since you don't have any reference to it after you remove it from the group, try this:
...
text = QGraphicsTextItem()
scene.text = text #just to keep the reference, ideally should be self.text = text
...
Now you don need scene.addItem(text)

Related

Where is the correct place to create/destroy and show/hide a QMdiSubWindow menu?

I am learning Python by creating an MDI application in PyQt5. This application contains a class derived from QMdiSubWindow. These sub-windows need their own menu to be added to the main menu-bar. Where is the 'correct' place to create and show/hide that part of the menu which is only relevant to the sub-window when it's in focus? And where should the menu be destroyed (if it doesn't happen automatically because ownership is taken by the parent)? My attempt at detecting when the sub-window gains/loses focus causes infinite recursion, presumably because the newly visible menu steals the focus back from the sub-window.
This is probably such a common requirement that it's not mentioned in the tutorials, but the only reference in the docs to sub-window menus seems to just refer to the system menu, and not the main menu-bar. Most other Q&A's just refer to activating other sub-windows from the main menu. Several hours of searching haven't quite got what I need, so thank you for your help in either pointing me to the right place in the docs, or by improving my code ... or even both!
A minimal app to illustrate the problem:
#!/usr/bin/python3
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class TestSubWin(QMdiSubWindow):
def __init__(self, parent=None):
super().__init__()
self.setWidget(QLabel("Hello world"))
self.own_menu = QMenu("Sub win menu")
parent.menuBar().addMenu(self.own_menu)
# Add sub-window actions to the menu here
## Causes infinite recursion
# def focusOutEvent(self, event):
# self.own_menu.setVisible(False)
#
# def focusInEvent(self, event):
# self.own_menu.setVisible(True)
class MainWindow(QMainWindow):
def __init__(self, parent = None):
super(MainWindow, self).__init__(parent)
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
file = bar.addMenu("File")
file.addAction("New")
file.triggered[QAction].connect(self.windowaction)
self.setWindowTitle("MDI demo")
def windowaction(self, q):
if q.text() == "New":
sub = TestSubWin(self)
self.mdi.addSubWindow(sub)
sub.show()
def main():
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
QMenu and QMenuBar don't take ownership of QActions (and QMenus), unless when created with the functions that accepts icon/title arguments.
This also means that you shall not need to destroy the menus, but only remove them from the menu bar.
The solution is to connect to the subWindowActivated signal, remove the previously added menu, retrieve the menu for the newly active sub window (if any) and add it.
Note that in order to remove a menu from QMenuBar you have to use removeAction() along with the menuAction(), which is the action associated with the menu and shown as menubar title for the menu (or item in a menu for sub menus).
In the following example I'm creating a base subclass for any mdi subwindows that will support menubar menus, and further subclasses for different window types.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class MenuSubWin(QMdiSubWindow):
own_menu = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
def menu(self):
return self.own_menu
class TestSubWin1(MenuSubWin):
def __init__(self):
super().__init__()
self.setWidget(QLabel("Hello world"))
self.own_menu = QMenu("Sub win menu 1")
self.own_menu.addAction('Test 1')
class TestSubWin2(MenuSubWin):
def __init__(self):
super().__init__()
self.setWidget(QLabel("How are you?"))
self.own_menu = QMenu("Sub win menu 2")
self.own_menu.addAction('Test 2')
class MainWindow(QMainWindow):
def __init__(self, parent = None):
super(MainWindow, self).__init__(parent)
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
fileMenu = bar.addMenu("File")
new1Action = fileMenu.addAction("New 1")
new1Action.setData(TestSubWin1)
new2Action = fileMenu.addAction("New 2")
new2Action.setData(TestSubWin2)
fileMenu.triggered.connect(self.newWindow)
self.setWindowTitle("MDI demo")
self.subWinMenu = None
self.mdi.subWindowActivated.connect(self.subWindowActivated)
def subWindowActivated(self, subWindow):
if self.subWinMenu:
self.menuBar().removeAction(self.subWinMenu.menuAction())
self.subWinMenu = None
if subWindow is None or not hasattr(subWindow, 'menu'):
return
self.subWinMenu = subWindow.menu()
if self.subWinMenu:
self.menuBar().addMenu(self.subWinMenu)
def newWindow(self, action):
cls = action.data()
if not cls:
return
sub = cls()
self.mdi.addSubWindow(sub)
sub.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
Notes:
you shall always set the WA_DeleteOnClose attribute when directly adding a QMdiSubWindow (as opposed to adding a QWidget), otherwise the window will still exist for the MDI area and listed in the subWindowList(), thus preventing proper focus switching (and menu removal) upon closure;
for simplicity, I used the setData() feature of QAction with the class of the window that has to be created;
specifying the signature of signals is only required when signals do have overrides, which is unnecessary for triggered() since it has no overrides; note that Qt is gradually removing signal overrides, preferring explicit and unique signals instead;

PyQt5 How to apply different QStyles to different widgets?

I am trying to make multiple widgets with different styles from the built in styles provided by QStyleFactory, but when I run my code they all look the same. How can I fix this?
from PyQt5 import QtWidgets, QtCore, QtGui
import sys
class Demo(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.container = QtWidgets.QWidget()
self.setCentralWidget(self.container)
self.layout = QtWidgets.QVBoxLayout()
self.container.setLayout(self.layout)
self.btn = QtWidgets.QPushButton("button")
self.lw = QtWidgets.QListWidget()
self.lw.addItems(["one", "two", "three"])
self.layout.addWidget(self.btn)
self.layout.addWidget(self.lw)
self.resize(400, 150)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
widgets = []
for style_name in QtWidgets.QStyleFactory.keys():
demo = Demo()
demo.setWindowTitle(style_name)
style = QtWidgets.QStyleFactory.create(style_name)
demo.setStyle(style)
widgets.append(demo)
sys.exit(app.exec_())
An important aspect of setting a QStyle on widgets (instead of setting it on the whole application) is reported in the QWidget.setStyle() documentation:
Setting a widget's style has no effect on existing or future child widgets.
So, what's happening is that you're setting the style on the QMainWindow only, while the children will always use the QApplication style.
What you could try to do is to manually set the style for the existing children:
for style_name in QtWidgets.QStyleFactory.keys():
demo = Demo()
demo.setWindowTitle(style_name)
style = QtWidgets.QStyleFactory.create(style_name)
demo.setStyle(style)
for child in demo.findChildren(QtWidgets.QWidget):
child.setStyle(style)
widgets.append(demo)
In any case, the above approach has a drawback: any new children created after setting the style will still inherit the QApplication style. The only way to avoid this is to watch for childEvent() by (recursively) installing an event filter on the parent, and set the styles accordingly; note that you need to watch for StyleChange events too.
class ChildEventWatcher(QtCore.QObject):
def __init__(self, parentWidget):
super().__init__()
self.parentWidget = parentWidget
self.parentWidget.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.ChildAdded and isinstance(event.child(), QtWidgets.QWidget):
event.child().installEventFilter(self)
event.child().setStyle(self.parentWidget.style())
for child in event.child().findChildren(QtWidgets.QWidget):
child.installEventFilter(self)
child.setStyle(self.parentWidget.style())
elif event.type() == QtCore.QEvent.StyleChange and source == self.parentWidget:
for child in self.parentWidget.findChildren(QtWidgets.QWidget):
child.setStyle(self.parentWidget.style())
return super().eventFilter(source, event)
class Demo(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# this *must* be created before adding *any* child
self.childEventWatcher = ChildEventWatcher(self)
# ...
Also remember another important aspect the documentation warns about:
Warning: This function is particularly useful for demonstration purposes, where you want to show Qt's styling capabilities. Real applications should avoid it and use one consistent GUI style instead.
While the above code will do what you're expecting, installing an event filter on all child QWidgets is not a good thing to do, especially if you only need to do the style change (which is something that should normally be done just once, possibly at the start of the program). Considering the warning about using different styles, I highly suggest you to do this exactly as suggested: for demonstration purposes only.
You can set it on the application.
from PyQt5 import QtWidgets, QtCore, QtGui
import sys
class Demo(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.container = QtWidgets.QWidget()
self.setCentralWidget(self.container)
self.layout = QtWidgets.QVBoxLayout()
self.container.setLayout(self.layout)
self.btn = QtWidgets.QPushButton("button")
self.lw = QtWidgets.QListWidget()
self.lw.addItems(["one", "two", "three"])
self.layout.addWidget(self.btn)
self.layout.addWidget(self.lw)
self.resize(400, 150)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
win = Demo()
app.setStyle(QtWidgets.QStyleFactory.create("Windows")) # Set style theme on app
sys.exit(app.exec_())

PyQt5 can't create Tabbars or Tabs

I have been struggling to learn PyQt5 (and object oriented programming). In my current script I need to create a tabbed interface but can't seem to manage it. I suspect the problem is related to OOP (I am a novice). "self" seems to be the problem, and I kind of know what that means but not enough to be able to fix it. Below is my latest attempt. It seems like I am using the "wrong self", from elsewhere in the script. I want very much to understand object oriented programming - thanks in advance to anyone kind enough to help!
Some of the code/errors are:
code:
tabbar = QTabBar()
tab1 = QTabWidget()
tabbar.addTab(tab1, 'tab1')
error:
TypeError: arguments did not match any overloaded call:
addTab(self, str): argument 1 has unexpected type 'QTabWidget'
addTab(self, QIcon, str): argument 1 has unexpected type 'QTabWidget'
And here's the code:
class App(QMainWindow):
def launch(self, filepath):
subprocess.run(filepath)
def newLauncher(self, matrix):
pass # cut for brevity
def __init__(self):
super(App, self).__init__()
tabbar = QTabBar()
tab1 = QTabWidget()
index = tabbar.addTab(tab1, 'tab1')
self.initUI()
def initUI(self):
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
It is good that you want to learn about OOP, but that is not the main problem in this case, but it seems that you do not read the documentation. If we check that it is a QTabBar it would see that it refers to the top part of the QTabWidget, it is that part with buttons.
You do not have to use QTabBar but QTabWidget, QTabWidget has the addTab method that requires as a first parameter the widget that will be displayed on a page, and as a second parameter a title that will appear on the buttons.
Another mistake that I see in your code is that you create the widget but not setting it as part of another widget are just local variables that we know are deleted when the function is finished.
Since you are using QMainWindow you must set QTabWidget as part of a central widget, for this we can use the layouts.
import sys
from PyQt5.QtWidgets import *
class App(QMainWindow):
def __init__(self):
super(App, self).__init__()
centralWidget = QWidget()
lay = QVBoxLayout(centralWidget)
tab = QTabWidget()
lay.addWidget(tab)
for i in range(5):
page = QWidget()
tab.addTab(page, 'tab{}'.format(i))
self.setCentralWidget(centralWidget)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
ex.show()
sys.exit(app.exec_())

PyQt widget keyboard focus

First off -- thanks for this group! I started delving into PyQt a month or so ago. In that time, I've bumped up against many questions, and virtually always found an answer here.
Until now.
I have a workaround for this, but I think it's a kluge and there probably is a proper way. I'd like to understand better what's going on.
Here's the code:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class FormWidget(QWidget):
def __init__(self, parent):
super(FormWidget, self).__init__(parent)
# Create view with image in it
self.image = QGraphicsPixmapItem(QPixmap())
self.scene = QGraphicsScene()
self.scene.addItem(self.image)
self.view = QGraphicsView(self.scene)
self.hlayout = QHBoxLayout()
self.hlayout.addWidget(self.view)
self.setLayout(self.hlayout)
# self.view.keyPressEvent = self.keyPressEvent
def keyPressEvent(self, event):
key = event.key()
mod = int(event.modifiers())
print(
"<{}> Key 0x{:x}/{}/ {} {} {}".format(
self,
key,
event.text(),
" [+shift]" if event.modifiers() & Qt.SHIFT else "",
" [+ctrl]" if event.modifiers() & Qt.CTRL else "",
" [+alt]" if event.modifiers() & Qt.ALT else ""
)
)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
form = FormWidget(self)
self.setCentralWidget(form)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
sys.exit(app.exec_())
As is, all keyboard input is detected by the overloaded keyPressEvent() function, except arrow keys. I've found enough posts talking about this to have a sense that it is because the child widget (self.view) is receiving them. I presume the child widget is, in fact, receiving all the keystrokes, but ignoring the ones that are getting through, and sucking up the arrow keys, which is why they aren't getting to the parent's keyPressEvent() function. That seems to be so, because if I uncomment the line in the middle:
self.view.keyPressEvent = self.keyPressEvent
It behaves as I expect -- the parent's keyPressEvent() gets all the keystrokes, arrows included.
How would I tell the child widget to ignore all keystrokes? I thought maybe this:
self.view.setFocusPolicy(Qt.NoFocus)
When I add that, keyPressEvent() doesn't see any keystrokes at all.
I suppose I could overload keyPressEvent() for the child as well, and just explicitly pass everything up to the parent. But that doesn't seem better than my kluge.
I think I must be misunderstanding something here.
Thanks. Just looking to learn ...
By default, a QWidget does not accept the keyboard focus, so you need to enable it explicitly:
class FormWidget(QWidget):
def __init__(self, parent):
...
self.setFocusPolicy(Qt.StrongFocus)
Rather than subclassing the child widget or attempting to prevent keystrokes from reaching it, you should consider using an eventFilter to capture events on the child widget. You will see all events before the child widget, and you can suppress or transform them.
http://doc.qt.io/qt-5.5/qobject.html#eventFilter

Why does itemAt() not always find QGraphicsItem

Consider this extremely simple example in which you can drag a square around a QGraphicsScene (using PyQt, C++ users read self as this)
import sys
from PyQt4 import QtGui, QtCore
class MainWindowUi(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.scene = Scene(0, 0, 300, 300, self)
self.view = QtGui.QGraphicsView()
self.setCentralWidget(self.view)
self.view.setScene(self.scene)
self.scene.addItem(Square(0,0,50,50))
class Scene(QtGui.QGraphicsScene):
def mousePressEvent(self, e):
self.currentItem = self.itemAt(e.pos())
print (self.currentItem)
QtGui.QGraphicsScene.mousePressEvent(self, e)
class Square(QtGui.QGraphicsRectItem):
def __init__(self, *args):
QtGui.QGraphicsRectItem.__init__(self, *args)
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
win = MainWindowUi()
win.show()
sys.exit(app.exec_())
When you click the mouse in the scene you should see a print statement telling you that you clicked either on the square or on nothing (ie. None). This works if you just start the program and click on the square.
Now drag the square away from the upper left corner and click on it again. This time itemAt() return None even when you click on the square.
What's going on?
The answer seems to be that I should have used self.itemAt(e.scenePos()) instead of self.itemAt(e.pos()). I found this in this SO question.
I note here that the reason I've been having trouble finding information on this issue is that moving QGraphicsItems around in a QGraphicsScene is not what Qt calls "drag and drop". To search for information on this topic you want to search for things like "move QGraphicsItem".

Categories