Zoom in and out in widget - python

How can I make a zoom effect with key input on a widget? The widget is inside a scroll area and there are some drawings made with QPainter who change with user input. The zoom would affect the length of the scrolling bar, the closer you are, the smaller the bar becomes. The zoom at minimum level should make the scroll bar as big as the widget area, so all the content in the widget could be visualized.
MRE:
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen, QColor
import sys
class Diedrico(QWidget):
def __init__(self, parent):
super().__init__(parent)
def paintEvent(self, event):
qp = QPainter(self)
qp.setPen(QPen(QColor(Qt.black), 5))
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(520, 520)
self.widget_central = QtWidgets.QWidget(self)
scrol = QtWidgets.QScrollArea(self.widget_central)
scrol.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scrol.setGeometry(QtCore.QRect(10, 10, 500, 500))
scrol.setWidgetResizable(False)
contenido = QtWidgets.QWidget()
contenido.setGeometry(QtCore.QRect(0, 0, 2000, 2000))
scrol.setWidget(contenido)
self.Diedrico = Diedrico(contenido)
self.Diedrico.setGeometry(QtCore.QRect(0, 0, 2000, 2000))
self.setCentralWidget(self.widget_central)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())

Why do you want to reinvent the wheel? Instead of wanting to implement the logic of the scaling feature, use the classes that already do it. In this case, a good option is to use QGraphicsView with QGraphicsScene:
Note: The shortcut standard Zoom In and Zoom Out are associated with Ctrl + + and Ctrl + -, respectively.
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
factor = 1.5
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self._scene = QtWidgets.QGraphicsScene(self)
self._view = QtWidgets.QGraphicsView(self._scene)
self._diedrico = Diedrico()
self._diedrico.setFixedSize(2000, 2000)
self._scene.addWidget(self._diedrico)
self.setCentralWidget(self._view)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn),
self._view,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_in,
)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.ZoomOut),
self._view,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_out,
)
#QtCore.pyqtSlot()
def zoom_in(self):
scale_tr = QtGui.QTransform()
scale_tr.scale(UiVentana.factor, UiVentana.factor)
tr = self._view.transform() * scale_tr
self._view.setTransform(tr)
#QtCore.pyqtSlot()
def zoom_out(self):
scale_tr = QtGui.QTransform()
scale_tr.scale(UiVentana.factor, UiVentana.factor)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self._view.transform() * scale_inverted
self._view.setTransform(tr)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
Update:
If you want to use + and - for ZoomIn and ZoomOut, respectively, then just change the shortcuts to:
QtWidgets.QShortcut(
QtGui.QKeySequence(QtCore.Qt.Key_Plus), # <---
self._view,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_in,
)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtCore.Qt.Key_Minus), # <---
self._view,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_out,
)

Related

Pyqt5 - Zoom on a QLabel

