I need to rotate a QGraphicsPixmapItem through a circle. That is, the circle always needs to be at the top left corner of the image and when I drag the circle, the image has to rotate. I set the rotation angle using setRotation and the rotation point with setTransformOriginPoint. However, the rotation is not working fine. Could someone point me in the right direction please?
import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsView
from PyQt5 import QtGui, QtWidgets, QtCore
import math
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.scene = Scene()
self.view = QGraphicsView(self)
self.setGeometry(10, 30, 850, 600)
self.view.setGeometry(20, 22, 800, 550)
self.setFixedSize(850, 600)
self.view.setScene(self.scene)
class Scene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(Scene, self).__init__(parent)
# other stuff here
self.set_image()
def set_image(self):
image = Image()
self.addItem(image)
image.set_pixmap()
class Image(QtWidgets.QGraphicsPixmapItem):
def __init__(self, parent=None):
super(Image, self).__init__(parent)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.setTransformOriginPoint(self.boundingRect().center())
def set_pixmap(self):
pixmap = QtGui.QPixmap("image.jpg")
self.setPixmap(pixmap)
self.pixmap_controller = PixmapController(self)
self.pixmap_controller.set_pixmap_controller()
class PixmapController(QtWidgets.QGraphicsEllipseItem):
def __init__(self, pixmap):
super(PixmapController, self).__init__(parent=pixmap)
self.pixmap = pixmap
color = QtGui.QColor(0, 0, 0)
brush = QtGui.QBrush(color)
self.setBrush(brush)
def set_pixmap_controller(self):
self.setRect(-5, -5, 10, 10)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.startPos = event.pos()
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
self.finalPos = event.pos()
delta = self.finalPos - self.startPos
item_position = self.pixmap.transformOriginPoint()
angle = math.atan2(item_position.y() - self.finalPos.y(), item_position.x() - self.finalPos.x()) / math.pi * 180 - 45
self.parentItem().setRotation(angle)
self.parentItem().setPos(self.parentItem().pos() + delta)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
As with your previous question, you have to consider that the item coordinate system is based on its parent (or the scene, if there's none).
There are also two problems with your code:
you are setting the transformation origin point when the item has no pixmap set, so that point will be null (QPointF(), as in the top left corner of the item) as the bounding rect is null at that time; this will make the transformation inconsistent, as the origin will always be on the top left of the pixmap item, not its center;
you are trying to do both rotation and translation within the same mouse movements and those transformations are obviously conceptually incompatible. The concept of rotation is that it's a circular movement around a center, which is fixed in the coordinate system of the rotation. While you can clearly have both rotation and translation of an object, with a singular reference point for the transformation you cannot have both of them;
If you still want to be able to do both transformations individually using mouse movements, you can use the event modifiers to choose which transformation actually apply. In the following example, the normal mouse movement causes translation, while keeping pressed the Ctrl key when clicking will apply a rotation.
class Image(QtWidgets.QGraphicsPixmapItem):
# ...
def set_pixmap(self):
pixmap = QtGui.QPixmap("image.jpg")
self.setPixmap(pixmap)
self.pixmap_controller = PixmapController(self)
self.pixmap_controller.set_pixmap_controller()
self.setTransformOriginPoint(self.boundingRect().center())
class PixmapController(QtWidgets.QGraphicsEllipseItem):
# ...
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.startPos = event.pos()
self.origin = self.parentItem().transformOriginPoint()
self.startAngle = math.atan2(
self.origin.y(),
self.origin.x()
) / math.pi * 180 - 45
self.isRotating = event.modifiers() == QtCore.Qt.ControlModifier
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
self.finalPos = event.pos()
delta = self.finalPos - self.startPos
angle = math.atan2(
self.origin.y() - delta.y(),
self.origin.x() - delta.x()
) / math.pi * 180 - 45
if self.isRotating:
self.parentItem().setRotation(
self.parentItem().rotation() + (angle - self.startAngle))
else:
self.parentItem().setPos(self.parentItem().pos() + delta)
A small suggestion: functions should always be created for their reusability and/or readability of the code; since you're always creating the same rectangle for the ellipse item (and that rectangle is always based on the parent's coordinate system), there's no use for a dedicate function like set_pixmap_controller and its related call, just use setRect() in the __init__.
Related
import sys
import pytesseract
from PyQt5.QtGui import QPainter, QPen, QImage, QPixmap, QCursor
from PyQt5.QtCore import Qt, QPoint, QRect, QSize
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QAction, QMenu, QSystemTrayIcon, QStyle, QRubberBand
from PIL import ImageGrab, Image, ImageFilter, ImageOps
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Create the UI elements
self.label = QLabel(self)
self.setCentralWidget(self.label)
# Add the menu items
self.menu = QMenu(self)
self.ocr_action = QAction("Perform OCR", self)
self.ocr_action.triggered.connect(self.perform_ocr)
self.menu.addAction(self.ocr_action)
# Set the window to be transparent
self.setWindowOpacity(0.1)
# Create the system tray icon
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.setIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
self.tray_icon.setContextMenu(self.menu)
self.tray_icon.show()
# Create a rubber band for selecting the area
self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.label)
self.rubber_band.setMouseTracking(True)
self.rubber_band.hide()
# Reset the window position and size to full screen
self.reset_position()
def reset_position(self):
screen_size = QApplication.desktop().screenGeometry()
self.setGeometry(screen_size)
self.move(0, 0)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.start_pos = event.pos()
self.rubber_band.setGeometry(QRect(self.start_pos, QSize()))
self.rubber_band.show()
def mouseMoveEvent(self, event):
if self.rubber_band.isVisible():
self.rubber_band.setGeometry(QRect(self.start_pos, event.pos()).normalized())
# Do not move the window while selecting the area for OCR
event.accept()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
# Hide the previous rubber band
if self.rubber_band.isVisible():
self.rubber_band.hide()
# Get the selected rectangle
x1 = min(self.start_pos.x(), event.pos().x())
y1 = min(self.start_pos.y(), event.pos().y())
x2 = max(self.start_pos.x(), event.pos().x())
y2 = max(self.start_pos.y(), event.pos().y())
# Grab the selected area as an image
img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
# Convert the image to a QImage and display it
qimg = QImage(img.tobytes(), img.width, img.height, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(qimg)
self.label.setPixmap(pixmap)
# Show the OCR menu item
self.ocr_action.setVisible(True)
# Show the rubber band
self.rubber_band.setGeometry(QRect(self.start_pos, QSize()))
self.rubber_band.show()
# Perform OCR on the selected area
self.perform_ocr()
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(Qt.red)
painter.drawRect(self.rubber_band.geometry())
def perform_ocr(self):
# Get the selected area as an image
pixmap = self.label.pixmap()
if pixmap is None:
return
# Convert the pixmap to a PIL image
qimage = pixmap.toImage()
buffer = qimage.constBits()
buffer.setsize(qimage.byteCount())
pil_image = Image.frombuffer(
'RGB', (qimage.width(), qimage.height()), buffer, 'raw', 'RGB', 0, 1)
# Perform OCR on the selected area of the image
text = pytesseract.image_to_string(pil_image, lang='eng', config='--psm 6')
# Copy the recognized text to the clipboard
clipboard = QApplication.clipboard()
clipboard.setText(text)
# Hide the window and reset the label
self.hide()
self.label.setPixmap(QPixmap())
# Hide the OCR menu item
self.ocr_action.setVisible(False)
# Hide the rubber band and reset start_pos
self.rubber_band.hide()
self.start_pos = None
def hideEvent(self, event):
super().hideEvent(event)
self.reset_position()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.setGeometry
In Pyqt5 I want to rotate a pixmap but every time i tried it changes the size.
My code is:
import math
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
from PyQt5.QtCore import QObject, QPointF, Qt, QRectF,QRect
from PyQt5.QtGui import QPixmap, QTransform, QPainter
class Window(QWidget):
def __init__(self, *args, **kwargs):
super(Window, self).__init__()
self.arch1 = QPixmap("arch1.png")
pm = QPixmap(556,556)
rectF = QRectF(0,0,556,556)
painter = QPainter(pm)
painter.drawPixmap(rectF, self.arch1, rectF)
painter.end()
self.label = QLabel("AAAAAAAAAA")
self.label.setPixmap(pm)
butA = QPushButton("A")
butA.clicked.connect(lambda: self.rotate_item())
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(butA)
self.setLayout(layout)
self.show()
def rotate_item(self):
rectF = QRectF(0,0,556,556)
self.arch1 = self.arch1.transformed(QTransform().rotate(36))
pix = QPixmap(556,556)
painter = QPainter(pix)
painter.drawPixmap(rectF, self.arch1,QRectF(self.arch1.rect()))
painter.end()
self.label.setPixmap(pix)
def main():
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
if __name__=="__main__":
main()
I want only rotate not resize. What do you suggest me to do?
I have four other files and i want to rotate differently. i post some photos to understand what i want to do.
any other way to do this?
Circle one
Circle two
Complete circle
The problem is that the rotated pixmap has a bigger bounding rectangle.
Consider the following example:
The light blue square shows the actual bounding rectangle of the rotated image, which is bigger.
Using drawPixmap(rectF, self.arch1, QRectF(self.arch1.rect())) will cause to painter to draw the pixmap in the rectangle rectF, using the new bounding rectangle as source, so it becomes "smaller".
Instead of rotating the image, you should rotate the painter. Since transformations by default use the origin point of the painter (0, 0), we need first to translate to the center of the rectangle, rotate the painter, and then retranslate back to the origin.
Note that in the following example I'm also always drawing starting from the source image, without modifying it: continuously applying a transformation will cause drawing artifacts due to aliasing, and after some rotation the quality would be very compromised.
The rotation variable is to keep track of the current rotation.
class Window(QWidget):
def __init__(self, *args, **kwargs):
# ...
self.rotation = 0
def rotate_item(self):
self.rotation = (self.rotation + 36) % 360
rectF = QRectF(0,0,556,556)
pix = QPixmap(556,556)
painter = QPainter(pix)
painter.translate(rectF.center())
painter.rotate(self.rotation)
painter.translate(-rectF.center())
painter.drawPixmap(0, 0, self.arch1)
painter.end()
self.label.setPixmap(pix)
I need to move a QGraphicsPixmapItem through a circle that it is at the top left corner of the image. That is, when I grab with the mouse the circle, I need the top left corner of the image to follow the circle. I subclassed a QGraphicsEllipseItem and reimplemented the itemChange method but when I set the position of the image to that value, the image is not being positioned correctly. What should I modify in my code?
import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QGraphicsView
from PyQt5 import QtGui, QtWidgets
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.scene = Scene()
self.view = QGraphicsView(self)
self.setGeometry(10, 30, 850, 600)
self.view.setGeometry(20, 22, 800, 550)
self.view.setScene(self.scene)
class Scene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(Scene, self).__init__(parent)
# other stuff here
self.set_image()
def set_image(self):
image = Image()
self.addItem(image)
image.set_pixmap()
class Image(QtWidgets.QGraphicsPixmapItem):
def __init__(self, parent=None):
super(Image, self).__init__(parent)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
def set_pixmap(self):
pixmap = QtGui.QPixmap("image.jpg")
self.setPixmap(pixmap)
self.pixmap_controller = PixmapController(self)
self.pixmap_controller.set_pixmap_controller()
self.pixmap_controller.setPos(self.boundingRect().topLeft())
self.pixmap_controller.setFlag(QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges, True)
def change_image_position(self, position):
self.setPos(position)
class PixmapController(QtWidgets.QGraphicsEllipseItem):
def __init__(self, pixmap):
super(PixmapController, self).__init__(parent=pixmap)
self.pixmap = pixmap
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
color = QtGui.QColor(0, 0, 0)
brush = QtGui.QBrush(color)
self.setBrush(brush)
def set_pixmap_controller(self):
self.setRect(-5, -5, 10, 10)
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemPositionChange:
self.pixmap.change_image_position(value)
return super(PixmapController, self).itemChange(change, value)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
When a graphics item has a parent, its coordinate system is based on that parent, not on the scene.
The problem is that when you try to move the PixmapController, the movement is in parent coordinates (the pixmap item). When you check for the ItemPositionChange you are you're changing the parent position but the item position is changed anyway, based on the parent coordinate system.
While you could just return an empty QPoint (which will not change the item position), this wouldn't be a good choice: as soon as you release the mouse and start to move it again, the pixmap will reset its position.
The solution is not to set the movable item flag, but filter for mouse movements, compute a delta based on the click starting position, and use that delta to move the parent item based on its current position.
class PixmapController(QtWidgets.QGraphicsEllipseItem):
def __init__(self, pixmap):
super(PixmapController, self).__init__(parent=pixmap)
self.pixmap = pixmap
# the item should *NOT* move
# self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
color = QtGui.QColor(0, 0, 0)
brush = QtGui.QBrush(color)
self.setBrush(brush)
def set_pixmap_controller(self):
self.setRect(-5, -5, 10, 10)
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.startPos = event.pos()
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
delta = event.pos() - self.startPos
self.parentItem().setPos(self.parentItem().pos() + delta)
If you want to use your change_image_position function, you need to change those functions accordingly; the code below does the same thing as the last line in the example above:
class Image(QtWidgets.QGraphicsPixmapItem):
# ...
def change_image_position(self, delta):
self.setPos(self.pos() + delta)
class PixmapController(QtWidgets.QGraphicsEllipseItem):
# ...
def mouseMoveEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
delta = event.pos() - self.startPos
self.pixmap.change_image_position(delta)
Tip: do not add a child widget to a QMainWindow like that, as it will not resize correctly when the window is resized. Use self.setCentralWidget(self.view) instead; if you want to add margins, use a container QWidget, set that widget as the central widget, add a simple QHBoxLayout (or QVBoxLayout), add the view to that layout and then set the margins with layout.setContentsMargins(left, top, right, bottom)
The main window of my PyQt5 application is set up with a text label along the top above a custom canvas widget which displays an image:
from PyQt5 import QtCore, QtGui, QtWidgets
class Canvas(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.image = None
def paintEvent(self, event):
qp = QtGui.QPainter(self)
if self.image:
qp.drawImage(0, 0, self.image)
class Window(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.canvas = Canvas()
self.label = QtWidgets.QLabel()
self.label.setText('foobar')
self.label.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Fixed)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.canvas)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
content = QtWidgets.QWidget()
content.setLayout(layout)
self.setCentralWidget(content)
self.load_image('a.jpg')
def load_image(self, filename):
image = QtGui.QImage(filename)
self.canvas.image = image
self.canvas.setFixedSize(image.width(), image.height())
self.update()
def keyPressEvent(self, event):
self.load_image('b.jpg')
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
This looks like this, which is what I want:
When the canvas changes to display a smaller image, I want to shrink the window to fit accordingly. However, it looks like this:
It seems that the minimum size that I can give the window if I manually drag to resize it is the size that fits the contents, but why isn't it resizing to this automatically?
When a fixed size is set, it is used as sizeHint, and the latter is used by layouts to set the widget size. So the size of the canvas depends on the size of the widget, but you want the opposite. You must scale the image size to the window size:
from PyQt5 import QtCore, QtGui, QtWidgets
class Canvas(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
self.image = QtGui.QImage()
#property
def image(self):
return self._image
#image.setter
def image(self, image):
self._image = image
self.update()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
if not self.image.isNull():
image = self.image.scaled(
self.size(), QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation
)
qp.drawImage(0, 0, image)
class Window(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.canvas = Canvas()
self.label = QtWidgets.QLabel("foobar")
self.label.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.canvas)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
content = QtWidgets.QWidget()
content.setLayout(layout)
self.setCentralWidget(content)
self.load_image("a.jpg")
def load_image(self, filename):
image = QtGui.QImage(filename)
self.canvas.image = image
def keyPressEvent(self, event):
self.load_image('b.jpg')
super().keyPressEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
In following code, when the button is clicked, I insert the plots into the tabs of an auinotebook in another frame.
for example, When I have multiple plots in the plt window, I can drag a notebook tab into the bottom (that results in displaying two plots). Later on when I delete the bottom tab, and try to go into other plots, I see a flicker like the closed tab is still there.
I guess the issue is with my on_nb_tab_close. Because, without that I was not able to notice any such problem.
I appreciate help. Code samples will be very useful. (wxpython version 2.812)
import wx
import wx.lib.agw.aui as aui
import matplotlib as mpl
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as mplCanvas
def create_plotter(self):
try:
self.plotter.Show()
except AttributeError:
self.plotter =PlotFrame(self, 500, 500)
self.plotter.Show()
return self.plotter
class PlotFrame(wx.Frame):
def __init__(self, parent, height, width):
wx.Frame.__init__(self, None, size=(height,width), title="plts")
self.parent=parent
self.nb = aui.AuiNotebook(self)
self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.on_nb_tab_close, self.nb)
def AddPlotTab(self,name="plot"):
page = Plot(self.nb)
self.nb.AddPage(page,name)
return page
def on_nb_tab_close(self, evt):
print "tab close fired"
s=self.nb.GetSelection()
v=self.nb.RemovePage(s)
if not self.nb.GetPageCount():
self.on_Close(evt)
evt.Veto()
class Plot(wx.Panel):
def __init__(self, parent, id = -1, dpi = None, **kwargs):
wx.Panel.__init__(self, parent, id=id, **kwargs)
self.figure = mpl.figure.Figure(dpi=dpi)
self.canvas = mplCanvas(self, -1, self.figure) # wxPanel object containing plot
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.canvas,1,wx.EXPAND)
self.SetSizer(sizer)
class MainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, title="Plotting test", size=(300, 300))
self.btn1 = wx.Button(self, -1, "Print 1")
self.Bind(wx.EVT_BUTTON, self.OnBtn1, self.btn1)
def OnBtn1(self, evt):
plotter=create_plotter(self)
page1 = plotter.AddPlotTab("case 1: first_plot")
page1.figure.gca().plot(range(10),range(10),'+')
page1.figure.gca().plot(range(10),range(10),'-',color='red')
page1.figure.canvas.draw()
if __name__ == '__main__':
APP = wx.App(False)
FRAME = MainFrame(None)
FRAME.Show()
APP.MainLoop()