How do you draw a grid and rectangles in Python? - python

What Python-related code (PyGTK, Glade, Tkinter, PyQT, wxPython, Cairo, ...) could you easily use to create a GUI to do some or all of the following?
Part of the GUI has an immovable square grid.
The user can press a button to create a resizable rectangle.
The user can drag the rectangle anywhere on the grid, and it will snap to the grid.

The DiagramScene Eaxmple that comes with PyQt implements much of the functionality you want. It has a fixed background grid, you can create a rectangle object but it's not resizable and doesn't snap to grid.
This SO article has advice on resizing graphical objects with the mouse. It's for C++ Qt but the technique should be easy to replicate in PyQt.
For snap-to-grid I don't think there is any built-in functionality. You would probably need to reimplement the itemChange(GraphicsItemChange change, const QVariant &value) function. Pseudocode:
if (object not possitioned exactly on the grid):
(possition the item on the grid)
Repossitioning the item will cause itemChange to get called again, but that's ok because the item will be possitioned correctly and won't be moved again, so you'll not be stuck in an endless loop.

I was looking for a while for something like this, and finally managed to cook up a "minimal" working example with Python wx, utilizing wx.lib.ogl and its Diagram and ShapeCanvas classes. The code (below) results with something like this:
Note:
The app starts with the circle added; press SPACE to add rectangles at random position
Click an object to select it (to show handles); to deselect it, click the object again (clicking the background has no effect) - this is functionality of ogl
The grid is drawn "manually"; however the snapping-to-grid is functionality of ogl
Snap-to-grid only works automatically when moving shapes with mouse drag; for other purposes you must manually call it
Snap-to-grid - as well as resizing of shape by handles - works in respect to the center of each shape (not sure if ogl allows for changing that anchor to, say, bottom left corner)
The example uses a MyPanel class that does its own drawing, and inherits both from ogl.ShapeCanvas and from wx.Panel (though the mixin with wx.Panel can be dropped, and the code will still work the same) - which is then added to a wx.Frame. Note the code comments for some caveats (such as the use of ogl.ShapeCanvas blocking all key events, unless a SetFocus is performed on that widget first).
The code:
import wx
import wx.lib.ogl as ogl
import random
# tested on wxPython 2.8.11.0, Python 2.7.1+, Ubuntu 11.04
# started from:
# http://stackoverflow.com/questions/25756896/drawing-to-panel-inside-of-frame-in-wxpython/27804975#27804975
# see also:
# wxPython-2.8.11.0-demo/demo/OGL.py
# https://www.daniweb.com/software-development/python/threads/186203/creating-editable-drawing-objects-in-wxpython
# http://gscept.com/svn/Docs/PSE/Milestone%203/code/trunk/python_test/src/oglEditor.py
# http://nullege.com/codes/search/wx.lib.ogl.Diagram
# http://nullege.com/codes/show/src%40w%40e%40web2cms-HEAD%40web2py%40gluon%40contrib%40pyfpdf%40designer.py/465/wx.lib.ogl.Diagram/python
# https://www.daniweb.com/software-development/python/threads/204969/setfocus-on-canvas-not-working
# http://stackoverflow.com/questions/3538769/how-do-you-draw-a-grid-and-rectangles-in-python
# http://stackoverflow.com/questions/7794496/snapping-to-pixels-in-wxpython
# ogl.ShapeCanvas must go first, else TypeError: Cannot create a consistent method resolution
class MyPanel(ogl.ShapeCanvas, wx.Panel):#(wx.PyPanel): #PyPanel also works
def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=0, name="MyPanel"):
super(MyPanel, self).__init__(parent, id, pos, size, style, name)
self.gridsize = 20 # in pixels
# must have these (w. Diagram) if using ogl.ShapeCanvas:
self.diagram = ogl.Diagram()
self.SetDiagram(self.diagram)
self.diagram.SetCanvas(self)
# set up snap to grid - note, like this it works only for drag (relative to shape center), not for resize via handles!
self.diagram.SetGridSpacing( self.gridsize )
self.diagram.SetSnapToGrid( True )
# initialize array of shapes with one element
self.shapes = []
self.MyAddShape(
ogl.CircleShape(85), # diameter - drag marquee will not be visible if (diameter mod gridsize == 0), as it will overlap with the grid lines
60, 60, wx.Pen(wx.BLUE, 3), wx.GREEN_BRUSH, "Circle"
)
self.Bind(wx.EVT_SIZE, self.OnSize)
self.Bind(wx.EVT_PAINT, self.OnPaint)
wx.EVT_KEY_DOWN(self, self.OnKeyPressedM)
def OnKeyPressedM(self, event):
keyCode = event.GetKeyCode()
print("MyPanel.OnKeyPressedM: %d"%(keyCode) )
# insert a rectangle here on [SPACE]:
if keyCode == wx.WXK_SPACE:
randx = random.randint(1, 300)
randy = random.randint(1, 200)
if self.diagram.GetSnapToGrid():
randx, randy = self.Snap(randx, randy) # must do snapping (if desired) manually, here at insertion!
self.MyAddShape(
ogl.RectangleShape(60, 20),
randx, randy, wx.BLACK_PEN, wx.LIGHT_GREY_BRUSH, "Rect %d"%(len(self.shapes))
)
self.Refresh(False)
event.Skip() # must have this, to have the MyFrame.OnKeyPressed trigger as well!
def OnSize(self, event):
#print("OnSize" +str(event))
self.Refresh() # must have here!
event.Skip()
def DrawBackgroundGrid(self):
dc = wx.PaintDC(self)
#print(dc)
rect = self.GetClientRect()
rx, ry, rw, rh = rect
dc.SetBrush(wx.Brush(self.GetForegroundColour()))
dc.SetPen(wx.Pen(self.GetForegroundColour()))
# draw ("tile") the grid
x = rx
while x < rx+rw:
y = ry
dc.DrawLine(x, ry, x, ry+rh) # long (vertical) lines
while y < ry+rh:
dc.DrawLine(x, y, x+self.gridsize, y) # short (horizontal) lines
y = y + self.gridsize
x = x + self.gridsize
def OnPaint(self, event):
dc = wx.PaintDC(self) # works
self.DrawBackgroundGrid()
# self.Refresh() # recurses here - don't use!
# self.diagram.GetCanvas().Refresh() # blocks here - don't use!
self.diagram.GetCanvas().Redraw(dc) # this to redraw the elements on top of the grid, drawn just before
# MyAddShape is from OGL.py:
def MyAddShape(self, shape, x, y, pen, brush, text):
# Composites have to be moved for all children to get in place
if isinstance(shape, ogl.CompositeShape):
dc = wx.ClientDC(self)
self.PrepareDC(dc)
shape.Move(dc, x, y)
else:
shape.SetDraggable(True, True)
shape.SetCanvas(self)
shape.SetX(x)
shape.SetY(y)
if pen: shape.SetPen(pen)
if brush: shape.SetBrush(brush)
if text:
for line in text.split('\n'):
shape.AddText(line)
#shape.SetShadowMode(ogl.SHADOW_RIGHT)
self.diagram.AddShape(shape)
shape.Show(True)
evthandler = MyEvtHandler(self)
evthandler.SetShape(shape)
evthandler.SetPreviousHandler(shape.GetEventHandler())
shape.SetEventHandler(evthandler)
self.shapes.append(shape)
return shape
# copyfrom OGL.pyl; modded
class MyEvtHandler(ogl.ShapeEvtHandler):
def __init__(self, parent): #
ogl.ShapeEvtHandler.__init__(self)
self.parent = parent
def UpdateStatusBar(self, shape):
x, y = shape.GetX(), shape.GetY()
width, height = shape.GetBoundingBoxMax()
self.parent.Refresh(False) # do here, to redraw the background after a drag move, or scale of shape
print("Pos: (%d, %d) Size: (%d, %d)" % (x, y, width, height))
def OnLeftClick(self, x, y, keys=0, attachment=0):
# note: to deselect a selected shape, don't click the background, but click the shape again
shape = self.GetShape()
canvas = shape.GetCanvas()
dc = wx.ClientDC(canvas)
canvas.PrepareDC(dc)
if shape.Selected():
shape.Select(False, dc)
#canvas.Redraw(dc)
canvas.Refresh(False)
else:
redraw = False
shapeList = canvas.GetDiagram().GetShapeList()
toUnselect = []
for s in shapeList:
if s.Selected():
# If we unselect it now then some of the objects in
# shapeList will become invalid (the control points are
# shapes too!) and bad things will happen...
toUnselect.append(s)
shape.Select(True, dc)
if toUnselect:
for s in toUnselect:
s.Select(False, dc)
##canvas.Redraw(dc)
canvas.Refresh(False)
self.UpdateStatusBar(shape)
def OnEndDragLeft(self, x, y, keys=0, attachment=0):
shape = self.GetShape()
ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment)
if not shape.Selected():
self.OnLeftClick(x, y, keys, attachment)
self.UpdateStatusBar(shape)
def OnSizingEndDragLeft(self, pt, x, y, keys, attch):
ogl.ShapeEvtHandler.OnSizingEndDragLeft(self, pt, x, y, keys, attch)
self.UpdateStatusBar(self.GetShape())
def OnMovePost(self, dc, x, y, oldX, oldY, display):
shape = self.GetShape()
ogl.ShapeEvtHandler.OnMovePost(self, dc, x, y, oldX, oldY, display)
self.UpdateStatusBar(shape)
if "wxMac" in wx.PlatformInfo:
shape.GetCanvas().Refresh(False)
def OnRightClick(self, *dontcare):
#self.log.WriteText("%s\n" % self.GetShape())
print("OnRightClick")
class MyFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "Custom Panel Grid Demo")
# This creates some pens and brushes that the OGL library uses.
# (else "global name 'BlackForegroundPen' is not defined")
# It should be called after the app object has been created, but
# before OGL is used.
ogl.OGLInitialize()
self.SetSize((300, 200))
self.panel = MyPanel(self) #wx.Panel(self)
self.panel.SetBackgroundColour(wx.Colour(250,250,250))
self.panel.SetForegroundColour(wx.Colour(127,127,127))
sizer_1 = wx.BoxSizer(wx.HORIZONTAL)
sizer_1.Add(self.panel, 1, wx.EXPAND | wx.ALL, 0)
self.SetSizer(sizer_1)
self.SetAutoLayout(1)
self.Layout()
self.Show(1)
# NOTE: on my dev versions, using ogl.Diagram causes _all_
# key press events, from *anywhere*, to stop propagating!
# Doing a .SetFocus on the ogl.ShapeCanvas panel,
# finally makes the Key events propagate!
# (troubleshoot via run.py from wx python demo)
self.panel.SetFocus()
self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPressed) # EVT_CHAR_HOOK EVT_KEY_DOWN
def OnKeyPressed(self, event):
print("MyFrame.OnKeyPressed (just testing)")
app = wx.App(0)
frame = MyFrame(None)
app.SetTopWindow(frame)
frame.Show()
app.MainLoop()

