QLayout.replace not replacing - pyqt5

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.

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!

How do I change the dropEvent behavior correctly?

I am trying to change the default behavior when dropping a file in QtextEdit
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(869, 499)
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.textEdit = QtWidgets.QTextEdit(Dialog)
self.textEdit.setObjectName("textEdit")
self.horizontalLayout_2.addWidget(self.textEdit)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.textEdit.setAcceptDrops(True)
self.textEdit.dropEvent = self.dropEvent
def dropEvent(self, event):
event.setDropAction(QtCore.Qt.CopyAction)
if event.mimeData().hasText():
print(event.mimeData().text())
event.accept()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())
But after performing dropEvent, the cursor in TextEdit stops moving.
What am I missing?
QTextEdit uses internal flags (not exposed to the API) that properly update the text cursor during drag and drop operations, mostly to allow pasting in the exact position within the text based on the mouse cursor, and in the meantime show the "cursor caret" to the user so that they will know where the content would be inserted.
This means that the default implementation of QTextEdit dropEvent() must always be called in order to properly update the cursor.
Now, proper drag&drop implementation of QTextEdit should always be done through insertFromMimeData() (and eventually canInsertFromMimeData() to prevent drop at all).
If you want to alter the behavior when dropping certain contents, then just override that function:
from PyQt5 import QtWidgets
class DropEdit(QtWidgets.QTextEdit):
def insertFromMimeData(self, data):
if data.hasUrls():
self.insertPlainText('%ONEFILE%')
else:
super().insertFromMimeData(data)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
test = DropEdit()
test.show()
sys.exit(app.exec_())
Now, the issue is that insertFromMimeData() is called no matter if the operation is done from clipboard (through Ctrl+V or via the context menu) or from drag&drop. Another issue is that drag&drop can also happen within the text edit, for instance to move a selected text somewhere else.
A basic solution, which would prevent pasting from d&d but not from clipboard, would be to use an internal flag that can be set in the dropEvent() and would be cleared in insertFromMimeData().
The following example will accept drops only if the dropped data has no urls in it, but will still accept pasting from clipboard if it contains urls (for instance, copying an object in the file browser):
class DropEdit(QtWidgets.QTextEdit):
acceptDrop = True
def insertFromMimeData(self, data):
if self.acceptDrop:
super().insertFromMimeData(data)
self.acceptDrop = True
def dropEvent(self, event):
self.acceptDrop = not event.mimeData().hasUrls()
super().dropEvent(event)

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

Changing Multiple Widget Styles from pushButton in a loop

