Animate QGraphicsView fitInView method - python

So lets say I have a simple QGraphicsView and QGraphicsScene, and I want to animate the view to smoothly scroll/scale over to an arbitrary QRecF within the scene. I know you can call view.fitInView() or view.centerOn() to 'frame up' on the rect, however neither of those provide for much animation. I can iteratively call view.centerOn() to animate the scrolling nicely, but this doesn't handle scale. I believe I need to modify the view's viewport but I'm not quite sure how to do that (calling view.setSceneRect() seemed promising but that just sets the area that the view can manage, not what it is currently 'looking at').
Based on some C++ answer I figured out how to re-write the fitInView method for the most part, however I am stuck on the part where I actually transform the view to look at a different part of the scene without calling centerOn. Here's what I have so far:
view = QtWidgets.QGraphicsView()
scene = QtWidgets.QGraphicsScene()
targetRect = QtCore.QRectF(500, 500, 500, 500)
viewRect = view.viewport().rect()
sceneRect = view.transform().mapRect(targetRect)
xratio = viewRect.width() / sceneRect.width()
yratio = viewRect.height() / sceneRect.height()
# keep aspect
xratio = yratio = min(xratio, yratio)
# so... now what to do with the ratio? I need to scale the view rect somehow.
# animation
start = view.mapToScene(viewRect).boundingRect()
end = sceneRect # I guess?
animator = QtCore.QVariantAnimation()
animator.setStartValue(start)
animator.setEndValue(end)
animator.valueChanged.connect(view.????) # what am I setting here?
animator.start()

I went down many twisty roads only to end up with an incredibly simple answer, thought I'd post it in case anyone finds it useful to animate both scroll and scale at the same time:
view = QtWidgets.QGraphicsView()
scene = QtWidgets.QGraphicsScene()
targetRect = QtCore.QRectF(500, 500, 500, 500)
animator = QtCore.QVariantAnimation()
animator.setStartValue(view.mapToScene(view.viewport().rect()).boundingRect())
animator.setEndValue(targetRect)
animator.valueChanged.connect(lambda x: view.fitInView(x, QtCore.Qt.KeepAspectRatio))
animator.start()

A simple, basic solution is to map the target rectangle and use that value as end value for the animation.
Note that this solution is not really optimal, for two reasons:
it completely relies on fitInView(), which has to compute the transformation for the whole scene at each iteration of the animation by checking the current viewport size; a better (but more complex) implementation would be to use scale() or setTransform() and also call centerOn() on the currently mapped rectangle of the transformation;
since the scene rect might be smaller than what the viewport is showing, zooming out could be a bit awkward;
class SmoothZoomGraphicsView(QtWidgets.QGraphicsView):
def __init__(self):
super().__init__()
scene = QtWidgets.QGraphicsScene()
self.setScene(scene)
self.pixmap = scene.addPixmap(QtGui.QPixmap('image.png'))
self.animation = QtCore.QVariantAnimation()
self.animation.valueChanged.connect(self.smoothScale)
self.setTransformationAnchor(self.AnchorUnderMouse)
def wheelEvent(self, event):
viewRect = self.mapToScene(self.viewport().rect()).boundingRect()
# assuming we are using a basic x2 or /2 ratio, we need to remove
# a quarter of the width/height and translate to half the position
# betweeen the current rectangle and cursor position, or add half
# the size and translate to a negative relative position
if event.angleDelta().y() > 0:
xRatio = viewRect.width() / 4
yRatio = viewRect.height() / 4
translate = .5
else:
xRatio = -viewRect.width() / 2
yRatio = -viewRect.height() / 2
translate = -1
finalRect = viewRect.adjusted(xRatio, yRatio, -xRatio, -yRatio)
cursor = self.viewport().mapFromGlobal(QtGui.QCursor.pos())
if cursor in self.viewport().rect():
line = QtCore.QLineF(viewRect.center(), self.mapToScene(cursor))
finalRect.moveCenter(line.pointAt(translate))
self.animation.setStartValue(viewRect)
self.animation.setEndValue(finalRect)
self.animation.start()
def smoothScale(self, rect):
self.fitInView(rect, QtCore.Qt.KeepAspectRatio)

