I have a QPushButton that has a QIcon set. I would like to grayscale the icon's colors until it is hovered over or selected, without writing 2 separate image files for each icon. If use QPushButton.setDisabled(True) the icon's colors do in fact turn to grayscale, so I would like this same behavior being controlled through a enterEvent. Is this at all possible?
Yes, you can do exactly what you described. Enable the button in the enterEvent and disable it in the leaveEvent if it's not checked.
import sys
from PySide2.QtWidgets import *
from PySide2.QtCore import *
from PySide2.QtGui import *
class Button(QPushButton):
def __init__(self):
super().__init__()
self.setCheckable(True)
self.setDisabled(True)
self.setIcon(QIcon('icon.png'))
self.setIconSize(QSize(100, 100))
def enterEvent(self, event):
self.setEnabled(True)
def leaveEvent(self, event):
if not self.isChecked():
self.setDisabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QWidget()
grid = QGridLayout(w)
grid.addWidget(Button(), 0, 0, Qt.AlignCenter)
w.show()
sys.exit(app.exec_())
I used a green check mark for the icon image. Result:
You also don't need to subclass QPushButton, this will work too.
btn = QPushButton()
btn.setCheckable(True)
btn.setIcon(QIcon('icon.png'))
btn.setIconSize(QSize(100, 100))
btn.setDisabled(True)
btn.enterEvent = lambda _: btn.setEnabled(True)
btn.leaveEvent = lambda _: btn.setEnabled(btn.isChecked())
Related
When I'm trying to make my app, I stumbled upon this unexpected behavior where when I re-display a new QPixmap in a QLabel. I tried to simplify the code and ended up with the code below. I also attached the video of the behavior.
I provided here a replicable example (It just needs some .jpg file in the same directory):
import sys
import os
import random
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QSizePolicy
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
class AppDemo(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(200, 200, 400, 400)
current_working_dir = os.path.abspath('')
dir_files = os.listdir(current_working_dir)
# Saving .jpg from the dir
self.picture = []
for file in dir_files:
if file.endswith(".jpg"):
self.picture.append(file)
self.label = QLabel()
self.label.setStyleSheet("border: 1px solid black;") # <- for the debugging
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding)
self.label.setPixmap(self.random_picture_selector())
button = QPushButton("Reload Picture")
button.clicked.connect(self.reload_picture)
layout = QVBoxLayout(self)
layout.addWidget(button)
layout.addWidget(self.label)
def reload_picture(self):
self.label.setPixmap(self.random_picture_selector())
def random_picture_selector(self):
rnd_picture = random.choice(self.picture)
pixmap = QPixmap(rnd_picture)
pixmap = pixmap.scaledToWidth(self.label.width(), Qt.SmoothTransformation)
# pixmap = pixmap.scaled(self.label.width(), self.label.height(), Qt.KeepAspectRatio) # <- even this is not working
return pixmap
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = AppDemo()
demo.show()
sys.exit(app.exec_())
Additional Infos:
When simplifying the code I realized that the problem disappears when I removed these following lines. (although I'm not very sure that these part of the code really causes the problem)
pixmap = pixmap.scaledToWidth(self.label.width(), Qt.SmoothTransformation)
# pixmap = pixmap.scaled(self.label.width(), self.label.height(), Qt.KeepAspectRatio) # <- even this is not working
I really have no idea what causes the problem even after looking for the Docs of QPixmap and QLabel.
The problem is caused by the stylesheet border. If you just print the pixmap and label size after setting the pixmap, you'll see that the label width is increased by 2 pixels, which is the sum of the left and right border.
You either remove the border, or you use the contentsRect():
width = self.label.contentsRect().width()
pixmap = pixmap.scaledToWidth(width, Qt.SmoothTransformation)
Read more about the Box Model in the Qt style sheet documentation.
I'm trying to produce an onion skin effect using a QLabel in PyQt. In the simplified example below, three images are loaded in and drawn to the label using QPainter.
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QImage, QPixmap, QPainter
import sys
from pathlib import Path
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# -------------------------------------------------------------
# Define the display.
self.display = QLabel()
# -------------------------------------------------------------
# Import the frames
frame_1 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{1}.png')))
frame_2 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{2}.png')))
frame_3 = QImage(str(Path(f'fixtures/test_onion_skin/frame_{3}.png')))
# -------------------------------------------------------------
# Populate the display
frame_1_scaled = frame_1.scaled(self.size(), Qt.KeepAspectRatio)
frame_2_scaled = frame_2.scaled(self.size(), Qt.KeepAspectRatio)
frame_3_scaled = frame_3.scaled(self.size(), Qt.KeepAspectRatio)
base_pixmap = QPixmap(frame_1_scaled.size())
painter = QPainter(base_pixmap)
painter.drawImage(QPoint(), frame_3_scaled)
painter.setOpacity(0.5)
painter.drawImage(QPoint(), frame_2_scaled)
painter.setOpacity(0.3)
painter.drawImage(QPoint(), frame_1_scaled)
painter.end()
self.display.setPixmap(base_pixmap)
# -------------------------------------------------------------
# Set the layout.
layout = QGridLayout()
layout.addWidget(self.display, 0, 0)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Ideally, the last image would show as fully opaque, with earlier images having an increasingly higher transparency. Instead, I'm getting an output where all three images blend together equally. This seems like a simple problem to fix, but my 'Google Fu' hasn't yielded much this time around.
Edit
Here are the image files. They seem have automatically been converted to .jpg unfortunately. If there's a better way to include them please let me know.
frame_1
frame_2
frame_3
Edit 2
After some experimentation I've decided to compromise and allow some 'blending' of the base image. I'm working with raw images from a camera device, so the background of each image is always going to be non-transparent.
In case anyone is interested here is the code:
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QLabel, QSizePolicy
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QImage, QPixmap, QPainter
import sys
from pathlib import Path
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# -------------------------------------------------------------
# Define values used for image painting.
self.first_opacity = 1 # Set first image to fully opaque so it does not blend into background.
self.falloff_value = 0.15 # The opacity of the second image.
self.falloff_rate = 0.5 # A factor used to decrement subsequent image transparencies.
# -------------------------------------------------------------
# Define the display.
self.display = QLabel()
self.display.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
self.display.setMinimumSize(1, 1)
# -------------------------------------------------------------
# Import frames.
self.images = []
for i in range(1, 4, 1):
self.images.append(QImage(str(Path(f'fixtures/test_onion_skin/frame_{i}.png'))))
# -------------------------------------------------------------
# Set the display.
self.update_display()
# -------------------------------------------------------------
# Set the layout.
layout = QGridLayout()
layout.addWidget(self.display, 0, 0)
self.setLayout(layout)
def update_display(self):
# -------------------------------------------------------------
# Define the base pixmap on which to merge images.
base_pixmap = QPixmap(self.display.size())
base_pixmap.fill(Qt.transparent)
# -------------------------------------------------------------
# Preform paint cycle for images.
painter = QPainter(base_pixmap)
for (image, opacity) in zip(reversed(self.images), reversed(self.get_opacities(len(self.images)))):
painter.setOpacity(opacity)
painter.drawImage(QPoint(), image.scaled(base_pixmap.size(), Qt.KeepAspectRatio))
painter.end()
# -------------------------------------------------------------
self.display.setPixmap(base_pixmap)
def get_opacities(self, num_images):
# -------------------------------------------------------------
# Define a list to store image opacity values.
opacities = [self.first_opacity]
value = self.falloff_value
# -------------------------------------------------------------
# Calculate additional opacity values if more than one image is desired.
if num_images > 1:
num_decrements = num_images - 1
for i in range(1, num_decrements + 1, 1):
opacities.insert(0, value)
value *= self.falloff_rate
# -------------------------------------------------------------
return opacities
def resizeEvent(self, event):
self.update_display()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Since the OP does not provide the images then the problem can be caused by:
The order of how opacity is set.
The background color of the image is not transparent.
For my demo I will use this gif and since the background is not transparent (which would be ideal) then I will apply a mask when I paint each image.
import os
from pathlib import Path
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtGui import QColor, QImageReader, QPainter, QPixmap, QRegion
from PyQt5.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
CURRENT_DIRECTORY = Path(__file__).resolve().parent
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.display = QLabel(alignment=Qt.AlignCenter)
lay = QVBoxLayout(self)
lay.addWidget(self.display)
background_color = QColor("white")
filename = os.fspath(CURRENT_DIRECTORY / "Animhorse.gif")
image_reader = QImageReader(filename)
pixmap = QPixmap(image_reader.size())
pixmap.fill(background_color)
images = []
while image_reader.canRead():
images.append(image_reader.read())
painter = QPainter(pixmap)
for image, opacity in zip(images[3:6], (0.3, 0.7, 1.0)):
painter.setOpacity(opacity)
p = QPixmap.fromImage(image)
mask = p.createMaskFromColor(background_color, Qt.MaskInColor)
painter.setClipRegion(QRegion(mask))
painter.drawImage(QPoint(), image)
painter.end()
self.display.setPixmap(pixmap)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
I want to make the label item of a plot clickable, that is, I can click the bottom or left label of a plot to call a menu.
My attempt:
The most obvious way is to rewrite the LabelItem class and overwrite the mousePressEvent() function, however, I did not see any methods in docs to add a LabelItem to a PlotItem at correct position. Code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import pyqtgraph as pg
import sys
class ClickableLabelItem(pg.LabelItem):
def __init__(self, text='No Data'):
super(ClickableLabelItem, self).__init__()
self.setText(text, justify='left')
def mousePressEvent(self, ev):
print('clicked')
class AbstractPlot(pg.GraphicsLayoutWidget):
def __init__(self):
super(AbstractPlot, self).__init__()
self.pl = pg.PlotItem()
self.addItem(self.pl)
left_label = ClickableLabelItem()
self.pl.addItem(left_label)
# "Clicked" is printed but the label
# appears at wrong position
if __name__ == "__main__":
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication([])
win = QMainWindow()
plot = AbstractPlot()
win.setCentralWidget(plot)
win.show()
if (sys.flags.interactive != 1) or not hasattr(Qt.QtCore, 'PYQT_VERSION'):
QApplication.instance().exec_()
Label appears at wrong position
If you want to detect in the label associated with the axis then you can override the mousePressEvent method of GraphicsLayoutWidget and compare the item associated with the point pressed by the mouse.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
class AbstractPlot(pg.GraphicsLayoutWidget):
def __init__(self):
super(AbstractPlot, self).__init__()
self.p1 = self.addPlot(title="Plot 1")
self.p1.setLabel("left", "Value", units="V")
self.p1.setLabel("bottom", "Time", units="s")
def mousePressEvent(self, event):
super().mousePressEvent(event)
item = self.itemAt(event.pos())
if item is self.p1.getAxis("left").label:
print("clicked left")
elif item is self.p1.getAxis("bottom").label:
print("clicked bottom")
if __name__ == "__main__":
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
app = QApplication([])
win = QMainWindow()
plot = AbstractPlot()
win.setCentralWidget(plot)
win.show()
if (sys.flags.interactive != 1) or not hasattr(Qt.QtCore, "PYQT_VERSION"):
QApplication.instance().exec_()
I have this code as follows:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
if __name__ == "__main__":
app = QApplication(sys.argv)
w = QWidget()
eff = QGraphicsOpacityEffect(w)
anim = QPropertyAnimation(eff, b"opacity")
btn = QPushButton("Button", w)
btn.clicked.connect(w.close)
eff.setOpacity(0)
w.setAutoFillBackground(True)
w.setGraphicsEffect(eff)
w.show()
anim.setDuration(10000)
anim.setStartValue(0)
anim.setEndValue(1)
anim.start()
sys.exit(app.exec_())
It works fine (fade in effect). However, when I resize the window, the animation restarts, causing the window to blink/flicker during resize. If I remove the call to animate after the call to show, no more blink (but no more fade in).
Moreover, after some time, resizing to a much bigger size causes a white rectangle over a black background to be displayed.
Is there something I am missing?
UPDATE
This is the behavior I am looking for (background remains white, i.e. opacity=1, after the animation run once). Works with the following code but does not look like best practice to do it like this.
import sys, time
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.w = QWidget()
self.w.setMinimumSize(400,400)
self.w.resizeEvent = self.selfOnResize # added this
def animate(self):
self.eff = QGraphicsOpacityEffect(self.w)
self.anim = QPropertyAnimation(self.eff, b"opacity")
self.btn = QPushButton("Button", self.w)
self.btn.clicked.connect(self.w.close)
self.eff.setOpacity(0)
self.w.setAutoFillBackground(True)
self.w.setGraphicsEffect(self.eff)
self.w.show()
self.anim.setDuration(100000)
self.anim.setStartValue(0)
self.anim.setEndValue(1)
self.anim.start()
# this fixes the issue but looks like a dirty hack
def selfOnResize(self, event):
self.eff.setOpacity(1)
# print(event)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWidget()
w.animate()
sys.exit(app.exec_())
I have a QMainWindow containing a child QWidget containing itself a QLabel.
When the window is maximized (e.g. by clicking the maximize icon on the window), the QLabel.resizeEvent() handler is called multiple times (supposedly to follow the progressive enlargement of the window until it takes the full desktop space).
The code in the event handler calls setPixmap() to scale the label pixmap. This is a relatively long operation which slows the process. Code for the label:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QLabel, QFrame, QGridLayout
from PyQt5.QtGui import QImageReader, QPixmap
class DisplayArea(QLabel):
def __init__(self):
super().__init__()
self.pix_map = None
self.init_ui()
def init_ui(self):
self.setMinimumSize(1, 1)
self.setStyleSheet("border:1px solid black;")
def set_image(self, image):
self.pix_map = QPixmap.fromImage(image)
self.scale_image(self.size())
def scale_image(self, size):
if self.pix_map is None:
return
scaled = self.pix_map.scaled(size, Qt.KeepAspectRatio)
self.setPixmap(scaled)
def resizeEvent(self, e):
self.scale_image(e.size())
super().resizeEvent(e)
Is there a possibility to process the event only once, when the window has reached its final size?
The problem is that the resizeEvent is called many times in the time that the window is maximized, and that same number of times is what you call scale_image. One possible possible is not to update unless a period of time passes. In the following example only resizes for times greater than 100 ms (the time you must calibrate):
from PyQt5 import QtCore, QtGui, QtWidgets
class DisplayArea(QtWidgets.QLabel):
def __init__(self):
super().__init__()
self.pix_map = QtGui.QPixmap()
self._flag = False
self.init_ui()
def init_ui(self):
self.setMinimumSize(1, 1)
self.setStyleSheet("border:1px solid black;")
def set_image(self, image):
self.pix_map = QtGui.QPixmap.fromImage(image)
self.scale_image()
def scale_image(self):
if self.pix_map.isNull():
return
scaled = self.pix_map.scaled(self.size(), QtCore.Qt.KeepAspectRatio)
self.setPixmap(scaled)
def resizeEvent(self, e):
if not self._flag:
self._flag = True
self.scale_image()
QtCore.QTimer.singleShot(100, lambda: setattr(self, "_flag", False))
super().resizeEvent(e)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QMainWindow()
da = DisplayArea()
da.set_image(QtGui.QImage("logo.png"))
w.setCentralWidget(da)
w.show()
sys.exit(app.exec_())