I'm trying to apply a zoom example that I've found on this site but it uses a QGraphicsScene and a QGraphicsView while I should use a simple QLabel. This is the code but it does not work. Can I zoom on a Qlabel or is it impossible?
The zoom should work with ctrl++ / ctrl+- shortcut.
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QLabel
class GuiZoom(QtWidgets.QMainWindow):
factor = 1.5
def __init__(self, parent=None):
super(GuiZoom, self).__init__(parent)
self.setFixedSize(800, 600)
self.lblZoom = QLabel("Facciamo lo zoom")
self.lblZoom.setStyleSheet("border:10px solid green")
self.setCentralWidget(self.lblZoom)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn),
self.lblZoom,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_in,
)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.ZoomOut),
self.lblZoom,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_out,
)
def zoom_in(self):
scale_tr = QtGui.QTransform()
scale_tr.scale(GuiZoom.factor, GuiZoom.factor)
tr = self.lblZoom.transform() * scale_tr
self.lblZoom.setTransform(tr)
def zoom_out(self):
scale_tr = QtGui.QTransform()
scale_tr.scale(GuiZoom.factor, GuiZoom.factor)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self.lblZoom.transform() * scale_inverted
self.lblZoom.setTransform(tr)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
ui = GuiZoom()
ui.show()
sys.exit(app.exec_())
Moreover can you explain me what the last lines of zoom_out functions do? Is this a way to invert the zoom?
One possible solution is to use a QGraphicsEffect to scale the QLabel's painting.
Note: The size of the widget is not changed but the painting is scaled.
from PyQt5 import QtCore, QtGui, QtWidgets
class ZoomEffect(QtWidgets.QGraphicsEffect):
factor = 1.5
_scale = 1.0
#property
def scale(self):
return self._scale
#scale.setter
def scale(self, scale):
self._scale = scale
self.update()
def draw(self, painter):
painter.setRenderHints(
QtGui.QPainter.Antialiasing
| QtGui.QPainter.SmoothPixmapTransform
| QtGui.QPainter.HighQualityAntialiasing
)
center = self.sourceBoundingRect(QtCore.Qt.DeviceCoordinates).center()
pixmap, offset = self.sourcePixmap(QtCore.Qt.DeviceCoordinates)
painter.setWorldTransform(QtGui.QTransform())
painter.translate(center)
painter.scale(self.scale, self.scale)
painter.translate(-center)
painter.drawPixmap(offset, pixmap)
def zoom_in(self):
self.scale *= self.factor
def zoom_out(self):
self.scale /= self.factor
class GuiZoom(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(GuiZoom, self).__init__(parent)
self.setFixedSize(800, 600)
self.lblZoom = QtWidgets.QLabel("Facciamo lo zoom")
self.lblZoom.setStyleSheet("border:10px solid green")
self.zoom_effect = ZoomEffect()
self.lblZoom.setGraphicsEffect(self.zoom_effect)
self.setCentralWidget(self.lblZoom)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn),
self.lblZoom,
context=QtCore.Qt.WindowShortcut,
activated=self.zoom_effect.zoom_in,
)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.ZoomOut),
self.lblZoom,
context=QtCore.Qt.WindowShortcut,
activated=self.zoom_effect.zoom_out,
)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
ui = GuiZoom()
ui.show()
sys.exit(app.exec_())

Pyqt5 draw a line between two widgets

I am trying to use QPainter to draw a line between two widgets. If I use a simple function inside the first class it works. But, I want to create a separate class of a QPainter event, that I can call at the first class whenever I want. But, it is not working as expected. Can you help me to figure out why the QPainter class is not adding a line.
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.okButton = QPushButton("OK")
self.cancelButton = QPushButton("Cancel")
l1 = self.okButton.pos()
l2 = self.cancelButton.pos()
# This is to call the class to draw a line between those two widgets
a = QPaint(l1.x(), l1.y(), l2.x(), l2.y(),parent=self)
vbox = QVBoxLayout()
vbox.addWidget(self.okButton)
vbox.addWidget(self.cancelButton)
self.setLayout(vbox)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle('Buttons')
self.show()
class QPaint(QPainter):
def __init__(self, x1, y1, x2, y2, parent=None):
super().__init__()
def paintEvent(self, event):
self.setPen(Qt.red)
self.drawLine(x1,y1,x2,y2)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Widgets can only be painted in the widget's paintEvent method, so if you don't want to paint it in the same class then you can use multiple inheritance. On the other hand, the initial positions you use to paint will be the positions before showing that they are 0 making no line is painted but a point so it is better to track the positions using an event filter.
import sys
from PyQt5.QtCore import QEvent
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
class Drawer:
def paintEvent(self, event):
painter = QPainter(self)
painter.drawLine(self.p1, self.p2)
class Example(QWidget, Drawer):
def __init__(self, parent=None):
super().__init__(parent)
self.initUI()
def initUI(self):
self.okButton = QPushButton("OK")
self.cancelButton = QPushButton("Cancel")
vbox = QVBoxLayout(self)
vbox.addWidget(self.okButton)
vbox.addWidget(self.cancelButton)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle("Buttons")
self.p1, self.p2 = self.okButton.pos(), self.cancelButton.pos()
self.okButton.installEventFilter(self)
self.cancelButton.installEventFilter(self)
def eventFilter(self, o, e):
if e.type() == QEvent.Move:
if o is self.okButton:
self.p1 = self.okButton.pos()
elif o is self.cancelButton:
self.p2 = self.cancelButton.pos()
self.update()
return super().eventFilter(o, e)
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())