Related

Qt Custom Animated Button (Ellipse effect)

I am trying to make a custom animated button on PyQt. I found a website which has custom buttons: Buttons website
I already created a topic for making a 3rd button: Stackoverflow for 3rd button
#musicamante helped for the 3rd button, thank you very much again. Now I'm trying to make the 19th button.
My code for 19th button:
import sys, os, time
from math import *
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *
class Button19(QPushButton):
Radius = 10
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.backgroundColors = (QtGui.QColor(QtCore.Qt.lightGray),QtGui.QColor(QtCore.Qt.white))
self.foregroundColors = (QtGui.QColor(QtCore.Qt.black), QtGui.QColor(QtCore.Qt.lightGray))
font = self.font()
font.setBold(True)
self.setFont(font)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.hoverAnimation = QtCore.QVariantAnimation(self)
self.hoverAnimation.setStartValue(0.)
self.hoverAnimation.setEndValue(1.)
self.hoverAnimation.setEasingCurve(QtCore.QEasingCurve.OutCubic)
self.hoverAnimation.setDuration(400)
self.hoverAnimation.valueChanged.connect(self.update)
self.setText("Button")
_m_isHover = False
def enterEvent(self, event):
super().enterEvent(event)
self._m_isHover = True
self.hoverAnimation.setDirection(self.hoverAnimation.Forward)
self.hoverAnimation.start()
def leaveEvent(self, event):
super().leaveEvent(event)
self._m_isHover = False
self.hoverAnimation.setDirection(self.hoverAnimation.Backward)
self.hoverAnimation.start()
def isHover(self):
return self._m_isHover
def paintEvent(self, event):
aniValue = self.hoverAnimation.currentValue()
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
path, path2 = QPainterPath(), QPainterPath()
painter.setBrush(QBrush(self.backgroundColors[0]))
painter.setPen(Qt.NoPen)
rect = QRectF(0, 0, self.width(), self.height())
padding = 10
rect = rect.adjusted(-padding * aniValue, -padding * aniValue, padding * aniValue, padding * aniValue)
path.addRoundedRect(rect.adjusted(padding / 2, padding, -padding / 2, -padding), self.Radius, self.Radius)
painter.drawPath(path)
painter.setBrush(QBrush(self.foregroundColors[0]))
painter.setClipPath(path)
radiusEffectSize = 75
path2.addEllipse(self.rect().center(), radiusEffectSize * aniValue, radiusEffectSize * aniValue)
painter.drawPath(path2)
if self.isHover() or self.hoverAnimation.currentValue() > 0.1: # when leaveEvent triggered, still background color black. So must wait to change textcolor (ofcourse there is probably a better way)
painter.setPen(self.foregroundColors[1])
else:
painter.setPen(self.foregroundColors[0])
painter.drawText(self.rect(), Qt.AlignCenter, self.text())
if __name__ == "__main__":
app = QApplication(sys.argv)
wind = QMainWindow()
wind.setStyleSheet("QMainWindow{background-color:rgb(247,247,250)}")
wind.resize(150, 80)
wid = QWidget()
lay = QHBoxLayout(wid)
lay.setAlignment(Qt.AlignCenter)
mycustombutton = Button19()
lay.addWidget(mycustombutton)
wind.setCentralWidget(wid)
wind.show()
sys.exit(app.exec())
Still feels different, not the same. I need help, thanks!
The main issue in your code is that the padding computation is wrong.
You are increasing the size of the padding from the current rectangle and then decrease it by half the padding size, which doesn't make a lot of sense.
You should instead consider the default padding minus the extent based on the animation value, then adjust (reduce) the rectangle based to it:
padding = 10 * (1 - aniValue)
path.addRoundedRect(
rect.adjusted(padding, padding, -padding, -padding),
self.Radius, self.Radius
)
That will not be sufficient, though: the radius has to consider the actual size of the widget, but that can be misleading: if you take the smaller dimension (between width and height) the ellipse could be smaller than the rectangle, while in the opposite case it would grow up too early, making the animation quite odd. The actual radius should actually be computed using the hypotenuse of the right triangle of the widget width and height (a "perfect" implementation should also consider the radius of the rounded rectangle, but that would be quite too much):
# using hypot() from the math module
radius = hypot(self.width(), self.height()) / 2
path2.addEllipse(self.rect().center(), radius, radius)
Not enough, though: if you closely look at the original animation, you'll see that the "leave" event will not be the same: there is no circle, the "black" rounded rectangle just fades out. We need to take care of that too:
radius = min(self.width(), self.height())
if (self.hoverAnimation.state()
and self.hoverAnimation.direction() == self.hoverAnimation.Forward):
radius *= aniValue
# always full opacity on "fade in"
opacity = 1.
else:
# "fade out"
opacity = aniValue
path2.addEllipse(self.rect().center(), radius, radius)
painter.save()
painter.setOpacity(opacity)
painter.drawPath(path2)
painter.restore()
Nearly there. But the text drawing still has issues. First of all, the "base" should always be painted, and the "hover" should be painted over with the opacity value specified above (unless you want an alpha value). Then, we should always remember that buttons could also use "mnemonics" (keyboard shortcuts that are always highlighted with an underlined character, specified with a preceding & in Qt).
For optimization reasons, it's better to "replicate" similar functions instead of using local variables. It might not be wonderful for reading purposes, but painting functions should be always try to be as fast as possible.
So, here's the final result:
def paintEvent(self, event):
aniValue = self.hoverAnimation.currentValue()
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(QBrush(self.backgroundColors[0]))
painter.setPen(Qt.NoPen)
rect = self.rect()
path = QPainterPath()
padding = 10 * (1 - aniValue)
path.addRoundedRect(
QRectF(rect).adjusted(padding, padding, -padding, -padding),
self.Radius, self.Radius
)
painter.setClipPath(path)
painter.drawPath(path)
painter.setBrush(QBrush(self.foregroundColors[0]))
if aniValue < 1:
# only draw the default text when the animation isn't finished yet
painter.setPen(self.foregroundColors[0])
painter.drawText(rect, Qt.AlignCenter|Qt.TextShowMnemonic, self.text())
if not aniValue:
# no hover, just ignore the rest
return
hoverPath = QPainterPath()
radius = hypot(self.width(), self.height()) / 2
if (aniValue and self.hoverAnimation.state()
and self.hoverAnimation.direction() == self.hoverAnimation.Forward):
hoverPath.addEllipse(rect.center(),
radius * aniValue, radius * aniValue)
painter.drawPath(hoverPath)
else:
hoverPath.addEllipse(rect.center(), radius, radius)
painter.save()
painter.setOpacity(aniValue)
painter.drawPath(hoverPath)
painter.restore()
painter.setPen(self.foregroundColors[1])
painter.drawText(rect, Qt.AlignCenter|Qt.TextShowMnemonic, self.text())
Some further notes:
isHover() is quite pointless unless you need it for something else but painting: except from extreme performance requirements (for which value caching would make sense), underMouse() is usually sufficient; for this case, it is also a bit irrelevant, as we can be quite sure that the hover state only happens when the animation value is 1 or the animation is active (animation.state()) and its direction is Forward;
the "smoothness" of the animation completely depends on its easingCurve(), so please do experiment with all available curves to find what best suits your needs;
when working with plain shapes and no borders ("pens"), Qt normally works fine, as it happens with the code above, but be aware that painting with pixel-based devices (as QWidgets) could create artifacts while using anti-aliasing; in that case you have to consider the "pen width" and translate the drawing by half its size to obtain a "perfect" shape on the screen;

