How to get objectName of a pyqtgraph plotwidget during a mouse wheelEvent? - python

I am trying to identify the object name of a pyqtgraph plotwidget I am mouse wheeling on. However, I can only seem to get the object id "PyQt5.QtWidgets.QWidget object at 0x0000018ED2ED74C8". If I use the QApplication.widgetAt(event.globalPos()).objectName I get nothing, even though I have set the object name. Can you help me?
Sample code:
# Import packages
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout
import pyqtgraph as pg
import sys
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.graphLayout = QHBoxLayout()
self.graph = pg.PlotWidget(name="graph1")
self.graph.setObjectName("graph1")
self.graphLayout.addWidget(self.graph)
self.setLayout(self.graphLayout)
def wheelEvent(self, event):
hoveredWidget = QApplication.widgetAt(event.globalPos())
print(hoveredWidget.objectName())
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())

A PlotWidget is actually a subclass of QAbstractScrollArea, which is a complex widget that has at least three children widgets: the scroll bars (even when they're hidden) and, most importantly, the viewport, which actually is the "content" of the scroll area.
This means that using widgetAt() you are not getting the plot widget (the scroll area), but its viewport. In fact, in your case you can get the plot widget by checking the parent:
def wheelEvent(self, event):
hoveredWidget = QApplication.widgetAt(event.globalPos())
if hoveredWidget and hoveredWidget.parent():
print(hoveredWidget.parent().objectName())
Be careful when intercepting events from a parent widget, especially for widget as complex as scroll areas: it's not guaranteed that you will receive them, as the children could accept them, preventing further propagation to their parent(s).
If you need more control over them, it's usually better to implement the respective methods in their subclasses or installing an event filter on the instances.
Note that, for the reason above, if you want to filter events on a scroll area you might prefer to install the filter on the viewport:
self.graph.viewport().installEventFilter(self)

Related

How to prevent QComboBox from displaying unnecessary scrollbar

The code below, which is based on an example from zetcode.com, creates a single combo box. There are several issues with the resulting dialog, but the following are especially annoying:
PyQt displays a vertical scrollbar for the combo box, although there is plenty of space to display the entire list of options without a scrollbar.
I've tried to move the combo box to a position near the upper-left corner of the window, but this isn't working.
#!/usr/bin/python
import sys
from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.setFixedWidth(400)
self.setFixedHeight(500)
self.initUI()
def initUI(self):
hbox = QHBoxLayout()
self.lbl = QLabel('Animals', self)
self.lbl.setStyleSheet('font-size:11pt')
combo = QComboBox(self)
combo.addItem('bear')
combo.addItem('cat')
combo.addItem('dog')
combo.addItem('dolphin')
combo.addItem('elephant')
combo.addItem('fish')
combo.addItem('frog')
combo.addItem('horse')
combo.addItem('rabbit')
combo.addItem('rat')
combo.addItem('shark')
combo.addItem('snake')
combo.addItem('tiger')
combo.addItem('whale')
combo.activated[str].connect(self.onActivated)
hbox.addWidget(combo)
hbox.setSpacing(20)
hbox.addWidget(self.lbl)
self.setContentsMargins(20, 20, 20, 20)
self.setLayout(hbox)
combo.move(20, 60)
self.setWindowTitle('QComboBox')
self.show()
def onActivated(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
There are two wrong assumptions in the question.
the list of a QComboBox is a popup widget, it doesn't care (nor it should) about the available space the combobox might have: consider it as much as a context menu, which just pops out over the window, possibly going outside its boundaries if it requires more space (and that's just because those boundaries are meaningless to the menu);
the combo has been added to a layout manager, which takes care about resizing and positioning its (managed) child widgets, and that's why you cannot manually "move" them: the layout already sets the geometries automatically on its own everytime the widget is resized (which also happen when it's shown the first time), so any attempt to use move(), resize() or setGeometry() is completely useless;
When adding a widget to a layout, the default behavior is to make it occupy as much space as possible; since a QComboBox is one of those widgets that have a fixed size, the result is that it's actually centered (vertically and horizontally) in the space the layout is "assigning" to it, and this is clearly visible in your case because you set a fixed size for the container widget that is much bigger than what its contents would need.
There are two ways to align those widgets on top:
add the alignment arguments to addWidget:
hbox.addWidget(combo, alignment=QtCore.Qt.AlignTop)
hbox.addWidget(self.lbl, alignment=QtCore.Qt.AlignTop)
note that this won't give you good results in your case, because the label and the combo box have different heights, so the label might look "higher" than the combo;
use a QVBoxLayout layout as main layout for the widget, add the horizontal layout to it and then add a stretch after that (a stretch on a box layout is a "spacer" that tries to occupy as much space as possible)
# ...
mainLayout = QVBoxLayout()
mainLayout.addLayout(hbox)
mainLayout.addStretch()
self.setLayout(mainLayout)
PS: if you need to add lots of (string only) elements to a QComboBox, use addItems() instead of individually adding each of them.