Embed QGraphics in widget

I have a main widget inside a window who contains a lot of widgets. How can I insert a QGraphics view and a QGraphicsScene in that widget? I have not found a direct insertion method, so I am trying using a wrapper, in this case a box layout but it is not a good solution. The QGraphicsScene stands out from the layout limits.
Code:
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(1500, 1015)
widget_central = QtWidgets.QWidget(self)
wrapper = QtWidgets.QHBoxLayout(widget_central)
scene = QtWidgets.QGraphicsScene(wrapper)
vista = QtWidgets.QGraphicsView(scene)
wrapper.addWidget(vista)
self.diedrico = Diedrico() # This is a class who draw things, not relevant
self.diedrico.setFixedSize(2000, 2000)
scene.addWidget(self.diedrico)
self.setCentralWidget(widget_central)
I would like to get this result:
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen, QColor
import sys
class Diedrico(QWidget):
def __init__(self, parent):
super().__init__(parent)
def paintEvent(self, event):
qp = QPainter(self)
qp.setPen(QPen(QColor(Qt.black), 5))
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(520, 520)
self.widget_central = QtWidgets.QWidget(self)
scrol = QtWidgets.QScrollArea(self.widget_central)
scrol.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scrol.setGeometry(QtCore.QRect(30, 30, 500, 500))
scrol.setWidgetResizable(False)
contenido = QtWidgets.QWidget()
contenido.setGeometry(QtCore.QRect(0, 0, 2000, 2000))
scrol.setWidget(contenido)
self.Diedrico = Diedrico(contenido)
self.Diedrico.setGeometry(QtCore.QRect(0, 0, 2000, 2000))
self.setCentralWidget(self.widget_central)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
But using QGraphics instead of a scroll area
The QGraphicsProxyWidget that is created using the widget takes into account the minimum size of the widget to set the boundingRect, and the QGraphicsScene uses the boundingRect to set the initial scene rect.
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
def minimumSizeHint(self):
return QtCore.QSize(2000, 2000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(520, 520)
widget_central = QtWidgets.QWidget()
self.setCentralWidget(widget_central)
lay = QtWidgets.QVBoxLayout(widget_central)
scene = QtWidgets.QGraphicsScene(self)
view = QtWidgets.QGraphicsView(scene)
diedrico = Diedrico()
scene.addWidget(diedrico)
lay.addWidget(view)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())

How to Animate an Image/Icon Widget