I am using PyQt5 to make a GUI, this GUI has several buttons on the top of the page which changes the index of the lower StackedWidget based on the selection. When the button is clicked, the selected button changes the style sheeet of that selected button to selected_style, but changes all others to unselected_style. I read the simplest and most universal way of doing this was to add single actions per line. But that has become quite redundant and culminates to excessive amounts of lines.This is currently being implemented by single lines, like:
self.pshBtn_1.clicked.connect(lambda: self.stckWdgt.setCurrentIndex(0))
self.pshBtn_1.clicked.connect(lambda: self.pshBtn_1.setStyleSheet(selected)
self.pshBtn_1.clicked.connect(lambda: self.pshBtn_2.setStyleSheet(unselected)
self.pshBtn_1.clicked.connect(lambda: self.pshBtn_3.setStyleSheet(unselected)
self.pshBtn_2.clicked.connect(lambda: self.stckWdgt.setCurrentIndex(1))
self.pshBtn_2.clicked.connect(lambda: self.pshBtn_2.setStyleSheet(selected)
self.pshBtn_2.clicked.connect(lambda: self.pshBtn_1.setStyleSheet(unselected)
self.pshBtn_2.clicked.connect(lambda: self.pshBtn_3.setStyleSheet(unselected)
# And so on, and so on, and so on
I attempted to loop through a list of the names of the buttons in a function under self, but I keep ending up with NameErrors. Something like:
self.pshBtn_1.clicked.connect(self.set_btn_style, pshBtn_1)
self.pshBtn_2.clicked.connect(self.set_btn_style, pshBtn_2)
self.pshBtn_3.clicked.connect(self.set_btn_style, pshBtn_3)
btn_dict = {pshBtn_1 : 0, pshBtn_2: 1, pshBtn_3: 2} #Button Name, Stacked Widget Index
def set_btn_style(self, var_name):
for i, j in btn_dict:
btn_name = i
idx = j
if btn_name == var_name:
self.keyname.setStyleSheet(button_selected)
self.stckWdgt.setCurrentIndex(idx))
else:
self.btn.setStyleSheet(button_unselected)
I guess the big question here is how can I have one function be used by several buttons or GUI actions, but pass in a specifc tableWidget, stackWidget, etc. as a parameter so I don't have to repeat some function 10 times for each button I want to use that function. Thanks.
Instead of setting a stylesheet each time the buttons are pressed, the simplest way is to enable the checkable property of the QPushButtons and thus use it as a filter in the Qt Style Sheet. To change the page and only one is selected, a QButtonGroup is used.
from PyQt5 import QtCore, QtWidgets
QSS = """
Button {
background-color: #00ff00;
}
Button:checked {
background-color: #ff0000;
}
"""
class Button(QtWidgets.QPushButton):
pass
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
page1 = QtWidgets.QLabel("page1", alignment=QtCore.Qt.AlignCenter)
page2 = QtWidgets.QLabel("page2", alignment=QtCore.Qt.AlignCenter)
page3 = QtWidgets.QLabel("page3", alignment=QtCore.Qt.AlignCenter)
options = ["Page1", "Page2", "Page3"]
stackedwidget = QtWidgets.QStackedWidget()
hlay = QtWidgets.QHBoxLayout()
group = QtWidgets.QButtonGroup(self)
group.buttonClicked[int].connect(stackedwidget.setCurrentIndex)
for i, (option, widget) in enumerate(zip(options, (page1, page2, page3))):
button = Button(text=option, checkable=True)
ix = stackedwidget.addWidget(widget)
group.addButton(button, ix)
hlay.addWidget(button)
if i == 0:
button.setChecked(True)
vbox = QtWidgets.QVBoxLayout(self)
vbox.addLayout(hlay)
vbox.addWidget(stackedwidget)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
app.setStyle("fusion")
app.setStyleSheet(QSS)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I run into this often where I want to pass a widget name to another function. I was able to solve this by making a nested dictionary to link one item to the widget name I want to pass in. The function is called in a for loop within the main class. In the function I use self.sender() and sending_button.objectName() to get the name of the item I used to start the function.
class Ui_MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(Ui_MainWindow, self).__init__(parent)
self.setupUi(self)
# ^^ Up here would be a bunch of buttons I want to do something with
# Make a nested Dictionary to hold the string name of button,
# the actual widget name, and the stacked widget index it corresponds to.
self.btn_change_sel = {'pushButton_1': {self.pushButton_1: 0},
'pushButton_2': {self.pushButton_2: 1} }
# if any button is pressed, launch function.
for item in self.btn_change_sel.keys():
for button in self.btn_change_sel[item].keys():
button.clicked.connect(self.btn_style_change)
def btn_style_change(self):
sending_button = self.sender()
check_sender = str(sending_button.objectName()) # Get the string name of sender button
for sender_name in self.btn_change_sel.keys(): # Get string name to match
# If the clicked button matches the string value, do some stuff
if check_sender == sender_name:
for btn_item in self.btn_change_sel[sender_name].keys():
btn_item.setStyleSheet(self.button_selected)
for stack_idx in self.btn_change_sel[sender_name].values():
self.stackedWidget_all.setCurrentIndex(stack_idx)
# Do something with all other buttons not selected, change to a different style
if check_sender != sender_name:
for btn_item2 in self.btn_change_sel[sender_name].keys():
btn_item2.setStyleSheet(self.button_unselected)
So on this example, if the clicked button matches, it changes the stylesheet and selects the stackedWidget that corresponds to the button click. All others receive the Unselected style. This makes the displayed widget button different so you easily can tell which widget page your on.

Moving a QGraphicsProxyWidget with ItemIgnoresTransformations after changing QGraphicsView scale

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 :)