How to show tooltip while not focusing at pyqt5 python?

I want to show tooltip while not focusing.
I made the code by referring to this PyQt Window Focus
But, it works after click window just one. Works fine, but window always blink at taskbar.
And I think this method is inefficient.
I think it's as if os are not resting while waiting for task to come, but checking every moment for task to come.
This is a simple window window, so it won't use up the cpu much, but I want to code it more efficiently.
Is there any way to improve this?
Or this method right because focusoutEvent excuted only one? ( Cpu resource 0% )
If right, how can I remove blink at taskbar?
I check reference focusPolicy-prop
import sys, os
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class MyApp(QWidget):
def __init__(self):
super().__init__()
self.setFocusPolicy(QtCore.Qt.ClickFocus)
self.initUI()
def initUI(self):
vbox = QVBoxLayout()
vbox.addStretch(2)
btn = QPushButton("Test")
btn.setToolTip("This tooltip")
vbox.addWidget(btn)
vbox.addStretch(1)
self.setLayout(vbox)
self.setGeometry(300, 300, 300, 200)
self.show()
def focusOutEvent(self, event):
self.setFocus(True)
self.activateWindow()
self.raise_()
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MyApp()
sys.exit(app.exec_())
You are having an XY problem: trying to find a solution (usually unorthodox and overly complicated) for a problem that is originated elsewhere.
What you want to do is to show tooltips even if the window is not focused, not to restore the focus of the window; to achieve this you must not reactivate the window when it loses focus (which not only is WRONG, but is both a wrong way and reason for doing so).
You just have to set the WA_AlwaysShowToolTips widget attribute on the top level window (and remove the unnecessary focusOutEvent override, obviously).
class MyApp(QWidget):
def __init__(self):
super().__init__()
self.setFocusPolicy(QtCore.Qt.ClickFocus)
self.initUI()
self.setAttribute(QtCore.Qt.WA_AlwaysShowToolTips, True)
Note that the attribute must be set on a widget that is a top level window, so, unless you're using a QMainWindow or you are absolutely sure that the QWidget will always be a window, it's usually better to do this instead:
self.window().setAttribute(QtCore.Qt.WA_AlwaysShowToolTips, True)
Besides that, the blinking is normal on windows, and has nothing to do with CPU usage:
activateWindow():
[...] On Windows, if you are calling this when the application is not currently the active one then it will not make it the active window. It will change the color of the taskbar entry to indicate that the window has changed in some way. This is because Microsoft does not allow an application to interrupt what the user is currently doing in another application.

Collapsible QToolButton acts weird in QScrollArea ( probably problem with stretch )