I have a custom 'loading' image which I need to be able to start/stop a rotation animation while some action is occurring. I'm looking for code that will show me how to do this but I can't find anything specific to rotation in PyQt. I already have the following code which causes the image to rotate at the click of a button:
import os
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class myApplication(QWidget):
def __init__(self, parent=None):
super(myApplication, self).__init__(parent)
self.imgPath = os.path.join('path/to/image','image.svg')
pixmap = QPixmap(self.imgPath)
diag = (pixmap.width()**2 + pixmap.height()**2)**0.5
self.label = QLabel()
self.label.setMinimumSize(diag, diag)
self.label.setAlignment(Qt.AlignCenter)
self.label.setPixmap(pixmap)
#---- Prepare a Layout ----
grid = QGridLayout()
button = QPushButton('Rotate 15 degrees')
button.clicked.connect(self.rotate_pixmap)
grid.addWidget(self.label, 0, 0)
grid.addWidget(button, 1, 0)
self.setLayout(grid)
self.rotation = 0
def rotate_pixmap(self):
pixmap = QPixmap(self.imgPath)
self.rotation += 15
transform = QTransform().rotate(self.rotation)
pixmap = pixmap.transformed(transform, Qt.SmoothTransformation)
#---- update label ----
self.label.setPixmap(pixmap)
if __name__ == '__main__':
app = QApplication(sys.argv)
instance = myApplication()
instance.show()
sys.exit(app.exec_())
I found this C++ code which shows how to create a rotation animation using QVariantAnimation and QTransform:
class RotateMe : public QLabel {
Q_OBJECT
public:
explicit RotateMe(QWidget* parent = Q_NULLPTR) :
QLabel(parent),
pixmap(100, 100),
animation(new QVariantAnimation )
{
resize(200, 200);
pixmap.fill(Qt::red);
animation->setDuration(10000);
animation->setStartValue(0.0f);
animation->setEndValue(90.0f);
connect(animation, &QVariantAnimation::valueChanged, [=](const QVariant &value){
qDebug()<<value;
QTransform t;
t.rotate(value.toReal());
setPixmap(pixmap.transformed(t));
});
animation->start();
}
private:
QPixmap pixmap;
QVariantAnimation *animation;
};
How can I re-write this in python? I can't find any examples which show how QVariantAnimation is used in PyQT.
The logic is similar as I show below:
import os
from PyQt5 import QtCore, QtGui, QtWidgets
class RotateMe(QtWidgets.QLabel):
def __init__(self, *args, **kwargs):
super(RotateMe, self).__init__(*args, **kwargs)
self._pixmap = QtGui.QPixmap()
self._animation = QtCore.QVariantAnimation(
self,
startValue=0.0,
endValue=360.0,
duration=1000,
valueChanged=self.on_valueChanged
)
def set_pixmap(self, pixmap):
self._pixmap = pixmap
self.setPixmap(self._pixmap)
def start_animation(self):
if self._animation.state() != QtCore.QAbstractAnimation.Running:
self._animation.start()
#QtCore.pyqtSlot(QtCore.QVariant)
def on_valueChanged(self, value):
t = QtGui.QTransform()
t.rotate(value)
self.setPixmap(self._pixmap.transformed(t))
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
label = RotateMe(alignment=QtCore.Qt.AlignCenter)
img_path = os.path.join('path/to/image','image.svg')
label.set_pixmap(QtGui.QPixmap(img_path))
button = QtWidgets.QPushButton('Rotate')
button.clicked.connect(label.start_animation)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(label)
lay.addWidget(button)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())

Adding custom widgets to a QScrollArea in pyqt5 does not make scrollbar appear

The example shown below adds a very simple custom pyqt5 widget to a QScrollArea. When i move the custom widget "off" the area of the scroll area I would expect that scrollbars should appear. Obviously this is not the case.
#!/usr/bin/python3
from PyQt5.QtWidgets import QMainWindow, QWidget, QApplication, QScrollArea
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QPainter, QColor, QPen
import sys
class ExampleWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawWidget(qp)
qp.end()
def drawWidget(self, qp):
qp.setPen(QColor(255, 0, 0))
qp.setBrush(QColor(255, 0, 0))
qp.drawRect(0, 0, 100, 100)
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(300, 300, 300, 200)
self.scrollArea = QScrollArea(self)
self.setCentralWidget(self.scrollArea)
self.widget = ExampleWidget(self.scrollArea)
self.widget.setGeometry(250, 150, 100, 100)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
How can I make the scrollbars appear once a widget is moved so it's not visible completely anymore?
In the code that samples ExampleWidget is a child of QScrollArea: self.widget = ExampleWidget(self.scrollArea), that does not imply that the widget is handled with the properties of the QScrollArea, the QScrollArea has a viewport() that is the background widget and the QScrollArea only shows a part of it. So if you want a widget to be inside you have to use the setWidget() method. On the other hand that viewport() has the size of the content and how it calculates that size ?, by default it uses the sizeHint() of the widget, and in your case it does not have it so it will not show what you want.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ExampleWidget(QtWidgets.QWidget):
def paintEvent(self, e):
qp = QtGui.QPainter(self)
self.drawWidget(qp)
def drawWidget(self, qp):
qp.setPen(QtGui.QColor(255, 0, 0))
qp.setBrush(QtGui.QColor(255, 0, 0))
qp.drawRect(0, 0, 100, 100)
def sizeHint(self):
return QtCore.QSize(500, 500)
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.scrollArea = QtWidgets.QScrollArea()
self.setCentralWidget(self.scrollArea)
self.widget = ExampleWidget()
self.scrollArea.setWidget(self.widget)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())

Categories