How to take action on clicking QPushButton [duplicate] - python

I'm having a problem updating the icon of a button set with QToolButton. The idea is to use the button for a movie player. To play, one presses the button and the icon changes to pause. When pressed again, play is paused and the icon reverts to play. I have some working code, but the problem is that the icon is not updating consistently. If I keep the Qt window in focus, it takes one or two button presses to change the icon to the intended image, by which time the actual image is not the intended image (swapped play/pause).
Here is some minimal example code:
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QStyle, QToolButton
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent=parent)
self.play_button = QToolButton(clicked=self.update_button)
self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaStop))
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.addWidget(self.play_button)
self.button_pressed = False
def update_button(self):
if self.button_pressed:
self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
self.button_pressed = False
print("Button should be set to PLAY. Press is", self.button_pressed)
else:
self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPause))
self.button_pressed = True
print("Button should be set to PAUSE. Press is", self.button_pressed)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
In the above I start with a stop icon just to be sure to observe a change (any click should always change the icon). Keeping the window focused, I get the following output:
1st click: "Button should be set to PAUSE. Press is True" (no change in icon)
2nd click: "Button should be set to PLAY. Press is False" (icon changes to pause)
3rd click: "Button should be set to PAUSE. Press is True" (icon changes to play)
(and so on, continues swapping as intended)
I've also noticed that if after each click, I click outside the Qt window, or resize the Qt window, the button icon updates to the correct one. What am I doing wrong? How do I force the icon to update?
This behaviour happens mostly with QToolButton, but QPushButton also give issues (works when focused, but misbehaves/loses track of correct status if I resize the Qt window). Using PyQt 5.12.3 and qt 5.12.5 on macOS.

Seems like this issue is a bug in the Qt implementation for macOS. I tested and it happens with both PyQt5 and PySide2, so it must come from Qt. Forcing a redraw with a call to .repaint() after .setIcon() seems to make the problem go away:
self.play_button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
self.play_button.repaint()

Related

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.

PyQt setText not rendering text properly in MacOs

I am creating an application using PyQt5 (pythyon 3.7 , MacOs X)
When I modify the text in a textbox using the instruction
self.line_main.setText(final_text)
from a function (which is connected to a push button)
The new text does not render properly in the textbox (see screenshot) and both the old and new text are overlapping in a strange way.
An over-simplified code to illustrate the problem is this one:
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QWidget, QLabel, QLineEdit
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtCore import QSize
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setMinimumSize(QSize(200, 200))
self.setWindowTitle("PyQt test")
self.line_main = QLineEdit(self)
self.line_main.move(20,20)
bt_upperCase = QPushButton('Upper Case', self)
bt_upperCase.move(20, 60)
bt_upperCase.clicked.connect(self.click_upCase)
def click_upCase(self):
final_text=self.line_main.text().upper()
self.line_main.setText(final_text)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mainWin = MainWindow()
mainWin.show()
sys.exit( app.exec_() )
screenshot of mini-app, with 're' as input as 'RE' as output
The same thing may happen for labels, although at times, only the old text is visible, and I need to select the text with the mouse or resize the main window to 'refresh' the textbox and see the new value.
The problem does not happen if I run the code in a PC, only in MacOs (tested in High Sierra and Mojave with identical results)
Some weird behaviours are:
If position the textbox at the (0,0) position in the main window, which is not very practical, then the setText functions correctly. In some other locations of the box, too, but only if it is very close to the top-left corner, and no other element is near.
If I modify the textbox value at the initialization of the main window, then it renders correctly in all cases, The problem appears when you try to do it from a function (after clicking a button, for instance), like in the code attached.
The problem does not happen if I run the code in a PC, only in MacOs (tested in High Sierra and Mojave with identical results)
If position the textbox at the (0,0) position in the main window, (which is not very practical), then the setText functions correctly. In some other locations of the box, too, but only if it is very close to the top-left corner, and no other element is near.
Does anyone have a guess why this may happen, and how could it be solved?
UPDATE.
The problem dissapears if i run the code with python 3.6 in a conda installation (PyQt version 5.9.2). Still very intrigued to know what caused the problem.

Qt application blinking at startup because of drawing text in a custom paint event

I discovered a very strange behavior which causes blinking (white rectangle is shown as the main window for about half a second) at application start up in some cases. After a long time of test and trial I narrowed down the problem to the situation when a text is first drawn in paint event of my custom widget. If a text is drawn also in another widget such as QLabel, the problem is gone. But my application main window only has tool buttons with icons and a custom widget which draws text, no other widgets. The white blinking is very ugly and I would like to get rid of it, ideally with some proper solution and without nasty hacks of introducing some artificial text drawing widgets. Moreover I am not very comfortable because I really do not know what is actually going on. Why the custom widget causes blinking and QLabel not? To prove the behavior try the following code (the same problem is in C++/Qt so it is not caused by Python wrapper). Then try to uncomment the marked line and comment the next one to see that the blinking is gone.
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget, QApplication, QVBoxLayout, QLabel
class CustomWidget(QWidget):
def paintEvent(self, event):
p = QPainter(self)
p.drawText(20, 20, "XYZ")
app = QApplication([])
container = QWidget()
layout = QVBoxLayout(container)
# label = QLabel("ABC") # uncomment this to prevent blinking
label = QLabel() # comment this out to prevent blinking
layout.addWidget(label)
layout.addWidget(CustomWidget())
container.resize(600, 600)
container.show()
app.exec()
Any ideas what is going on there? I am using Qt 5.9.2.

Why do stylesheets influence QTableWidget behaviour?

I use python2.7 and PyQt4. I created a simple app with a button-box and a table-widget. If I edit a table cell and press the Ok button, the cell editor always disappears. But after I add the app.setStyleSheet(s) line, the cell editor does not disappear after the OK button is pressed. What is going on?
import sys
from PyQt4 import QtGui
class Widget(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
l = QtGui.QVBoxLayout(self)
table = QtGui.QTableWidget()
table.setColumnCount(3)
table.setRowCount(5)
l.addWidget(table)
l.addWidget(QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel))
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
s = "QWidget{background:red;}"
# app.setStyleSheet(s)
app.setStyleSheet(s)
mw = QtGui.QMainWindow()
w = Widget()
mw.setCentralWidget(w)
mw.show()
sys.exit(app.exec_())
This seems to be a bug in Qt4, but I can't find a specific report for it. The same code works as expected when using PyQt5, so it somehow got fixed at some point.
The bug is actually in QDialogButtonBox rather than QTableWidget. If you add a QLineEdit to the example and set focus on it, you will see that clicking on the Ok button does not switch focus. Or to be more precise, clicking on the first button does not switch focus. All the other buttons in the button-box work normally.
I thought that this might have something to do with the default and/or autoDefault properties, because they usually change the way buttons are styled (e.g. a highlighted border). But setting the properties doesn't have any effect - the bug really does seem to affect just the first button.

Categories

Resources