My problem is that collapsible QToolButton is acting weird in QScrollArea. It's not my first problem with collapsible QToolButton and at first it was my layout not stretching , so I added stretching(addStretch(1)) and it started working fine. Today, I tried to add QScrollArea to layout and then adding QToolButtons with widgets under it and same looking problem started again. So I thought that is again problem with stretching , so I was trying to search way of stretching QScrollArea and after looking, tried setting setSizePolicy(), sizeHint(), but it wouldn't fix problem. Can somebody help me find problem ?
More detailed explanation of problem : when you are expanding all collapsible QToolButton's first time there is no problem , but when you close them all and start opening again , starting from second QToolButton they start not opening from first few clicks. Also, I don't know if it's problem or not , but at first when you expand those buttons out of UI , they start shaking back and forward a little, basically not opening smoothly.
Here is code:
import random
from PySide2.QtGui import QPixmap, QBrush, QColor, QIcon, QPainterPath, QPolygonF, QPen, QTransform
from PySide2.QtCore import QSize, Qt, Signal, QPointF, QRect, QPoint, QParallelAnimationGroup, QPropertyAnimation, QAbstractAnimation
from PySide2.QtWidgets import QMainWindow, QDialog, QVBoxLayout, QHBoxLayout, QGraphicsView, QGraphicsScene, QFrame, \
QSizePolicy, QGraphicsPixmapItem, QApplication, QRubberBand, QMenu, QMenuBar, QTabWidget, QWidget, QPushButton, \
QSlider, QGraphicsPolygonItem, QToolButton, QScrollArea, QLabel
extraDict = {'buttonSetA': ['test'], 'buttonSetB': ['test'], 'buttonSetC': ['test'], 'buttonSetD': ['test']}
class MainWindow(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
self.create()
def create(self, **kwargs):
main_layout = QVBoxLayout()
tab_widget = QTabWidget()
main_layout.addWidget(tab_widget)
tab_extra = QWidget()
tab_widget.addTab(tab_extra, 'Extra')
tab_main = QWidget()
tab_widget.addTab(tab_main, 'Main')
tab_extra.layout = QVBoxLayout()
tab_extra.setLayout(tab_extra.layout)
scroll = QScrollArea()
content_widget = QWidget()
scroll.setWidget(content_widget)
scroll.setWidgetResizable(True)
#scroll.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
tab_extra.layout.addWidget(scroll)
content_layout = QVBoxLayout(content_widget)
for name in extraDict.keys():
box = CollapsibleBox(name)
content_layout.addWidget(box)
box_layout = QVBoxLayout()
for j in range(8):
label = QLabel("{}".format(j))
color = QColor(*[random.randint(0, 255) for _ in range(3)])
label.setStyleSheet("background-color: {}; color : white;".format(color.name()))
label.setAlignment(Qt.AlignCenter)
box_layout.addWidget(label)
box.setContentLayout(box_layout)
content_layout.addStretch(1)
self.setLayout(main_layout)
class CollapsibleBox(QWidget):
def __init__(self, name):
super(CollapsibleBox, self).__init__()
self.toggle_button = QToolButton(text=name, checkable=True, checked=False)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(Qt.RightArrow)
self.toggle_button.pressed.connect(self.on_pressed)
self.toggle_animation = QParallelAnimationGroup(self)
self.content_area = QScrollArea(maximumHeight=0, minimumHeight=0)
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.content_area.setFrameShape(QFrame.NoFrame)
lay = QVBoxLayout(self)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.toggle_button)
lay.addWidget(self.content_area)
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self.content_area, b"maximumHeight"))
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(Qt.DownArrow if not checked else Qt.RightArrow)
self.toggle_animation.setDirection(QAbstractAnimation.Forward
if not checked
else QAbstractAnimation.Backward
)
self.toggle_animation.start()
def setContentLayout(self, layout):
lay = self.content_area.layout()
del lay
self.content_area.setLayout(layout)
collapsed_height = (self.sizeHint().height() - self.content_area.maximumHeight())
content_height = layout.sizeHint().height()
for i in range(self.toggle_animation.animationCount()):
animation = self.toggle_animation.animationAt(i)
animation.setDuration(500)
animation.setStartValue(collapsed_height)
animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(self.toggle_animation.animationCount() - 1)
content_animation.setDuration(500)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.setGeometry(500, 100, 500, 500)
window.show()
sys.exit(app.exec_())
Edit :
Here is link to a small video with problem I recorded , for more clear picture. (problem start on 0:12)
I noticed problem occurs only when I open all QToolBox and then close it one by one from below and then start to open them again.
The problem comes from the fact that when the collapsible box starts resizing, your mouse button will probably still be pressed, and if the button moves due to the scrolling, it will be moved "outside" the cursor position, resulting in the button release event being received outside the button area.
It's a common convention with buttons that if the user clicks on it but moves the cursor outside the button area and then releases the button, the button is not considered as clicked (or checked).
Also, checkable buttons become checked (see down property) when pressed, but are not toggled until the button is released, and if they are already checked, they become unchecked (as in "not down") when released (not when clicked), but, again, the toggled signal is is emitted if the mouse button is released within their geometry.
If you carefully look at your video, you can see that the unchecked buttons are gray, while when checked they have a light blue shade. When you try to uncheck them back, they still receive the pressed event (so the animation works as expected), but then they still remain pressed (blue-ish), and that's because they received the button release event outside their area. You can see the color difference when you try to click the second button after trying to expand it the second time.
So, when you click them again, they are already down, they receive the "pressed" signal, but since they're already down, they state is actually checked.
One would think that using the toggled signal would suffice, but this would mean to wait for the mouse button release (as explained before) and for similar cases is not that intuitive, since the user might prefer an immediate reaction to the mouse press, without waiting the release; this is another common convention for this kind of collapsible widgets.
The only solution I can think of is to create a fake release event and send it to the button as soon as the pressed signal is received. This will make the button "think" that the mouse has been released, thus applying the correct checked state.
def on_pressed(self):
checked = self.toggle_button.isChecked()
fakeEvent = QtGui.QMouseEvent(
QtCore.QEvent.MouseButtonRelease, self.toggle_button.rect().center(),
QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
QApplication.postEvent(self.toggle_button, fakeEvent)
This class constructor of QMouseEvent (there are 4 of them) is the simplest and you only need to set the local position based on the button rectangle; using the center ensures that the event is always received.
Finally, with postEvent the event is actually sent to the widget through QApplication (it's usually better to avoid sending an event directly to the receiver).
About the "shaking" widgets, that's probably due to the fact that you're using a parallel animation that sets the heights of both the contents and the container; while technically this happens in parallel, I believe that the problem comes from there are certain moments during which the two sizes are not "synchronized", and the layout is receiving (temporarily) unreliable data about their size and hints, probably due to the fact that the widget gets both minimum and maximum size, with the content area being resized afterwards.
After some tests I can tell that there's some slight difference between what could happen between the setMinimumHeight and setMaximumHeight.
def __init__(self, name):
# ...
self.toggle_animation.animationAt(0).valueChanged.connect(self.checkSizePre)
self.toggle_animation.animationAt(1).valueChanged.connect(self.checkSizePost)
def checkSizePre(self, value):
self.pre = self.y()
def checkSizePost(self, value):
QApplication.processEvents()
post = self.y()
if self.pre != post:
print('pre {} post {} diff {}'.format(self.pre, post, abs(self.pre - post)))
This results in a difference that varies between 0 and 6 pixel, which shows that setting those min/max values affects the overall positioning of the widgets. Obviously, those values are usually insignificant for when manually resizing a widget, but since resize events are always slightly delayed, in this case there's no sufficient time for the whole layout system to adjust everything without glitches.
Unfortunately, I can't think of a solution right now, sorry.

How to resize QWidget without intermediate paint

During the study, Qt encountered such a problem. Suppose I have a QWidget on the QMainWindow. How can I make sure that when I resize QMainWindow, QWidget on this QMainWindow do not repaint content until the resizing does not stop.
Yeah, I saw this example How to disable multiple auto-redrawing at resizing widgets in PyQt?
But when I tried this method it's just locked widget content. I just wondered if it was possible to make sure that the contents of the QWidget did not change while we resizing MainWindow. Please tell me, is this possible?
Thanks a lot.
I'm still guessing slightly as to what you want exactly but it sounds as if you essentially want two modes for your paintEvent method -- the normal one that takes care of rendering the widget most of the time and a second, lightweight, mode that can be used whilst the widget is being resized.
If that's the case then you could try something like the following...
#!/usr/bin/python3
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class widget(QWidget):
def __init__(self):
super().__init__()
self.resize_timer = QTimer(self)
self.resize_timer.setSingleShot(True)
self.resize_timer.setInterval(100)
self.resize_timer.timeout.connect(self.delayed_update)
def delayed_update(self):
self.update()
def paintEvent(self, event):
if self.resize_timer.isActive():
print("painting deferred")
# Your `lightweight' rendering goes here and will be used
# while the widget is being resized.
else:
print("painting now")
# Full rendering code goes here.
def resizeEvent(self, event):
super(widget, self).resizeEvent(event)
self.resize_timer.start()
if __name__ == '__main__':
app = QApplication(sys.argv)
f = widget()
f.show()
sys.exit(app.exec_())
Note that it is essentially just a simple modification of the code in the answer you linked to.

QWidget showFullScreen produces multiple resizeEvents

I have a QT application written in python using PySide and I stumbled across a little problem regarding the showFullScreen method of the QGLWidget (although the problem occurs with every other widget too):
The problem is, that the widget doesn't have its 'final' resolution after the program returns from showFullScreen.
The switch seems to be triggered asynchronously between 5 and 10 milliseconds later.
This is a problem for me because I have to do some layout calculations which depend on the widget's size after it is shown.
Below is a little reproducer which subclasses QGLWidget. Using this reproducer you will take notice, that resizeEvent will be called twice after showFullScreen.
I'm looking for a convinient way of knowing which resizeEvent is the 'final' one, or a way of knowing, when the widget really is in fullscreen mode. Is there maybe any signals I could connect to?
Thanks a lot for any help on this.
#!/usr/bin/python
import sys
from PySide.QtGui import QApplication
from PySide.QtCore import QTimer
from PySide.QtOpenGL import QGLWidget
class TestWidget(QGLWidget):
def __init__(self, parent=None):
super(TestWidget, self).__init__(parent)
self._timer = QTimer()
self._timer.setInterval(5)
self._timer.timeout.connect(self.showsize)
self._timer.start()
def resizeEvent(self, event):
print "Resize event:", event.size().width(), event.size().height()
def showsize(self):
w = widget.size().width()
print "Timer: ", w, widget.size().height()
if w == 1680:
self._timer.stop()
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = TestWidget()
widget.showFullScreen()
print "After showFullScreen:", widget.size().width(), widget.size().height()
# this will always be 640 480...1680 1050 is what I'm expecting
app.exec_()
One possibility is to check that if the resize event is spontaneous or not. From limited testing here (using Qt C++ on Linux), the second resize event is spontaneous, while the first one is not.
You could do your calculations only when the event is spontaneous.
Note: I'm not sure how portable this is, it might depend on your window manager/windowing system.

Categories

Resources