Those actions are not that difficult. All you really need for that is hit detection, which is not hard (is the cursor over the correct area? Okay, perform the operation then). The harder part is finding an appropriate canvas widget for the toolkit in use.

Related

wxPython Manual Scroll of panel ScrolledPanel failing with wx.Grid

I've created a panel thats derived from wx.lib.scrolledpanel. I could scroll on it fine with my mousewheel until I put a grid into the panel. Now when the mouse cursor is on top of the grid, the scroll stopped working, and would start working again if i moved the cursor outside of the grid.
I figured the easiest solution after searching and searching was to just manually capture the mousewheel event and scroll the panel manually. I bound this handler to wx.EVT_MOUSEWHEEL inside my wx.App object
class Wx_app(wx.App):
def __init__(self):
super().__init__(clearSigInt=True)
self.frame = MyFrame(None, pos=(0,0), size=(1900, 1100))
self.Bind(wx.EVT_MOUSEWHEEL, self.on_mouse_wheel)
def on_mouse_wheel(self, e):
# get the current scroll pos, is tuple with x as first val, y as second val
pos = self.frame.panel.CalcUnscrolledPosition(0, 0)
y_pos = pos[1]
# detemrine if user is scrolling up or down
if e.GetWheelRotation() > 0:
# user is scrolling up
print("UP")
self.frame.panel.Scroll(0, y_pos + 10)
else:
# user is scrolling down
print("DOWN")
self.frame.panel.Scroll(0, y_pos - 10)
This code works when i try to scroll down, but when i try to scroll back up with the the mousewheel nothing happens, even though "UP" registers in my terminal. Also i would think up should be y_pos - 10 and not y_pos + 10, but then the wheel scrolls in the opposite direction you would expect. What am I doing wrong? Perhaps i'm not getting the correct existing position in the first place, but CalcUnscrolledPosition is the only thing I could find that could maybe do that. I'm new to Python please explain it like I'm a 5 year old. thanks
Your immediate issue is that Up would be Y - 10 and Down would be Y + 10.
You need to go further up (less y) or further down (more y).
You also may well be comparing Apples with Oranges, when relying on the position as returned from the mouse event and how that relates to the position within the scrolled window.
The crux of this is that both the scrolledpanel and the grid are scrollable widgets.
Your issue seems to be that your grid is not of sufficent size to show the grid's scrollbars, thus causing confusion.
You can force the scrollbars on the grid and size it so that it is obvious that there are two sets of scrollbars and hopefully your users will work it out.
Here is a sample grid in a scrolled panel to play with (ignore the bound functions, they were there because initially I misunderstood your issue, thinking that you wanted manual scrolling within the grid)
import wx
import wx.lib.scrolledpanel
import wx.grid
class MyPanel(wx.Panel):
def __init__(self, *args, **kwargs):
wx.Panel.__init__(self, *args, **kwargs)
self.sizer = wx.BoxSizer(wx.HORIZONTAL)
self.fileFormPanel = FileFormPanel(self)
self.sizer.Add(self.fileFormPanel, 1, wx.EXPAND)
self.SetSizer(self.sizer)
class FileFormPanel(wx.lib.scrolledpanel.ScrolledPanel):
def __init__(self, parent):
wx.lib.scrolledpanel.ScrolledPanel.__init__(self, parent)
sizer = wx.BoxSizer(wx.VERTICAL)
# Create a wxGrid object
self.grid = wx.grid.Grid(self, -1, size=(300,480))
self.grid.CreateGrid(5, 10)
self.grid.SetRowLabelSize(1)
self.grid.SetDefaultColSize(120)
self.grid.SetColSize(0, 50)
self.grid.ShowScrollbars(True, True)
for j in range(5):
for k in range(10):
self.grid.SetCellValue(j, k, str(k))
text = wx.TextCtrl(self, wx.ID_ANY, value="Some text")
sizer.Add(self.grid)
sizer.Add(text)
self.SetSizer(sizer)
self.SetupScrolling()
#self.grid.Bind(wx.EVT_MOUSEWHEEL, self.OnGrid)
#self.Bind(wx.EVT_MOUSEWHEEL, self.OnScroll)
#self.grid.Bind(wx.grid.EVT_GRID_SELECT_CELL, self.OnSelect)
# def OnGrid(self, event):
# obj = event.GetEventObject()
# if event.WheelRotation > 0:
# self.grid.MoveCursorUp(False)
# else:
# self.grid.MoveCursorDown(False)
#
# def OnSelect(self, event):
# obj = event.GetEventObject()
# r = event.GetRow()
# c = event.GetCol()
# self.grid.MakeCellVisible(r,c)
# event.Skip()
#
# def OnScroll(self, event):
# print("window scroll")
# event.Skip()
class DemoFrame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, parent = None, size = (400, 400))
MyPanel(self)
class App(wx.App):
def OnInit(self):
self.frame = DemoFrame()
self.frame.Show()
self.SetTopWindow(self.frame)
return True
app = App()
app.MainLoop()

