I need to ask you guys for advice, as I'm running out of ideas. I'm working onclickable label. I have already done some "clickable label" class and handled mouseover event - which changes the label border and returns to the normal state when the mouse leaves.
Now I want it to have a custom glow effect on the label, but I want it to return to normal state after, let's say 0.5 s from a click.
I want my label with an image to imitate button. time.sleep does not work nice, especially with spamming clicks it freezes application main thread.
Hope I am not reinventing the wheel but as far as I know that's the way to go.
Heres the sample code, any answer is appreciated.
from PySide2.QtWidgets import QLabel, QSizePolicy, QGraphicsDropShadowEffect
from PySide2.QtGui import QPixmap
from PySide2.QtCore import (Signal, QEvent, QObject, QRect)
class ClickableLabel(QLabel):
def __init__(self, pic_path, width, height, border_color, click_function):
super(ClickableLabel, self).__init__()
# Setting the picture path and setting pixmap to label
self.pic_path = pic_path
self.pixmap = QPixmap(self.pic_path)
self.setPixmap(self.pixmap)
# Set the size
self.setFixedSize(width, height)
# Enable tracking and assign function
self.setMouseTracking(True)
self.click_function = click_function
# Set default
if border_color is None:
self.border_color = 'lightblue'
else:
self.border_color = border_color
def mouseMoveEvent(self, event):
# event.pos().x(), event.pos().y()
self.setStyleSheet("border: 1px solid " + str(self.border_color) + ";")
def leaveEvent(self, event):
# event.pos().x(), event.pos().y()
self.setStyleSheet("border: None")
def mousePressEvent(self, event):
self.click_function()
effect = QGraphicsDropShadowEffect(self)
effect.setOffset(0, 0)
effect.setBlurRadius(20)
effect.setColor(self.border_color)
self.setGraphicsEffect(effect)
If you want to run a task after a while you should not use time.sleep() because as it blocks the GUI causing it not to behave correctly, the best option is to use QTimer::singleShot().
On the other hand I see that you are passing a function so that it executes a task when it is clicked, that is not scalable, in Qt the correct thing is to create a signal and the advantage is that you can connect the same signal to several functions without coupling , that is, the one that emits the signal must not know in advance who will receive the signal.
I recommend taking values for defects for the arguments, I have taken the time to give an improvement to your code:
from PySide2 import QtCore, QtGui, QtWidgets
class ClickableLabel(QtWidgets.QLabel):
clicked = QtCore.Signal()
def __init__(self, pic_path="", width=80, height=30, border_color=QtGui.QColor("lightblue"), parent=None):
super(ClickableLabel, self).__init__(parent)
self.setPixmap(QtGui.QPixmap(pic_path))
self.setFixedSize(width, height)
self.setMouseTracking(True)
self.border_color = QtGui.QColor(border_color)
self.effect = QtWidgets.QGraphicsDropShadowEffect(self,
offset=QtCore.QPointF(0, 0),
blurRadius=20,
color=self.border_color)
self.setGraphicsEffect(self.effect)
self.disable_effect()
def mouseMoveEvent(self, event):
self.setStyleSheet("border: 1px solid {};".format(self.border_color.name()))
super(ClickableLabel, self).mouseMoveEvent(event)
def leaveEvent(self, event):
self.setStyleSheet("border: None")
super(ClickableLabel, self).leaveEvent(event)
def mousePressEvent(self, event):
self.clicked.emit()
self.effect.setEnabled(True)
QtCore.QTimer.singleShot(500, self.disable_effect)
super(ClickableLabel, self).mousePressEvent(event)
#QtCore.Slot()
def disable_effect(self):
self.effect.setEnabled(False)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
def on_click():
print("click")
w = ClickableLabel(pic_path="heart.png")
w.clicked.connect(on_click)
w.show()
sys.exit(app.exec_())
Related
I'm currently trying to reproduce the Window 10 Start Menu scroll bar in Qt (python) but I can't figure how to resize my custom QScrollBar to change it's width in runtime.
I tried to resize it using the QScrollBar.resize method (in the enterEvent and leaveEvent) but it scale the widget outside it's "drawing area".
For example, my scroll bar is set to a QScrollArea and when I try to resize it, it doesn't take more space and move the widgets, instead of that it just scale on it's right, where I can't see it.
The only solution I've found for now is to use StyleSheet but I can't animate this to have the smooth resize I'm looking for.
There is some code for you to test and see what's wrong:
from PySide2 import QtWidgets, QtCore
from functools import partial
class MyTE(QtWidgets.QPlainTextEdit):
def __init__(self):
super(MyTE, self).__init__()
self.setVerticalScrollBar(MyScrollBar(self))
self.setPlainText('mmmmmmmmmmmmmmmmmmmmmmmmmmmmm'*50)
class MyScrollBar(QtWidgets.QScrollBar):
def __init__(self, parent=None):
super(MyScrollBar, self).__init__(parent=parent)
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
def enterEvent(self, event):
super(MyScrollBar, self).enterEvent(event)
self.resize(QtCore.QSize(4, self.height()))
def leaveEvent(self, event):
super(MyScrollBar, self).leaveEvent(event)
self.resize(QtCore.QSize(10, self.height()))
wid = MyTE()
wid.show()
To make the width change smoother then you can use a QVariantAnimation:
from PySide2 import QtWidgets, QtCore
class MyTE(QtWidgets.QPlainTextEdit):
def __init__(self):
super(MyTE, self).__init__()
self.setVerticalScrollBar(MyScrollBar(self))
self.setPlainText("mmmmmmmmmmmmmmmmmmmmmmmmmmmmm" * 50)
class MyScrollBar(QtWidgets.QScrollBar):
def __init__(self, parent=None):
super(MyScrollBar, self).__init__(parent=parent)
self._animation = QtCore.QVariantAnimation(
startValue=10, endValue=25, duration=500
)
self._animation.valueChanged.connect(self.change_width)
def enterEvent(self, event):
super(MyScrollBar, self).enterEvent(event)
self._animation.setDirection(QtCore.QAbstractAnimation.Forward)
self._animation.start()
def leaveEvent(self, event):
super(MyScrollBar, self).leaveEvent(event)
self._animation.setDirection(QtCore.QAbstractAnimation.Backward)
self._animation.start()
def change_width(self, width):
self.setStyleSheet("""QScrollBar:vertical{ width: %dpx;}""" % (width,))
if __name__ == "__main__":
app = QtWidgets.QApplication()
wid = MyTE()
wid.show()
app.exec_()
.0I've finally found how to do it without using stylesheet !
Thanks eyllanesc for your answer, it give me a new bases to work on, I've finally understand how Qt handle the resizing (I guess), I use the setFixedSize in a method called everytime the value of my animation change.
To work, this need to have an overrided sizeHint method that return the value of the animation for the width.
Also, this works on Autodesk Maya (unfortunately the solution offered by eyllanesc didn't worked in Maya for unknown reason).
There is my solution:
from PySide2 import QtWidgets, QtCore, QtGui
class MyTE(QtWidgets.QPlainTextEdit):
def __init__(self):
super(MyTE, self).__init__()
self.setVerticalScrollBar(MyScrollBar(self))
self.setPlainText("mmmmmmmmmmmmmmmmmmmmmmmmmmmmm" * 50)
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
def resizeEvent(self, event):
super(MyTE, self).resizeEvent(event)
class MyScrollBar(QtWidgets.QScrollBar):
def __init__(self, parent=None):
super(MyScrollBar, self).__init__(parent=parent)
self._animation = QtCore.QVariantAnimation(
startValue=10.0, endValue=25.0, duration=300
)
self._animation.valueChanged.connect(self.changeWidth)
self._width = 10
def enterEvent(self, event):
super(MyScrollBar, self).enterEvent(event)
self._animation.setDirection(QtCore.QAbstractAnimation.Forward)
self._animation.start()
def leaveEvent(self, event):
super(MyScrollBar, self).leaveEvent(event)
self._animation.setDirection(QtCore.QAbstractAnimation.Backward)
self._animation.start()
def sizeHint(self):
"""This must be overrided and return the width processed by the animation
.. note:: In my final version I've replaced self.height() with 1 because it does not change
anything but self.height() was making my widget grow a bit each time.
"""
return QtCore.QSize(self._width, self.height())
def changeWidth(self, width):
self._width = width # This will allow us to return this value as the sizeHint width
self.setFixedWidth(width) # This will ensure to scale the widget properly.
if __name__ == "__main__":
app = QtWidgets.QApplication()
wid = MyTE()
wid.show()
app.exec_()
Note: startValue and endValue of the QVariantAnimation must be float, animation won't work if they are of type int (Work in Qt 5.6.1 (Maya 2018) but not Qt 5.12.5 (Maya 2020)
PS: If someone is interrested about my final Widget (Window 10 Start menu scroll bar) let ask me in private.
I need some sort of visual feedback of mouse position when it is clicked or clicked and dragged within a widget or a rectangular area. The visual feed back should stay where the mouse is released. I did something like the following, but it is not exactly what I'm tying to do:
import sys
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
class test(QLabel):
def __init__(self):
super(test, self).__init__()
self.setMouseTracking(True)
self.resize(300, 300)
self.handle = QPushButton()
self.handle.setFixedSize(15, 15)
self.handle.setParent(self)
self.handle.setText("+")
self.handle.setStyleSheet("background: none;"
"border: 1px solid;"
"border-radius: 7px;")
def mousePressEvent(self, pos):
if pos.button() == Qt.LeftButton:
self.handle.move(pos.x(), pos.y())
print(str(pos.x()) + ", " + str(pos.y()))
def mouseMoveEvent(self, pos):
if pos.buttons() & Qt.LeftButton:
self.handle.move(pos.x(), pos.y())
print(str(pos.x()) + ", " + str(pos.y()))
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
t = test()
t.show()
sys.exit(app.exec_())
The problems with the above code are:
The button's position is calculated from its top left corner, so I have to do something like
self.handle.move(pos.x() -7, pos.y() -7)
to make it appear in the center, which is very inconsistent.
The visual feedback should stay within the widget or area and not go out of bounds when the mouse does. Again, I could work around that with a few lines of inconsistent code (as I'm not an expert).
I was looking for something that can help me with achieving it and the best best thing I come across was to install pyqtgraph. But I doubt if it will take more resources and adding a new library is going to complicate things for me. Or is this my best bet?
The image below shows something similar.
As indicated in the post, it is enough to take into account the geometry of the dragged element and the window:
import sys
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QApplication, QLabel
class TestLabel(QLabel):
def __init__(self):
super(TestLabel, self).__init__()
self.setMouseTracking(True)
self.resize(300, 300)
self.handle = QLabel(self)
self.handle.setFixedSize(15, 15)
self.handle.setText("+")
self.handle.setStyleSheet("border: 1px solid;" "border-radius: 7px;")
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton and self.rect().contains(event.pos()):
self.handle.move(event.pos() - self.handle.rect().center())
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton and self.rect().contains(event.pos()):
self.handle.move(event.pos() - self.handle.rect().center())
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
t = TestLabel()
t.show()
sys.exit(app.exec_())
Finding the name of the place where I drag the mouse in pyqt
position where mouse is released
def dropEvent(self, event): # Drag and Drop in Camera View
mimeData = QtCore.QMimeData()
format = 'application/x-qabstractitemmodeldatalist'
name_str = codecs.decode(data,'utf-8')
if mimeData.hasText:
destination = self.childAt(event.pos()) # position where mouse is released
destination.objectName() # object name where mouse is released
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_())
My actual application is much more complicated than this, but the example below sums up the majority of my problem. I have multiple QLabels that I've subclassed to make them clickable. The labels display 16x16 images which requires a process of loading the images via Pillow, converting them to ImageQt objects, and then setting the pixmap of the label. In the example, I have 3 clickable QLabels that run the print_something function each time I click on them. My goal is to be able to hold the mouse down, and for each label I hover over, the function gets called. Any pointers would be great.
from PyQt5 import QtCore, QtWidgets, QtGui
from PIL import Image
from PIL.ImageQt import ImageQt
import sys
class ClickableLabel(QtWidgets.QLabel):
def __init__(self):
super().__init__()
clicked = QtCore.pyqtSignal()
def mousePressEvent(self, ev):
if app.mouseButtons() & QtCore.Qt.LeftButton:
self.clicked.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
central_widget = QtWidgets.QWidget()
self.setFixedSize(300, 300)
image = Image.open("16x16image.png")
image_imageqt = ImageQt(image)
hbox = QtWidgets.QHBoxLayout()
hbox.setSpacing(0)
hbox.addStretch()
label01 = ClickableLabel()
label01.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label01.clicked.connect(self.print_something)
hbox.addWidget(label01)
label02 = ClickableLabel()
label02.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label02.clicked.connect(self.print_something)
hbox.addWidget(label02)
label03 = ClickableLabel()
label03.setPixmap(QtGui.QPixmap.fromImage(image_imageqt))
label03.clicked.connect(self.print_something)
hbox.addWidget(label03)
hbox.addStretch()
central_widget.setLayout(hbox)
self.setCentralWidget(central_widget)
def print_something(self):
print("Printing something..")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
The cause of the problem is stated in the docs for QMouseEvent:
Qt automatically grabs the mouse when a mouse button is pressed inside
a widget; the widget will continue to receive mouse events until the
last mouse button is released.
It does not look like there is a simple way around this, so something hackish will be required. One idea is to initiate a fake drag and then use dragEnterEvent instead of enterEvent. Something like this should probably work:
class ClickableLabel(QtWidgets.QLabel):
clicked = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.dragstart = None
def mousePressEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
self.dragstart = event.pos()
self.clicked.emit()
def mouseReleaseEvent(self, event):
self.dragstart = None
def mouseMoveEvent(self, event):
if (self.dragstart is not None and
event.buttons() & QtCore.Qt.LeftButton and
(event.pos() - self.dragstart).manhattanLength() >
QtWidgets.qApp.startDragDistance()):
self.dragstart = None
drag = QtGui.QDrag(self)
drag.setMimeData(QtCore.QMimeData())
drag.exec_(QtCore.Qt.LinkAction)
def dragEnterEvent(self, event):
event.acceptProposedAction()
if event.source() is not self:
self.clicked.emit()
I'm very new to PySide/PyQt environment. I'm trying to make a menu of buttons on top and assign a task to each so that when they are clicked a function draws a painting on the central window. But I also want to make the button change when they are clicked.
I think this might be an straighforward problem to solve if I use QPushButton, but my buttons are images and I'm using the method suggested HERE and use QAbstractButton to create them.
It is mentioned there that
You can add second pixmap and draw it only when the mouse pointer is
hover over button.
And I'm trying to do exactly that. My question is this:
what are possible ways to achieve this? Are the same methods in QPushButtons applicable here? If so, are there any examples of it somewhere?
Here is a snippet of my code:
import sys
from PySide import QtGui, QtCore
BACKGROUND_COLOR = '#808080'
ICON_PATH_ACTIVE = 'icons/activ'
ICON_PATH_PASSIVE = 'icons/pasiv'
class MainWindow(QtGui.QMainWindow):
def __init__(self, app=None):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
dockwidget = QtGui.QWidget()
self.setGeometry(200, 200, 400, 300)
hbox = QtGui.QHBoxLayout()
1_button = PicButton(QtGui.QPixmap("icons/pasiv/1.png"))
2_button = PicButton(QtGui.QPixmap("icons/pasiv/2.png"))
3_button = PicButton(QtGui.QPixmap("icons/pasiv/3.png"))
hbox.addWidget(1_button)
hbox.addWidget(2_button)
hbox.addWidget(3_button)
vbox = QtGui.QVBoxLayout()
vbox.addLayout(hbox)
vbox.setAlignment(hbox, QtCore.Qt.AlignTop)
dockwidget.setLayout(vbox)
self.setCentralWidget(dockwidget)
class PicButton(QtGui.QAbstractButton):
def __init__(self, pixmap, parent=None):
super(PicButton, self).__init__(parent)
self.pixmap = pixmap
self.setFixedSize(100, 100)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.drawPixmap(event.rect(), self.pixmap)
def main():
app = QtGui.QApplication(sys.argv)
central = MainWindow()
central.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Thank you.
Use a regular QPushButton with an icon.
iplay = QtGui.QIcon("path/play_icon.png")
ipause = QtGui.QIcon("path/pause_icon.png")
btn = QtGui.QPushButton(ipause, "", None)
def toggle_play():
if btn.icon() == ipause:
btn.setIcon(iplay)
# Do Pause Action
else:
btn.setIcon(ipause)
# Do Play Action
btn.clicked.connect(toggle_play)
btn.show()
If you want hover functionality then you will have to subclass the QPushButton
class MyButton(QtGui.QPushButton):
custom_click_signal = QtCore.Signal()
def enterEvent(self, event):
super().enterEvent(event)
# Change icon hove image here
def leaveEvent(self, event):
super().leaveEvent(event)
# Change icon back to original image here.
def mousePressEvent(self, event):
super().mousePressEvent(event)
self.custom_click_signal.emit()
# connect to signal btn.custom_click_signal.connect(method)
Icons are probably the easiest way instead of manually managing the paint event. There are also mousePressEvent and mouseReleaseEvents if you want the icon to change for someone holding the button down.