Moving a QGraphicsProxyWidget with ItemIgnoresTransformations after changing QGraphicsView scale - qt5

I have a QGraphicsScene that contains multiple custom QGraphicsItems. Each item contains a QGraphicsProxyWidget which itself contains whatever widgets are needed by the business logic. The proxy has a Qt::Window flag applied to it, so that it has a title bar to move it around. This is all working well, except when moving a proxy widget when the view has been scaled.
The user can move around the scene à la google maps, ie by zooming out then zooming in back a little farther away. This is done with calls to QGraphicsView::scale. Items should always be visible no matter the zoom value, so they have the QGraphicsItem::ItemIgnoresTransformations flag set.
What happens when moving a proxyWidget while the view has been scaled is that on the first move event the widget will jump to some location before properly being dragged.
I had this issue with Qt5.7.1, and could reproduce it with PyQt5 as it is simpler to reproduce and hack around, please see the snippet below.
Steps to reproduce:
move the widget around, notice nothing unusual
use the mouse wheel to zoom in or out. The higher the absolute scale, the higher the effect on the issue.
click on the widget, and notice how it jumps on the first moving of the mouse.
Snippet:
import sys
import PyQt5
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsProxyWidget, QGraphicsWidget, QGraphicsObject
global view
global scaleLabel
def scaleScene(event):
delta = 1.0015**event.angleDelta().y()
view.scale(delta, delta)
scaleLabel.setPlainText("scale: %.2f"%view.transform().m11())
view.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
# create main widget
w = QWidget()
w.resize(800, 600)
layout = QVBoxLayout()
w.setLayout(layout)
w.setWindowTitle('Example')
w.show()
# rescale view on mouse wheel, notice how when view.transform().m11() is not 1,
# dragging the subwindow is not smooth on the first mouse move event
w.wheelEvent = scaleScene
# create scene and view
scene = QGraphicsScene()
scaleLabel = scene.addText("scale: 1")
view = QGraphicsView(scene)
layout.addWidget(view)
view.show();
# create item in which the proxy lives
item = QGraphicsWidget()
scene.addItem(item)
item.setFlag(PyQt5.QtWidgets.QGraphicsItem.ItemIgnoresTransformations)
item.setAcceptHoverEvents(True)
# create proxy with window and dummy content
proxy = QGraphicsProxyWidget(item, Qt.Window)
button = QPushButton('dummy')
proxy.setWidget(button)
# start app
sys.exit(app.exec_())
The jump distance is:
proportional to the scaling of the view , and to the distance of the mouse from the scene origin
goes from scene position (0,0) towards the mouse position (I think)
might be caused by the proxy widget not reporting the mouse press/move properly. I'm hinted at this diagnostic after looking at QGraphicsProxyWidgetPrivate::mapToReceiver in qgraphicsproxywidget.cpp (sample source), which does not seem to take scene scaling into account.
I am looking for either
confirmation that this is an issue with Qt and I did not misconfigured the proxy.
an explanation on how fix the mouse location given by the proxy to its children widgets (after installing a eventFilter)
any other workaround
Thanks