How to create circular image using pyqt4?

Here I wrote this code but did not work:
import sys
from PyQt4 import QtGui, QtCore
class CricleImage(QtCore.QObject):
def __init__(self):
super(CricleImage, self).__init__()
self.pix = QtGui.QGraphicsPixmapItem(QtGui.QPixmap("bird(01).jpg"))
#drawRoundCircle
rect = self.pix.boundingRect()
self.gri = QtGui.QGraphicsRectItem(rect)
self.gri.setPen(QtGui.QColor('red'))
if __name__ == '__main__':
myQApplication = QtGui.QApplication(sys.argv)
IMG = CricleImage()
#scene
scene = QtGui.QGraphicsScene(0, 0, 400, 300)
scene.addItem(IMG.pix)
#view
view = QtGui.QGraphicsView(scene)
view.show()
sys.exit(myQApplication.exec_())
One possible solution is to overwrite the paint() method of the QGraphicsPixmapItem and use setClipPath to restrict the painting region:
from PyQt4 import QtCore, QtGui
class CirclePixmapItem(QtGui.QGraphicsPixmapItem):
#property
def radius(self):
if not hasattr(self, "_radius"):
self._radius = 0
return self._radius
#radius.setter
def radius(self, value):
if value >= 0:
self._radius = value
self.update()
def paint(self, painter, option, widget=None):
painter.save()
rect = QtCore.QRectF(QtCore.QPointF(), 2 * self.radius * QtCore.QSizeF(1, 1))
rect.moveCenter(self.boundingRect().center())
path = QtGui.QPainterPath()
path.addEllipse(rect)
painter.setClipPath(path)
super().paint(painter, option, widget)
painter.restore()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
pixmap = QtGui.QPixmap("logo.jpg")
scene = QtGui.QGraphicsScene()
view = QtGui.QGraphicsView(scene)
view.setRenderHints(
QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform
)
it = CirclePixmapItem(pixmap)
scene.addItem(it)
it.radius = pixmap.width() / 2
view.show()
sys.exit(app.exec_())
Update:
# ...
view = QtGui.QGraphicsView(
scene, alignment=QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft
)
# ...
view.show()
it.setPos(80, 80)
sys.exit(app.exec_())
Second possible solution:
import sys
#from PyQt4 import QtCore, QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class Label(QLabel):
def __init__(self, *args, antialiasing=True, **kwargs):
super(Label, self).__init__(*args, **kwargs)
self.Antialiasing = antialiasing
self.setMaximumSize(200, 200)
self.setMinimumSize(200, 200)
self.radius = 100
self.target = QPixmap(self.size())
self.target.fill(Qt.transparent) # Fill the background with transparent
# Upload image and zoom to control level
p = QPixmap("head2.jpg").scaled(
200, 200, Qt.KeepAspectRatioByExpanding, Qt.SmoothTransformation)
painter = QPainter(self.target)
if self.Antialiasing:
# antialiasing
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
path = QPainterPath()
path.addRoundedRect(
0, 0, self.width(), self.height(), self.radius, self.radius)
# pruning
painter.setClipPath(path)
painter.drawPixmap(0, 0, p)
self.setPixmap(self.target)
class Window(QWidget):
def __init__(self, *args, **kwargs):
super(Window, self).__init__(*args, **kwargs)
layout = QHBoxLayout(self)
layout.addWidget(Label(self))
self.setStyleSheet("background: green;")
if __name__ == "__main__":
app = QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
Another approach, slightly different from the one provided by eyllanesc. While this might seem much more complicated than that, I believe that it offers a better implementation and interface, with the addition of better performance.
In this case, instead of overriding the paint method (that is run everytime the item is painted, which happens very often), I'm using the shape() function along with the QGraphicsItem.ItemClipsToShape flag, that allows to limit the painting only within the boundaries of the path shape.
What shape() does is to return a QPainterPath that includes only the "opaque" portions of an item that will react to mouse events and collision detection (with the scene boundaries and its other items). In the case of a QGraphicsPixmapItem this also considers the possible mask (for example, a PNG based pixmap with transparent areas, or an SVG image). By setting the ItemClipsToShape we can ensure that the painting will only cover the parts of the image that are within that shape.
The main advantage of this approach is that mouse interaction and collision detection with other items honors the actual circle shape of the item.
This means that if you click outside the circle (but still within the rectangle area of the full image), the item will not receive the event. Also, if the image supports masking (a PNG with transparent areas) which by default would not be part of the shape, this method will take that into account.
Also, by "caching" the shape we are also speeding up the painting process a bit (since Qt will take care of it, without any processing done using python).
class CircleClipPixmapItem(QtGui.QGraphicsPixmapItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFlag(self.ItemClipsToShape)
self.updateRect()
def updateRect(self):
baseRect = super().boundingRect()
minSize = min(baseRect.width(), baseRect.height())
self._boundingRect = QtCore.QRectF(0, 0, minSize, minSize)
self._boundingRect.moveCenter(baseRect.center())
self._shape = QtGui.QPainterPath()
self._shape.addEllipse(self._boundingRect)
# the shape might include transparent areas, using the & operator
# I'm ensuring that _shape only includes the areas that intersect
# the shape provided by the base implementation
self._shape &= super().shape()
def setPixmap(self, pm):
super().setPixmap(pm)
# update the shape to reflect the new image size
self.updateRect()
def setShapeMode(self, mode):
super().setShapeMode(mode)
# update the shape with the new mode
self.updateRect()
def boundingRect(self):
return self._boundingRect
def shape(self):
return self._shape
Keep in mind that there's a catch about both methods: if the aspect ratio of the image differs very much from 1:1, you'll always end up with some positioning issues. With my image, for example, it will always be shown 60 pixel right from the actual item position. If you want to avoid that, the updateRect function will be slightly different and, unfortunately, you'll have to override the paint() function (while still keeping it a bit faster than other options):
def updateRect(self):
baseRect = super().boundingRect()
minSize = min(baseRect.width(), baseRect.height())
self._boundingRect = QtCore.QRectF(0, 0, minSize, minSize)
# the _boundingRect is *not* centered anymore, but a new rect is created
# as a reference for both shape intersection and painting
refRect= QtCore.QRectF(self._boundingRect)
refRect.moveCenter(baseRect.center())
# note the minus sign!
self._reference = -refRect.topLeft()
self._shape = QtGui.QPainterPath()
self._shape.addEllipse(self._boundingRect)
self._shape &= super().shape().translated(self._reference)
# ...
def paint(self, painter, option, widget):
# we are going to translate the painter to the "reference" position,
# let's save its state before that
painter.save()
painter.translate(self._reference)
super().paint(painter, option, widget)
painter.restore()
This will make the boundingRect (and resulting internal shape) position the whole item at the top-left of the item position.
The following image shows the differences between the two approaches; I've used a PNG with transparent areas to better explain the whole concept.
On the top there is the source image, in the middle the paint() override approach, and finally the shape() implementation at the bottom.
While there seems to be no difference between the two methods, as shown on the examples on the left, on the right I've highlighted the actual boundaries of each item, by showing their boundingRect (in blue), shape (in red), which will be used for mouse events, collision detection and paint clipping; the green circle shows the overall circle used for both shape and painting.
The examples in the middle show the positioning based on the original image size, while on the right you can see the absolute positioning based on the effective circle size as explained above.
Drawing a circle around the image
Unfortunately, the ItemClipsToShape flag doesn't support antialiasing for clipping: if we just draw a circle after painting the image the result will be ugly. On the left you can see that the circle is very pixellated and does not overlap perfectly on the image. On the right the correct painting.
To support that, the flag must not be set, and the paint function will be a bit different.
class CircleClipPixmapItem(QtGui.QGraphicsPixmapItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# we don't need this anymore:
# self.setFlag(self.ItemClipsToShape)
# always set the shapeMode to the bounding rect without any masking:
# if the image has transparent areas they will be clickable anyway
self.setShapeMode(self.BoundingRectShape)
self.updateRect()
self.pen = QtGui.QPen(QtCore.Qt.red, 2)
# ...
def setPen(self, pen):
self.pen = pen
self.update()
def paint(self, painter, option, widget):
# we are going to translate the painter to the "reference" position,
# and we are also changing the pen, let's save the state before that
painter.save()
painter.translate(.5, .5)
painter.setRenderHints(painter.Antialiasing)
# another painter save "level"
painter.save()
# apply the clipping to the painter
painter.setClipPath(self._shape)
painter.translate(self._reference)
super().paint(painter, option, widget)
painter.restore()
painter.setPen(self.pen)
# adjust the rectangle to precisely match the circle to the image
painter.drawEllipse(self._boundingRect.adjusted(.5, .5, -.5, -.5))
painter.restore()
# restore the state of the painter

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.

How to get mouse hover events for shapes with a hole in a QGraphicsItem?

I have a QGraphicsPathItem in Qt (using the PySide bindings in Python) where there is a big rectangle and a smaller rectangle inside. Because of the default filling rule (Qt.OddEvenFill) the inner rectangle is transparent. This effectively draws a shape with a hole.
Now I want to listen to mouse events like enter, leave, click, ... My simple approach of implementing hoverEnterEvent, .. of QGraphicsItem does not create mouse events when moving over the hole because the hole is still part of the item even if it is not filled.
I want to have a QGraphicsItem derivative that displays a custom shape whose outline is defined by a QPainterPath or one or several polygons and that can have holes and when the mouse enters a hole this is regarded as outside of the shape.
Example shape with a hole (when the mouse is in the inner rectangle it should be regarded as outside of the shape and mouse leave events should be fired):
However the solution should also work for arbitrary shapes with holes.
Example code in PySide/Python 3.3
from PySide import QtCore, QtGui
class MyPathItem(QtGui.QGraphicsPathItem):
def __init__(self):
super().__init__()
self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, event):
print('inside')
def hoverLeaveEvent(self, event):
print('outside')
app = QtGui.QApplication([])
scene = QtGui.QGraphicsScene()
path = QtGui.QPainterPath()
path.addRect(0, 0, 100, 100)
path.addRect(25, 25, 50, 50)
item = MyPathItem()
item.setPath(path)
item.setBrush(QtGui.QBrush(QtCore.Qt.blue))
scene.addItem(item)
view = QtGui.QGraphicsView(scene)
view.resize(200, 200)
view.show()
app.exec_()
It seems that method shape from QGraphicsItem by default returns the bounding rectangle. Its returned path is used to determine if a position is inside or outside of a complex shape. However in case of a QGraphicsPathItem we already have a path and returning this instead of the bounding rectangle could solve the problem. And to my surprise it does.
Just add these two lines to the QGraphicsPathItem derivative from the question.
def shape(self):
return self.path()
You can extend event handler to check if a given position is the specific (inner) path. Different approach - draw using move/lineTo (maybe ambiguous). For example moveTo/lineTo:
from PySide import QtCore, QtGui
class MyPathItem(QtGui.QGraphicsPathItem):
def __init__(self):
QtGui.QGraphicsPathItem.__init__(self)
self.setAcceptHoverEvents(True)
def hoverEnterEvent(self, event):
print('inside')
def hoverLeaveEvent(self, event):
print('outside')
app = QtGui.QApplication([])
scene = QtGui.QGraphicsScene()
path = QtGui.QPainterPath()
path.moveTo(0, 0)
path.lineTo(100, 0)
path.moveTo(0, 0)
path.lineTo(0, 100)
path.moveTo(100, 100)
path.lineTo(0, 100)
path.moveTo(100, 0)
path.lineTo(100, 100)
item = MyPathItem()
pen = QtGui.QPen()
pen.setWidth(25)
pen.setColor(QtCore.Qt.blue)
item.setPen(pen)
item.setPath(path)
scene.addItem(item)
view = QtGui.QGraphicsView(scene)
view.resize(200, 200)
view.show()
app.exec_()

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