Blending text into background while using native font rendering

I'm trying to implement a plain text display widget, which fades-out into the background on both its sides.
Unfortunately the only way I've been able to achieve this fade-out effect while using the Windows' font engine is by overlaying a gradient going from a solid background color into transparency. This method works fine for when the background behind the widget is consistent, but this is not always the case (e.g. when placed into a QTabWidget it uses the Button role instead of the Window role, or anything non uniform) and causes the gradient's color to be mismatched
Here's an example of when I'm using the Window color role for the background but the actual background is using the Button color role
I have tried painting both into QImage and then painting it as a whole into the widget, and a QGraphicsOpacityEffect set on the widget, but both of these do not use the native Windows drawing and thus have degraded looks, which is highlighted on these images compared to the current method.
The first image highlights how it should look, with it being rendered using ClearType. On the second image, painting into a QImage is used which loses the subpixel anti-aliasing. The third image is using the QGraphicsOpacityEffect which causes the text to look even more blurry, and darker.
The current overlaying is done by painting simple gradient images over the text like so:
def paint_event(self, paint_event: QtGui.QPaintEvent) -> None:
"""Paint the text at its current scroll position with the fade-out gradients on both sides."""
text_y = (self.height() - self._text_size.height()) // 2
painter = QtGui.QPainter(self)
painter.set_clip_rect(
QtCore.QRect(
QtCore.QPoint(0, text_y),
self._text_size,
)
)
painter.draw_static_text(
QtCore.QPointF(-self._scroll_pos, text_y),
self._static_text,
)
# Show more transparent half of gradient immediately to prevent text from appearing cut-off.
if self._scroll_pos == 0:
fade_in_width = 0
else:
fade_in_width = min(
self._scroll_pos + self.fade_width // 2, self.fade_width
)
painter.draw_image(
-self.fade_width + fade_in_width,
text_y,
self._fade_in_image,
)
fade_out_width = self._text_size.width() - self.width() - self._scroll_pos
if fade_out_width > 0:
fade_out_width = min(self.fade_width, fade_out_width + self.fade_width // 2)
painter.draw_image(
self.width() - fade_out_width,
text_y,
self._fade_out_image,
)
And the whole widget code can be found at https://github.com/Numerlor/Auto_Neutron/blob/3c1bdb8211411e86846710cceec9dc2b23b91cc6/auto_neutron/windows/gui/plain_text_scroller.py#L16
As far as I know, and at least on Linux, I sincerely doubt that that would be possible, as "blending" the background would require knowing the (possibly cumulative) background of the parent(s), and subpixel rendering is not available whenever the background and/or foreground have alpha value below 1.0 (or 255) for raster drawing.
Also, text rendering with subpixel correction requires a surface that is aware of the screen, which makes drawing on image pointless.
If you're fine with the default text antialiasing, though, there's a much simpler approach to achieve the fading, and there's no need to override the painting, as you can achieve this with a basic QLabel and using a QLinearGradient set for the WindowText palette role.
The trick is to use the minimumSizeHint() to get the optimal width for the text and compute the correct stops of the gradient, since those values are always in the range between 0 and 1.
class FaderLabel(QtWidgets.QLabel):
fadeWidth = 20
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
palette = self.palette()
self.baseColor = palette.color(palette.WindowText)
self.fadeColor = QtGui.QColor(self.baseColor)
self.fadeColor.setAlpha(0)
self.grad = QtGui.QLinearGradient(0, 0, 1, 0)
self.grad.setCoordinateMode(self.grad.ObjectBoundingMode)
self.setMinimumWidth(self.fadeWidth * 2)
def updateColor(self):
fadeRatio = self.fadeWidth / self.minimumSizeHint().width()
self.grad.setStops([
(0, self.fadeColor),
(fadeRatio, self.baseColor),
(1 - fadeRatio, self.baseColor),
(1, self.fadeColor)
])
palette = self.palette()
palette.setBrush(palette.WindowText, QtGui.QBrush(self.grad))
self.setPalette(palette)
def setText(self, text):
super().setText(text)
self.updateColor()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateColor()
app = QtWidgets.QApplication([])
p = app.palette()
p.setColor(p.Window, QtCore.Qt.black)
p.setColor(p.WindowText, QtCore.Qt.white)
app.setPalette(p)
test = FaderLabel('Hello, I am fading label')
test.show()
app.exec()
The subpixel rendering (like ClearType) will be not be available as written above, since using a gradient makes it almost impossible for the engine to properly draw the "mid" pixels.
Another problem with the above code is that it won't work when using stylesheets. In that case, the solution is to create a helper function that will set the existing stylesheet (including the inherited one), get the actual text color, then create a custom stylesheet with the gradient and finally apply that.
class FaderLabel2(QtWidgets.QLabel):
fadeWidth = 20
_styleSheet = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.updateTimer = QtCore.QTimer(
singleShot=True, interval=1, timeout=self.updateColor)
def updateColor(self):
# restore the default stylesheet (if any)
super().setStyleSheet(self._styleSheet)
# ensure that the palette is properly updated
self.ensurePolished()
baseColor = self.palette().color(QtGui.QPalette.WindowText)
fadeColor = QtGui.QColor(baseColor)
fadeColor.setAlpha(0)
fadeRange = self.fadeWidth / self.minimumSizeHint().width()
styleSheet = '''
color: qlineargradient(x1:0, y1:0, x2:1, y2:0,
stop:0 {fade},
stop:{start} {full},
stop:{end} {full},
stop:1 {fade});
'''.format(
fade=fadeColor.name(QtGui.QColor.HexArgb),
full=baseColor.name(QtGui.QColor.HexArgb),
start=fadeRange, end=1-fadeRange)
super().setStyleSheet(styleSheet)
def changeEvent(self, event):
if event.type() == event.StyleChange:
self.updateTimer.start()
def setText(self, text):
super().setText(text)
self.updateColor()
def setStyleSheet(self, styleSheet):
self._styleSheet = styleSheet
self.updateTimer.start()
def resizeEvent(self, event):
super().resizeEvent(event)
self.updateTimer.start()

How to paint QGraphicsItems from scene onto QImage without changing them in the QGraphicsScene?

I'm converting QGraphicsItem's into rasterized masks at their location in a QGraphicsScene. I'm using those masks for further processing of a video (taking the average intensity inside the mask). To achieve this, I'm painting each item in the scene one by one on a QImage, which has a size just big enough to envelop the item. Everything works well enough, but the items in the scene disappear. That is because I'm removing the pen from the item when I paint it on the QImage. I set the original pen back when I'm done, but the items don't reappear on the scene.
How can I "refresh" the scene to make the items reappear, or alternatively, prevent the items form disappearing altogether?
I couldn't really find anything of people running into this problem. So maybe I'm just doing something fundamentally wrong. Any suggestions are welcome.
Here's my code:
class MyThread(QtCore.QThread):
def __init__(self, scene):
super().__init__()
self.scene = scene
def run(self):
for item in self.scene.items():
# Render the ROI item to create a rasterized mask.
qimage = self.qimage_from_shape_item(item)
# do some stuff
#staticmethod
def qimage_from_shape_item(item: QtWidgets.QAbstractGraphicsShapeItem) -> QtGui.QImage:
# Get items pen and brush to set back later.
pen = item.pen()
brush = item.brush()
# Remove pen, set brush to white.
item.setPen(QtGui.QPen(QtCore.Qt.NoPen))
item.setBrush(QtCore.Qt.white)
# Determine the bounding box in pixel coordinates.
top = int(item.scenePos().y() + item.boundingRect().top())
left = int(item.scenePos().x() + item.boundingRect().left())
bottom = int(item.scenePos().y() + item.boundingRect().bottom()) + 1
right = int(item.scenePos().x() + item.boundingRect().right()) + 1
size = QtCore.QSize(right - left, bottom - top)
# Initialize qimage, use 8-bit grayscale.
qimage = QtGui.QImage(size, QtGui.QImage.Format_Grayscale8)
qimage.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(qimage)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
# Offset the painter to paint item in its correct pixel location.
painter.translate(item.scenePos().x() - left, item.scenePos().y() - top)
# Paint the item.
item.paint(painter, QtWidgets.QStyleOptionGraphicsItem())
# Set the pen and brush back.
item.setPen(pen)
item.setBrush(brush)
# Set the pixel coordinate offset of the item to the QImage.
qimage.setOffset(QtCore.QPoint(left, top))
return qimage
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
view = QtWidgets.QGraphicsView()
layout.addWidget(view)
scene = QtWidgets.QGraphicsScene()
view.setScene(scene)
thread = MyThread(scene)
view.setFixedSize(400, 300)
scene.setSceneRect(0, 0, 400, 300)
rect_item = QtWidgets.QGraphicsRectItem()
p = QtCore.QPointF(123.4, 56.78)
rect_item.setPos(p)
r = QtCore.QRectF(0., 0., 161.8, 100.)
rect_item.setRect(r)
scene.addItem(rect_item)
button = QtWidgets.QPushButton("Get masks")
layout.addWidget(button)
button.clicked.connect(thread.start)
widget.show()
sys.exit(app.exec_())
The problem is that you "can only create and use GUI widgets on main thread", see more information in this SO answer here.
The way I solved it was to take the GUI interaction part, i.e. qimage_from_shape_item(), out of the thread and deal with it in the main loop. I suppose it's still not great that I'm using the items directly, although there is no visible flicker effect or anything from temporarily setting NoPen.
An alternative might have been to use QGraphicsScene::render; however, I don't know how to render the items one by one without interacting with the other items on the scene.

Cannot properly position QGraphicsRectItem in scene

I cannot figure this out for the life of me, but I've boiled this down to a self contained problem.
What I am trying to do, is draw a QGraphicsRectItem around the items that are selected in a QGraphicsScene. After the rect is drawn it can be moved in a way that moves all of the items together. I've looked into QGraphicsItemGroup already and decided it is not feasible in my real use case.
The problem: I've been able to accomplish everything mentioned above, except I can't get the rect item to be positioned properly i.e. it is the right size and by moving it all items are moved but it is not lined up with the united bounding rect of the selected items. I've tried to keep everything in scene coordinates so I'm not sure why there is an offset.
Why does there appear to be an offset and how can this be mitigated?
Here is the runnable code that can be tested by ctrl-clicking or rubber band selection (I know this is a good amount of code but the relevant sections are commented).
#####The line where the position of the rect item is set is marked like this#####
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys
class DiagramScene(QGraphicsScene):
def __init__(self, parent=None):
super().__init__(parent)
self.selBox = None
self.selectionChanged.connect(self.onSelectionChange)
#pyqtSlot()
def onSelectionChange(self):
count = 0
items = self.selectedItems()
# Get bounding rect of all selected Items
for item in self.selectedItems():
if count == 0:
rect = item.mapRectToScene(item.boundingRect())
else:
rect = rect.unite(item.mapRectToScene(item.boundingRect()))
count += 1
if count > 0:
if self.selBox:
# Update selBox if items are selected and already exists
self.selBox.setRect(rect)
self.selBox.items = items
else:
# Instantiate selBox if items are selected and does not already exist
self.selBox = DiagramSelBox(rect, items)
##### Set position of selBox to topLeft corner of united rect #####
self.selBox.setPos(rect.topLeft())
self.addItem(self.selBox)
elif self.selBox:
# Remove selBox from scene if no items are selected and box is drawn
self.removeItem(self.selBox)
del self.selBox
self.selBox = None
class DiagramSelBox(QGraphicsRectItem):
def __init__(self, bounds, items, parent=None, scene=None):
super().__init__(bounds, parent, scene)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.pressPos = None
self.items = items
def paint(self, painter, option, widget=None):
pen = QPen(Qt.DashLine)
painter.setPen(pen)
painter.drawRect(self.rect())
def mousePressEvent(self, e):
# Get original position of selBox when clicked
self.pressPos = self.pos()
# mouseEvent is not passed on to scene so item selection
# does not change
def mouseMoveEvent(self, e):
super().mouseMoveEvent(e)
if self.pressPos:
# Move selBox is original position is set
newPos = self.mapToScene(e.pos()) - self.rect().center()
self.setPos(newPos)
def mouseReleaseEvent(self, e):
# Update position of all selected items
change = self.scenePos() - self.pressPos
for item in self.items:
item.moveBy(change.x(), change.y())
super().mouseReleaseEvent(e)
if __name__ == "__main__":
app = QApplication(sys.argv)
view = QGraphicsView()
view.setDragMode(QGraphicsView.RubberBandDrag)
scene = DiagramScene()
scene.setSceneRect(0, 0, 500, 500)
rect1 = scene.addRect(20, 20, 100, 50)
rect2 = scene.addRect(80, 80, 100, 50)
rect3 = scene.addRect(140, 140, 100, 50)
rect1.setFlag(QGraphicsItem.ItemIsSelectable, True)
rect2.setFlag(QGraphicsItem.ItemIsSelectable, True)
rect3.setFlag(QGraphicsItem.ItemIsSelectable, True)
view.setScene(scene)
view.show()
sys.exit(app.exec_())
I don't have PyQt installed, but I've run into similar issues with the regular QT and QGraphicsRectItem.
I think you've mixed some things up regarding the coordinate system. The bounding-rect of every QGraphicsItem is in local coordinates. The Point (0,0) in local-coordinates appears at the scene on the coordinates given by QGraphicsItem::pos() (scene-coordiantes).
QGraphicsRectItem is a bit special, because we normally don't touch pos at all (so we leave it at 0,0) and pass a rect in scene-coordinates to setRect. QGraphicsRectItem::setRect basically set's the bounding rect to the passed value. So if you don't call setPos (in onSelectionChange) at all, and only pass scene-coordinates to setRect you should be fine.
The mouseEvents in DiagramSelBox need to be adjusted as well. My approach would look like this:
mousePress: store the difference between e.pos (mapped to scene) and self.rect.topLeft() in self.diffPos and copy self.rect.topLeft to self.startPos
mouseMove: ensure that the difference between e.pos (mapped to scene) and self.rect.topLeft() stays the same, by moving self.rect around (use self.diffPos for the calculation)
mouseRelease: move the items by the difference between self.rect.topLeft() and self.startPos.
Hope that helps to get you started.

Rescale image when parent is resized in wxPython

I need to be able to rescale an image (in realtime) in a wx.Panel when parent wx.Frame is resized (for example for wxPython for image and buttons (resizable)).
This code now works, the behaviour is like in a standard Photo Viewer : the image fits perfectly the parent window, and the rescaling respects aspect ratio.
import wx
class MainPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, -1, style=wx.FULL_REPAINT_ON_RESIZE)
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.img = wx.Image('background.png', wx.BITMAP_TYPE_PNG)
self.imgx, self.imgy = self.img.GetSize()
def OnPaint(self, event):
dc = wx.PaintDC(self)
dc.Clear()
x,y = self.GetSize()
posx,posy = 0, 0
newy = int(float(x)/self.imgx*self.imgy)
if newy < y:
posy = int((y - newy) / 2)
y = newy
else:
newx = int(float(y)/self.imgy*self.imgx)
posx = int((x - newx) / 2)
x = newx
img = self.img.Scale(x,y, wx.IMAGE_QUALITY_HIGH)
self.bmp = wx.BitmapFromImage(img)
dc.DrawBitmap(self.bmp,posx,posy)
class MainFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, title='Test', size=(600,400))
self.panel = MainPanel(self)
self.Show()
app = wx.App(0)
frame = MainFrame(None)
app.MainLoop()
Before continuing to implement some things, I would to know :
is this the "good way" to do it ?
the FULL_REPAINT_ON_RESIZE might be a bit too much (inefficient), but I couldn't make it without this, do you have ideas to improve this ?
how to track mouse clicks ? Example : I want to track a click in a rectangle (10,20) to (50,50) in the original coordinates of the original image. Should I convert this in new coordinates (because the image has been rescaled!) ? This would mean I should do all things now in a very low-level...
This is not a bad way to do it but there are a couple of things you could easily optimize:
Don't call wxImage::Scale() every time in OnPaint(). This is a slow operation and you should do it only once, in your wxEVT_SIZE handler instead of doing it every time you repaint the window.
Call SetBackgroundStyle(wxBG_STYLE_PAINT) to indicate that your wxEVT_PAINT handler already erases the background entirely, this will reduce flicker on some platforms (you won't see any difference on the others which always use double buffering anyhow, but it will still make repainting marginally faster even there).
As for the mouse clicks, I think you don't have any choice but to translate the coordinates yourself.

Categories