Almost 2 years later I got back to this issue again, and finally found a solution. Or rather a workaround, but a simple one at least. It turns out I can easily avoid getting into the issue with local/scene/ignored transforms in the first place.
Instead of parenting the QGraphicsProxyWidget to a QGraphicsWidget, and explicitly setting the QWidget as proxy target, I get the proxy directly from the QGraphicsScene, letting it set the window flag on the wrapper, and set the ItemIgnoresTransformations flag on the proxy. Then (and here's the workaround) I install an event filter on the proxy, intercept the GraphicsSceneMouseMove event where I force the proxy position to currentPos+mouseDelta (both in scene coordinates).
Here's the code sample from above, patched with that solution:
import sys
import PyQt5
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
global view
global scaleLabel
def scaleScene(event):
delta = 1.0015**event.angleDelta().y()
view.scale(delta, delta)
scaleLabel.setPlainText("scale: %.2f"%view.transform().m11())
view.update()
class ItemFilter(PyQt5.QtWidgets.QGraphicsItem):
def __init__(self, target):
super(ItemFilter, self).__init__()
self.target = target
def boundingRect(self):
return self.target.boundingRect()
def paint(self, *args, **kwargs):
pass
def sceneEventFilter(self, watched, event):
if watched != self.target:
return False
if event.type() == PyQt5.QtCore.QEvent.GraphicsSceneMouseMove:
self.target.setPos(self.target.pos()+event.scenePos()-event.lastScenePos())
event.setAccepted(True)
return True
return super(ItemFilter, self).sceneEventFilter(watched, event)
if __name__ == '__main__':
app = QApplication(sys.argv)
# create main widget
w = QWidget()
w.resize(800, 600)
layout = QVBoxLayout()
w.setLayout(layout)
w.setWindowTitle('Example')
w.show()
# rescale view on mouse wheel, notice how when view.transform().m11() is not 1,
# dragging the subwindow is not smooth on the first mouse move event
w.wheelEvent = scaleScene
# create scene and view
scene = QGraphicsScene()
scaleLabel = scene.addText("scale: 1")
view = QGraphicsView(scene)
layout.addWidget(view)
view.show();
button = QPushButton('dummy')
proxy = scene.addWidget(button, Qt.Window)
proxy.setFlag(PyQt5.QtWidgets.QGraphicsItem.ItemIgnoresTransformations)
itemFilter = ItemFilter(proxy)
scene.addItem(itemFilter)
proxy.installSceneEventFilter(itemFilter)
# start app
sys.exit(app.exec_())
Hoping this may help someone who's ended up in the same dead end I was :)

Related

Setting the view of a scene via mlab in traitsui is not working

I am trying to code a program based on traitsUI and Mayavi, but I have some problems. Following the code I am using:
#!/usr/bin/env python
import os
from traits.api import HasTraits, Instance, String, on_trait_change
from traitsui.api import View, Item
from tvtk.pyface.scene_editor import SceneEditor
from mayavi.tools.mlab_scene_model import MlabSceneModel
from mayavi.core.ui.mayavi_scene import MayaviScene
class ActorViewer(HasTraits):
scene = Instance(MlabSceneModel, ())
view = View(Item(name='scene',
editor=SceneEditor(scene_class=MayaviScene),
show_label=True,
resizable=True,
dock='tab',
height=500,
width=500),
resizable=True
)
def __init__(self, engine=None, **traits):
HasTraits.__init__(self, **traits)
if engine is not None:
self.scene=MlabSceneModel(engine=engine)
else:
self.scene=MlabSceneModel()
self.generate_data()
#on_trait_change('scene.activated')
def generate_data(self):
src=self.scene.mlab.pipeline.open(Path+i)
self.scene.mlab.view(40, 50)
self.scene.mlab.pipeline.outline(src)
self.scene.mlab.pipeline.iso_surface(src, contours=60, opacity=0.5)
if __name__ == '__main__':
Path = "/path/to/my/folder"
filelist = os.listdir(Path)
for i in filelist:
if i.endswith(".vtr"):
if ("E1_" in i) or ("E2_" in i):
print("file name ", i)
a = ActorViewer()
a.configure_traits()
The call self.scene.mlab.view(40, 50) returns AttributeError: 'NoneType' object has no attribute 'active_camera', thus I don't know how to set the camera. I have read that it is related to when the scene is activated, but I couldn't find a solution.
Without setting the view, the code works, but each file is loaded and rendered alone. In order to proceed with the main loop, each render has to be closed. I would like to dock each of the file without closing them.
I couldn't find a way to set a custom label to each tab after allowing show_label=True and to have it aligned horizontally at the top of the scene.
I tried to set the outline with the 'cornered' layout, but I couldn't find a way to do that. self.scene.mlab.pipeline.outline.outline_mode('cornered') gets simply ignored.
Thank you for your help!

Fault in my resource file or resource import for PyQt5?

I'm new to working with PyQt for GUI development. As a tutorial for one of the trainings, I'm to develop a phone dialer, which basically just displays a number pad and makes the appropriate dial tones when pressed. This involves importing a resource file that contains the 12 tones. i downloaded the tones from the internet and tested them. They all work just fine. however after I create the resource file, and convert it using pyrcc5, the tones do not play. I've copied the resources.py from the tutorial and the dial pad works as expected. I'm wondering if anyone can help me debug this issue. I haven't seen anything online that shows a similar problem.
if someone can help me attach files, I can add the resource files to the question as well.
Here is a copy of my code although it doesn't appear to be the problem:
import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtMultimedia as qtmm
import resources1
class SoundButton(qtw.QPushButton):
def __init__(self, wav_file, *args, **kwargs):
super().__init__(*args, **kwargs)
self.wav_file = wav_file
self.player = qtmm.QSoundEffect()
self.player.setSource(qtc.QUrl.fromLocalFile(wav_file))
self.clicked.connect(self.player.play)
class MainWindow(qtw.QMainWindow):
def __init__(self):
"""MainWindow constructor.
This widget will be our main window.
We'll define all the UI components in here.
"""
super().__init__()
# Main UI code goes here
dialpad = qtw.QWidget()
self.setCentralWidget(dialpad)
dialpad.setLayout(qtw.QGridLayout())
for i, symbol in enumerate('123456789*0#'):
button = SoundButton(f':/dtmf/{symbol}.wav', symbol)
row = i // 3
column = i % 3
dialpad.layout().addWidget(button, row, column)
# End main UI code
self.show()
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
# it's required to save a reference to MainWindow.
# if it goes out of scope, it will be destroyed.
mw = MainWindow()
sys.exit(app.exec())
Thanks in advance for your help

QLayout.replace not replacing

I have the following code to replace a widget (self.lbl) each time I click on a button (self.btn):
import sys
from PySide2.QtCore import Slot
from PySide2.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget, \
QPushButton
class Workshop(QWidget):
def __init__(self):
super().__init__()
self.n = 0
self.btn = QPushButton('Push me')
self.lbl = QLabel(str(self.n))
self.main_layout = QVBoxLayout()
self.sub_layout = QVBoxLayout()
self.sub_layout.addWidget(self.lbl)
self.sub_layout.addWidget(self.btn)
self.main_layout.addLayout(self.sub_layout)
self.btn.clicked.connect(self.change_label)
self.setLayout(self.main_layout)
self.show()
#Slot()
def change_label(self):
new_label = QLabel(str(self.n + 1))
self.main_layout.replaceWidget(self.lbl, new_label)
self.n += 1
self.lbl = new_label
if __name__ == '__main__':
app = QApplication()
w = Workshop()
sys.exit(app.exec_())
Right after its initialization, the object w looks like this:
When I click on the "Push me" button (self.btn), the number is incremented as wanted, but the initial "0" remains in the background:
But the other numbers do not however remain in the background ; only "0" does. Fore example, here is "22" (result after I clicked 22 times on "Push me"):
Note: I know that I could achieve the resultant I want with the setText method, but this code is just a snippet that I will adapt for a class in which I will not have a method like setText.
Thank you!
When you replace the widget in the layout, the previous one still remains there.
From replaceWidget():
The parent of widget from is left unchanged.
The problem is that when a widget is removed from a layout, it still keeps its parent (in your case, the Workshop instance), so you can still view it. This is more clear if you set the alignment to AlignCenter for each new QLabel you create: you'll see that if you add a new label and resize the window, the previous one will keep its previous position:
class Workshop(QWidget):
def __init__(self):
# ...
self.lbl = QLabel(str(self.n), alignment=QtCore.Qt.AlignCenter)
# ...
def change_label(self):
new_label = QLabel(str(self.n + 1), alignment=QtCore.Qt.AlignCenter)
# ...
You have two possibilities, which are actually very similar:
set the parent of the "removed" widget to None: the garbage collector will remove the widget as soon as you overwrite self.lbl:
self.lbl.setParent(None)
remove the widget by calling deleteLater() which is what happens when reparenting a widget to None and, if it has no other persisting references, gets garbage collected:
self.lbl.deleteLater()
For your pourposes, I'd suggest you to go with deleteLater(), as calling setParent() (which is a reimplementation of QObject's setParent) actually does lots of other things (most importantly, checks the focus chain and resets the widget's window flags), and since the widget is going to be deleted anyway, all those things are actually unnecessary, and QObject's implementation of setParent(None) would be called anyway.
The graphic "glitch" you are facing might depend on the underlying low-level painting function, which has some (known) unexpected behaviors on MacOS in certain cases.

Enable/disable text entry in IntCtrl on RadioButton click in wxPython

I have a simple gui that has two radio buttons and an IntCtrl for text entry. Initially, I have the top radio button selected and the IntCtrl disabled (unfortunately, I can't figure out how to set it to blank or "grayed out"):
Relevant code snippet:
def loadSettingsPanel(self):
panel = wx.Panel(self)
self.exposureAutomatic = wx.RadioButton(panel, label="Automatic (1ms)", style=wx.RB_GROUP)
self.exposureManual = wx.RadioButton(panel, label="Manual")
self.exposureValue = wx.lib.intctrl.IntCtrl(panel, style=wx.TE_READONLY)
self.exposureManual.Bind(wx.EVT_RADIOBUTTON, self.onClick)
# Add sizers, etc.
I want to "enable" the IntCtrl area in the onClick method, but I can't figure out how to do this. SetStyle() doesn't seem to have the option clear out the wx.TE_READONLY styling and I'd prefer not to recreate the IntCtrl entirely because then it gets annoying to reshuffle everything in the sizer. If there's some way to do this using a TextCtrl, I'd be happy to switch to that and just do the character filtering manually, but I haven't been able to figure out how to enable/disable those either.
Use the Enable function rather than a style.
import wx
import wx.lib.intctrl
class MyFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "Intctrl Demo")
panel = wx.Panel(self)
self.exposureAutomatic = wx.RadioButton(panel, label="Automatic (1ms)", style=wx.RB_GROUP, pos=(50,50))
self.exposureManual = wx.RadioButton(panel, label="Manual", pos=(50,80))
self.ic = wx.lib.intctrl.IntCtrl(panel, -1, pos=(150, 80))
self.ic.Enable(False)
self.Bind(wx.EVT_RADIOBUTTON, self.onClick)
def onClick(self, event):
self.ic.Enable(self.exposureManual.GetValue())
app = wx.App()
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()

How to launch the QMainwindow from QDialog after the interval selected by user on QDialog combobox using QTimer

I have a QMainWIndow called Main which calls QDialog called popup_on_waiver. QDialog has a combobox to select number of hours. Once user selects hours and clicks Ok, I want to close the popup, hide the QMainwindow and launch the QMainwindow after selected number of hours from combobox. Program works until user selects hours and cliks ok. It closes popup and hides main window.(Requirement is that app has to be running in hidden forever, so hiding the main window). When it calls launch_after_interval, its failing with error "Process finished with exit code 1073741845". Please advise on the correct steps.
I am launching the Main window on certain other conditions that are not provided below so I am writing a separate block for launching the main window again after waiver hours selected by the user. Also, I tried to fetch the result of popup window, accepted or rejected but it didnt return anything.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import QUrl, Qt, QTimer, QSize, QRect
import sys
class popup_on_waiver(QDialog):
#pop up window
def __init__(self, parent=None):
super(QDialog,self).__init__(parent)
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.setMinimumSize(QSize(660, 340))
self.setWindowTitle("Waiver")
self.cb = QComboBox() #combobox
self.cb.setGeometry(QRect(40, 40, 100, 30))
self.cb.addItems(["1", "2", "3", "4"])
self.cb.currentIndexChanged[str].connect(self.returnInterval)
self.cb.setObjectName("combobox")
self.cb.move(80, 80)
self.buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self)
self.buttons.accepted.connect(self.hide_main)
self.buttons.rejected.connect(self.reject) #buttons
vbox = QVBoxLayout(self) #layout
vbox.addWidget(self.cb)
vbox.addWidget(self.buttons)
self.setLayout(vbox)
def hide_main(self, hours):
self.accept
self.parent().hide()
launch_after_interval(self.interval) #calling timer function
def returnInterval(self, hours): #combobox value that is number of hours
self.interval = int(hours) * 3600 * 1000
#QMainwindow
class Main(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.initUI()
def initUI(self):
self.centralwidget = QWidget(self)
self.Waiver = QPushButton('Waiver')
self.Waiver.clicked.connect(lambda: self.popup())
hbox = QHBoxLayout()
hbox.addWidget(self.Waiver)
self.centralwidget.setLayout(hbox)
self.setGeometry(50, 50, 1200, 600)
self.setWindowTitle("Timesheet")
self.setWindowIcon(QIcon(""))
self.setStyleSheet("background-color:")
self.setCentralWidget(self.centralwidget)
self.show()
def popup(self):
self.p = popup_on_waiver()
self.p.exec_()
def launch_after_interval(interval):
timer = QTimer()
timer.setSingleShot(True)
timer.setInterval(interval)
timer.timeout().connect(lambda: Main())
timer.start()
There are various problems with your code:
you create the dialog without setting the parent, so when you try to call self.parent().hide() it won't work because parent() returns None, which obviously doesn't have a hide attribute;
you have connected the accepted signal to hide_main, which requires an argument, but the accepted signal doesn't have any;
you missed the parentheses of accepted in hide_main, so it wouldn't be called;
self.interval is set only whenever the index of the combo is changed, but if the user doesn't change it (by leaving the default value), there won't be any self.interval set;
you are setting the WindowStaysOnTopHint flag only, which will reset any other window flags; the result will be that you won't have a new window, but a widget that is "embedded" in the parent; to correctly set the flag you should use self.setWindowFlags(self.flags() | Qt.WindowStaysOnTopHint);
signals cannot be "called", so there should be no parentheses in timer.timeout().connect;
the timer object has no reference outside the scope of launch_after_interval, nor it has no parent object set, so it will be deleted as soon as the function returns and will never be fired;
Revised code (modifications are in bold):
class popup_on_waiver(QDialog):
def __init__(self, parent=None):
super(QDialog,self).__init__(parent)
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
self.setMinimumSize(QSize(660, 340))
self.setWindowTitle("Waiver")
self.cb = QComboBox() #combobox
self.cb.setGeometry(QRect(40, 40, 100, 30))
self.cb.addItems(["1", "2", "3", "4"])
self.cb.currentIndexChanged[str].connect(self.returnInterval)
self.cb.setObjectName("combobox")
self.cb.move(80, 80)
self.buttons = QDialogButtonBox(
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal, self)
self.buttons.accepted.connect(self.hide_main)
self.buttons.rejected.connect(self.reject)
vbox = QVBoxLayout(self)
vbox.addWidget(self.cb)
vbox.addWidget(self.buttons)
self.setLayout(vbox)
# set the default interval
self.interval = 3600000
# no arguments here!
def hide_main(self):
self.accept() # <-- the parentheses!
self.parent().hide()
launch_after_interval(self.interval)
def returnInterval(self, hours):
self.interval = int(hours) * 3600 * 1000
class Main(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
self.initUI()
def initUI(self):
self.centralwidget = QWidget(self)
self.Waiver = QPushButton('Waiver')
# if the function does not have arguments, lambda is usually not required
self.Waiver.clicked.connect(self.popup)
hbox = QHBoxLayout()
hbox.addWidget(self.Waiver)
self.centralwidget.setLayout(hbox)
self.setGeometry(50, 50, 1200, 600)
self.setWindowTitle("Timesheet")
self.setWindowIcon(QIcon(""))
self.setStyleSheet("background-color:")
self.setCentralWidget(self.centralwidget)
self.show()
def popup(self):
# the parent is required to make the dialog modal *and* allow it
# to call parent().hide()!
self.p = popup_on_waiver(self)
self.p.exec_()
def launch_after_interval(interval):
# set a parent QObject for the timer, so that it's not removed
# when the function returns
timer = QTimer(QApplication.instance())
timer.setSingleShot(True)
timer.setInterval(interval)
timer.timeout.connect(lambda: Main())
timer.start()
Other relatively minor issues:
similar attribute names (like centralwidget, which is too similar to QMainWindow's centralWidget()) should be avoided, as they can create confusion and lead to hard to find bugs and issues;
a timer that acts on an object should not be created outside of the object that will eventually call/access/show it (even if indirectly); while technically there's nothing wrong with it, it's usually better to keep objects "organized", so that they can be accessed if required (for example, showing the window and stopping the timer before it times out);
creating a new instance of the main window is not suggested, as one already exists; this is related to the previous point: if you have a direct reference to the timer and the window, you can also call self.someWindow.show();
avoid mixing and confusing naming styles: you've used upper case names for attributes (Waiver) and lower for classes (popup_on_waiver), while it should be the opposite; then there's also mixedCase (returnInterval) and under_score (hide_main); choose a style and keep that one (read more about it in the style guide for Python code, aka PEP-8);
I preferred to edit only the parts of your code that prevented the program to work, but you should really keep the aspects above in mind, even if they are "relatively minor" (emphasis on relatively).
Finally, (trivial, but not irrelevant): mixing import modes from the same modules should be avoided: you either use wildcard imports like from module import * (but you normally shouldn't) or explicit ones like from module import ClassA, ClassB, [...]; for big modules like PyQt5, it's common to import the submodule, like from PyQt5 import QtWidgets:
Good:
from PyQt5 import QtWidgets
class SomeWidget(QtWidgets.QWidget):
# ...
Also good, but tends to be very obnoxious as you have to remember to add classes each time you need a new one and you might end up with a really long list of imports, possibly resulting in unnecessary classes as you ended up in not using some (also, I doubt there's a substantial benefit, at least on Qt):
from PyQt5.QtWidgets import QWidget, QHBoxLayout # etc...
class SomeWidget(QWidget):
# ...
Not so good, but it works (keeping submodule names can be useful to keep also in mind their "scope") and behaves as the previous one:
from PyQt5.QtWidgets import *
And this, this is simply wrong (I mean, it works, but it doesn't make any sense):
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QWidget