I am trying to create a simple drawing application using Python, GTK3 and cairo. The tool should have different brushes and some kind of a highlighter pen.
I figured I can use the alpha property of the stroke to create it. However,
the connecting points are created overlapping and that creates a weird effect.
Here is the code responsible for this red brush and the highlighter mode:
def draw_brush(widget, x, y, odata, width=2.5, r=1, g=0, b=0, alpha=1):
cr = cairo.Context(widget.surface)
cr.set_source_rgba(r, g, b, alpha)
cr.set_line_width(width)
cr.set_line_cap(1)
cr.set_line_join(0)
for stroke in odata:
for i, point in enumerate(stroke):
if len(stroke) == 1:
radius = 2
cr.arc(point['x'], point['y'], radius, 0, 2.0 * math.pi)
cr.fill()
cr.stroke()
elif i != 0:
cr.move_to(stroke[i - 1]['x'], stroke[i - 1]['y'])
cr.line_to(point['x'], point['y'])
cr.stroke()
cr.save()
The code that draws on mouse click:
def motion_notify_event_cb(self, widget, event):
point = {'x': event.x, 'y': event.y, 'time': time.time()}
if self.odata:
self.odata[-1].append(point)
if widget.surface is None:
return False
if event.state & Gdk.EventMask.BUTTON_PRESS_MASK:
if self.buttons['current'] == 'freehand':
draw_brush(widget, event.x, event.y, self.odata)
if self.buttons['current'] == 'highlight':
draw_brush(widget, event.x, event.y, self.odata, width=12.5,
r=220/255, g=240/255, b=90/255, alpha=0.10)
widget.queue_draw()
return True
Can someone point out a way to prevent the overlapping points in this curve?
Update
Uli's solution seems to offer a partial remedy, but the stroke is still not good looking, it seems that it's redrawn over and over:
Update with partially working code
I still have not succeeded in creating a highlighter pen with cairo.
The closest I can get is in the following gist.
The application shutter, has a similar functionality but it's written in Perl on top of the libgoocanvas which is not maintained anymore.
I hope a bounty here will change the situation ...
update
available operators (Linux, GTK+3):
In [3]: [item for item in dir(cairo) if item.startswith("OPERATOR")]
Out[3]:
['OPERATOR_ADD',
'OPERATOR_ATOP',
'OPERATOR_CLEAR',
'OPERATOR_DEST',
'OPERATOR_DEST_ATOP',
'OPERATOR_DEST_IN',
'OPERATOR_DEST_OUT',
'OPERATOR_DEST_OVER',
'OPERATOR_IN',
'OPERATOR_OUT',
'OPERATOR_OVER',
'OPERATOR_SATURATE',
'OPERATOR_SOURCE',
'OPERATOR_XOR']
First, sorry for causing all of that confusion in the comments to your question. It turns out that I was complicating the problem for (partially) no reason! Here is my (heavily-modified) code:
#!/usr/bin/python
from __future__ import division
import math
import time
import cairo
import gi; gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
from gi.repository.GdkPixbuf import Pixbuf
import random
class Brush(object):
def __init__(self, width, rgba_color):
self.width = width
self.rgba_color = rgba_color
self.stroke = []
def add_point(self, point):
self.stroke.append(point)
class Canvas(object):
def __init__(self):
self.draw_area = self.init_draw_area()
self.brushes = []
def draw(self, widget, cr):
da = widget
cr.set_source_rgba(0, 0, 0, 1)
cr.paint()
#cr.set_operator(cairo.OPERATOR_SOURCE)#gets rid over overlap, but problematic with multiple colors
for brush in self.brushes:
cr.set_source_rgba(*brush.rgba_color)
cr.set_line_width(brush.width)
cr.set_line_cap(1)
cr.set_line_join(cairo.LINE_JOIN_ROUND)
cr.new_path()
for x, y in brush.stroke:
cr.line_to(x, y)
cr.stroke()
def init_draw_area(self):
draw_area = Gtk.DrawingArea()
draw_area.connect('draw', self.draw)
draw_area.connect('motion-notify-event', self.mouse_move)
draw_area.connect('button-press-event', self.mouse_press)
draw_area.connect('button-release-event', self.mouse_release)
draw_area.set_events(draw_area.get_events() |
Gdk.EventMask.BUTTON_PRESS_MASK |
Gdk.EventMask.POINTER_MOTION_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK)
return draw_area
def mouse_move(self, widget, event):
if event.state & Gdk.EventMask.BUTTON_PRESS_MASK:
curr_brush = self.brushes[-1]
curr_brush.add_point((event.x, event.y))
widget.queue_draw()
def mouse_press(self, widget, event):
if event.button == Gdk.BUTTON_PRIMARY:
rgba_color = (random.random(), random.random(), random.random(), 0.5)
brush = Brush(12, rgba_color)
brush.add_point((event.x, event.y))
self.brushes.append(brush)
widget.queue_draw()
elif event.button == Gdk.BUTTON_SECONDARY:
self.brushes = []
def mouse_release(self, widget, event):
widget.queue_draw()
class DrawingApp(object):
def __init__(self, width, height):
self.width = width
self.height = height
self.window = Gtk.Window()
self.window.set_border_width(8)
self.window.set_default_size(self.width, self.height)
self.window.connect('destroy', self.close)
self.box = Gtk.Box(spacing=6)
self.window.add(self.box)
self.canvas = Canvas()
self.box.pack_start(self.canvas.draw_area, True, True, 0)
self.window.show_all()
def close(self, window):
Gtk.main_quit()
if __name__ == "__main__":
DrawingApp(400, 400)
Gtk.main()
Here are the list of changes I made:
Replaced the inheritance in your code with a composition-based approach. That is, instead of inheriting from Gtk.Window or Gtk.DrawingArea, I created Brush, Canvas, and DrawingApp objects that contain these Gtk elements. The idea of this is to allow more flexibility in creating relevant classes to our application and hides all of the nasty Gtk internals as much as possible in setup functions. Hopefully this makes the code a bit clearer. I have no idea why all the tutorials for Gtk insist on using inheritance.
Speaking of the Brush class, there is now a Brush class! Its purpose is simple: it just contains information about the coordinates draw for a given stroke, its line width, and its color. A list of brush strokes making the drawing is stored as a property of DrawingApp. This is convenient because...
... all of the rendering is contained within the draw function of the Canvas class! All this does is draw the black screen, followed by rendering the brush strokes one by one as individual paths to the screen. This solves the problem with the code provided by #UliSchlachter. While the idea of a single connected path was right (and I used that here), all of the iterations of that path were being accumulated and drawn on top of each other. This explains your update image, where the start of each stroke was more opaque due to accumulating the most incomplete strokes.
For the sake of color variety, I made the app generate random highlighter colors every time you click with the left mouse button!
Note that the last point illustrates an issue with the blending. Try drawing multiple overlapping strokes and see what happens! You will find that the more overlaps there are, the more opaque it gets. You can use the cairo.OPERATOR_SOURCE setting to counteract this, but I don't think this is an ideal solution as I believe it overwrites the content underneath. Let me know if this solution is fine or if this also needs to be corrected. Here is a picture of the final result, for your reference:
Hope this helps!
Each move_to() creates a new sub-path that is drawn separately. What you want is a single, connected path.
As far as I know, cairo turns a line_to()-call into a move_to() if there is no current point yet, so the following should work:
def draw_brush(widget, x, y, odata, width=2.5, r=1, g=0, b=0, alpha=1):
cr = cairo.Context(widget.surface)
cr.set_source_rgba(r, g, b, alpha)
cr.set_line_width(width)
cr.set_line_cap(1)
cr.set_line_join(0)
for stroke in odata:
cr.new_path()
for i, point in enumerate(stroke):
if len(stroke) == 1:
radius = 2
cr.arc(point['x'], point['y'], radius, 0, 2.0 * math.pi)
cr.fill()
else:
cr.line_to(point['x'], point['y'])
cr.stroke()
cr.save() # What's this for?
Note that I removed the cr.stroke() after the cr.fill(), because it doesn't do anything. The fill just cleared the path, so there is nothing to stroke.
Related
I have to generate two turtle windows and draw in each one, so I'm using tkinter to create and show the windows. My code currently opens the right screen and draws in it, but the turtle is really slow so I want to set the turtle tracer to false to use the update function, but I can't figure out how to.
This is my turtle_interpreter.py file, which has all the functions I use to draw the L-system:
import turtle
from tkinter import *
class Window(Tk):
def __init__(self, title, geometry):
super().__init__()
self.running = True
self.geometry(geometry)
self.title(title)
self.protocol("WM_DELETE_WINDOW", self.destroy_window)
self.canvas = Canvas(self)
self.canvas.pack(side=LEFT, expand=True, fill=BOTH)
self.turtle = turtle.RawTurtle(turtle.TurtleScreen(self.canvas))
def update_window(self):
'''
sets window to update
'''
if self.running:
self.update()
def destroy_window(self):
'''
sets window to close
'''
self.running = False
self.destroy()
def drawString(turt, dstring, distance, angle):
'''Interpret the characters in string dstring as a series
of turtle commands. Distance specifies the distance
to travel for each forward command. Angle specifies the
angle (in degrees) for each right or left command. The list
of turtle supported turtle commands is:
F : forward
- : turn right
+ : turn left
'''
for char in dstring:
if char == 'F':
turt.forward(distance)
elif char == '-':
turt.right(angle)
elif char == '+':
turt.left(angle)
def place(turt, xpos, ypos, angle=None):
'''
places turtle at given coordinates and angle
'''
turt.penup()
turt.goto(xpos, ypos)
if angle != None:
turt.setheading(angle)
turt.pendown()
def goto(turt, xpos, ypos):
'''
moves turtle to given coordinates
'''
turt.penup()
turt.goto(xpos, ypos)
turt.pendown()
def setColor(turt, color):
'''
sets turtle color
'''
turt.color(color)
And this is the file where the functions get called. Running it draws the L-system.
import turtle_interpreter as turt_int
import lsystem_scene_three as lsystem
def turtle_scene_two():
'''
generates scene two
'''
# create window
win_two = turt_int.Window('Turtle Scene 2', '640x480+650+0')
# assign turtle
turt2 = win_two.turtle
# lsystem setup
lsystemFile = lsystem.Lsystem('lsystem_scene_two.txt')
tstr = lsystemFile.buildString(4)
# draw stuff
turt_int.setColor(turt2, (0, 0, 0))
turt_int.place(turt2, 0, -200, 90)
turt_int.drawString(turt2, tstr, 4, 90)
# update window (loop)
while win_two.running:
win_two.update_window()
turtle_scene_two()
Hope this makes sense. Let me know if it doesn't.
Appreciate your help!
Tried a few things but nothing was promising. Calling turtle generates another screen (which I don't want).
Since you didn't provide all your code, I can't test this, so I'm guessing a good start would be changing this:
self.turtle = turtle.RawTurtle(turtle.TurtleScreen(self.canvas))
to something like:
screen = turtle.TurtleScreen(self.canvas)
screen.tracer(False)
self.turtle = turtle.RawTurtle(screen)
I coded a game using pylet. It uses a static window with width 1600 and height 900 assuming users have a fullHD display so everything will be visible. However on some devices (with small displays) the window is way bigger as expected. I figured out that the pixel_ratio is set up (for example to 2.0) making each virtual pixel to be displayed double size (2x2) in physical pixel.
I want to prevent this behavior but can't figure out how, I know I can get the pixel ratio easily by get_pixel_ratio() but I actually don't know how to set them or prevent pyglet from automatically setting them.
I also tried to use glViewport which seemed to have an effect but it didn't worked the way I wanted.
So how can I change the pixel_ratio or prevent changing it automatically.
Asked around in the official discord server for information, as I tried to reproduce the issue myself with some code, and this is what I used to test it:
import math
from pyglet import *
from pyglet.gl import *
key = pyglet.window.key
class main(pyglet.window.Window):
def __init__ (self, width=800, height=600, fps=False, *args, **kwargs):
super(main, self).__init__(width, height, *args, **kwargs)
self.x, self.y = 0, 0
self.keys = {}
verts = []
for i in range(30):
angle = math.radians(float(i)/30 * 360.0)
x = 100*math.cos(angle) + 300
y = 100*math.sin(angle) + 200
verts += [x,y]
self.pixel_ratio = 100
self.circle = pyglet.graphics.vertex_list(30, ('v2f', verts))
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_release(self, symbol, modifiers):
try:
del self.keys[symbol]
except:
pass
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
self.keys[symbol] = True
def render(self):
self.clear()
glClear(pyglet.gl.GL_COLOR_BUFFER_BIT)
glColor3f(1,1,0)
self.circle.draw(GL_LINE_LOOP)
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
if __name__ == '__main__':
x = main()
x.run()
Not that the code mattered heh, since the variable pixel_ratio is just, and I quote: "indicating that the actual number of pixels in the window is larger than the size of the Window created."
This is something OSX does to cope with the high DPI, using this information you should be able to scale your graphics accordingly. Window.get_framebuffer_size() will show you the difference in requested Window size and Framebuffer size, if any.
So your only way to actually scale up, would be to use glScale or if you're using sprites you can use Sprite.scale to scale the image-data. If you're using 2D graphics I'd go with the sprite option as it's pretty easy to work with.
I'm working on a GUI in Python with PySide2. I have a GraphicsView, where I'll put an image, and I'd like to draw and move a polygon around on that image. I've found many examples of simply drawing polygons, circles, etc. in PySide, PySide2, or PyQt 4/5 in Python. However, I haven't been able to figure out why my graphics items do not move on an event without deleting and redrawing.
I'm using the keyboard to change the X value on a PySide2 QRectF. The X value is clearly changing, but the rectangle does not actually move.
Here is a minimal example:
from PySide2 import QtCore, QtGui, QtWidgets
from functools import partial
class DebuggingDrawing(QtWidgets.QGraphicsView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# initialize the scene and set the size
self._scene = QtWidgets.QGraphicsScene(self)
self._scene.setSceneRect(0,0,500,500)
self.setScene(self._scene)
# make a green pen and draw a 10 wide, 20 high rectangle at x=20, y=30
self.pen = QtGui.QPen(QtCore.Qt.green, 0)
self.draw_rect = QtCore.QRectF(20, 30, 10, 20)
# add the rectangle to our scene
self._scene.addRect(self.draw_rect, self.pen)
def move_rect(self, dx: int):
# method for moving the existing rectangle
# get the x value
x = self.draw_rect.x()
print('x: {} dx: {}'.format(x, dx))
# use the moveLeft method of QRectF to change the rectangle's left side x value
self.draw_rect.moveLeft(x + dx)
self.update()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.labelImg = DebuggingDrawing()
# Get a keyboard shortcut and hook it up to the move_rect method
next_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence('Right'), self)
next_shortcut.activated.connect(partial(self.labelImg.move_rect, 1))
# get the left key shortcut, move_rect one pixel left
back_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence('Left'), self)
back_shortcut.activated.connect(partial(self.labelImg.move_rect, -1))
self.setCentralWidget(self.labelImg)
self.setMaximumHeight(480)
self.update()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
testing = MainWindow()
testing.show()
app.exec_()
Here's what the output looks like:
You clearly can't see in the image, but even though the rectangle's x value is changing according to our print calls, nothing moves around in the image. I've confirmed it's not just my eyes, because if I draw new rectangles in move_rect, they clearly show up.
draw_rect is a QRectF is an input to create an item(QGraphicsRectItem) that is returned by the addRect() method similar to pen, that is, it takes the information but then no longer uses it. The idea is to move the item using setPos():
class DebuggingDrawing(QtWidgets.QGraphicsView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# initialize the scene and set the size
self._scene = QtWidgets.QGraphicsScene(self)
self._scene.setSceneRect(0, 0, 500, 500)
self.setScene(self._scene)
# make a green pen and draw a 10 wide, 20 high rectangle at x=20, y=30
pen = QtGui.QPen(QtCore.Qt.green, 0)
draw_rect = QtCore.QRectF(20, 30, 10, 20)
# add the rectangle to our scene
self.item_rect = self._scene.addRect(draw_rect, pen)
def move_rect(self, dx: int):
p = self.item_rect.pos()
p += QtCore.QPointF(dx, 0)
self.item_rect.setPos(p)
If you still want to use draw_rect then you have to set it again in the item:
self.pen = QtGui.QPen(QtCore.Qt.green, 0)
self.draw_rect = QtCore.QRectF(20, 30, 10, 20)
# add the rectangle to our scene
self.item_rect = self._scene.addRect(self.draw_rect, self.pen)
def move_rect(self, dx: int):
# method for moving the existing rectangle
# get the x value
x = self.draw_rect.x()
print('x: {} dx: {}'.format(x, dx))
# use the moveLeft method of QRectF to change the rectangle's left side x value
self.draw_rect.moveLeft(x + dx)
self.item_rect.setRect(self.draw_rect)
It is recommended that "Graphics View Framework" be read so that the QGraphicsItems, QGraphicsView and QGraphicsScene work.
I have user-adjustable annotations in a graphics scene. The size/rotation of annotations is handled by dragging corners of a rectangle about the annotation. I'm using a custom rect (instead of the boundingRect) so it follows the rotation of the parent annotation. The control corners are marked by two ellipses whose parent is the rect so transformations of rect/ellipse/annotation are seamless.
I want to detect when the cursor is over one of the corners, which corner it is, and the exact coordinates. For this task it seems that I should filter the hoverevents with the parent rect using a sceneEventFilter.
I've tried umpty zilch ways of implementing the sceneEventFilter to no avail. All events go directly to the hoverEnterEvent function. I've only found a few bits of example code that do something like this but I'm just plain stuck. btw, I'm totally self taught on Python and QT over the past 3 months, so please bear with me. I'm sure I'm missing something very basic. The code is a simplified gui with two ellipses. We're looking to capture events in the sceneEventFilter but always goes to hoverEnterEvent.
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsItem
import sys
class myHandle(QtGui.QGraphicsEllipseItem):
def __init__(self, parent = None):
super(myHandle, self).__init__(parent)
def addTheHandle(self, h_parent = 'null', kind = 'null'):
handle_w = 40
if kind == 'scaling handle':
handle_x = h_parent.boundingRect().topRight().x() - handle_w/2
handle_y = h_parent.boundingRect().topRight().y() - handle_w/2
if kind == 'rotation handle':
handle_x = h_parent.boundingRect().topLeft().x() - handle_w/2
handle_y = h_parent.boundingRect().topLeft().y() - handle_w/2
the_handle = QtGui.QGraphicsEllipseItem(QtCore.QRectF(handle_x, handle_y, handle_w, handle_w))
the_handle.setPen(QtGui.QPen(QtGui.QColor(255, 100, 0), 3))
the_handle.setParentItem(h_parent)
the_handle.setAcceptHoverEvents(True)
the_handle.kind = kind
return the_handle
class myRect(QtGui.QGraphicsRectItem):
def __init__(self, parent = None):
super(myRect, self).__init__(parent)
def rectThing(self, boundingrectangle):
self.setAcceptHoverEvents(True)
self.setRect(boundingrectangle)
mh = myHandle()
rotation_handle = mh.addTheHandle(h_parent = self, kind = 'rotation handle')
scaling_handle = mh.addTheHandle(h_parent = self, kind = 'scaling handle')
self.installSceneEventFilter(rotation_handle)
self.installSceneEventFilter(scaling_handle)
return self, rotation_handle, scaling_handle
def sceneEventFilter(self, event):
print('scene ev filter')
return False
def hoverEnterEvent(self, event):
print('hover enter event')
class Basic(QtGui.QMainWindow):
def __init__(self):
super(Basic, self).__init__()
self.initUI()
def eventFilter(self, source, event):
return QtGui.QMainWindow.eventFilter(self, source, event)
def exit_the_program(self):
pg.exit()
def initUI(self):
self.resize(300, 300)
self.centralwidget = QtGui.QWidget()
self.setCentralWidget(self.centralwidget)
self.h_layout = QtGui.QHBoxLayout(self.centralwidget)
self.exit_program = QtGui.QPushButton('Exit')
self.exit_program.clicked.connect(self.exit_the_program)
self.h_layout.addWidget(self.exit_program)
self.this_scene = QGraphicsScene()
self.this_view = QGraphicsView(self.this_scene)
self.this_view.setMouseTracking(True)
self.this_view.viewport().installEventFilter(self)
self.h_layout.addWidget(self.this_view)
self.circle = self.this_scene.addEllipse(QtCore.QRectF(40, 40, 65, 65), QtGui.QPen(QtCore.Qt.black))
mr = myRect()
the_rect, rotation_handle, scaling_handle = mr.rectThing(self.circle.boundingRect())
the_rect.setPen(QtGui.QPen(QtCore.Qt.black))
the_rect.setParentItem(self.circle)
self.this_scene.addItem(the_rect)
self.this_scene.addItem(rotation_handle)
self.this_scene.addItem(scaling_handle)
def main():
app = QtGui.QApplication([])
main = Basic()
main.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
The main problem is that you are installing the event filter of the target items on the rectangle: the event filter of the rectangle will never receive anything. Moreover, sceneEventFilter accepts two arguments (the watched item and the event), but you only used one.
What you should do is to install the event filter of the rectangle on the target items:
rotation_handle.installSceneEventFilter(self)
scaling_handle.installSceneEventFilter(self)
That said, if you want to use those ellipse items for scaling or rotation of the source circle, your approach is a bit wrong to begin with.
from math import sqrt
# ...
class myRect(QtGui.QGraphicsRectItem):
def __init__(self, parent):
super(myRect, self).__init__(parent)
self.setRect(parent.boundingRect())
# rotation is usually based on the center of an object
self.parentItem().setTransformOriginPoint(self.parentItem().rect().center())
# a rectangle that has a center at (0, 0)
handleRect = QtCore.QRectF(-20, -20, 40, 40)
self.rotation_handle = QtGui.QGraphicsEllipseItem(handleRect, self)
self.scaling_handle = QtGui.QGraphicsEllipseItem(handleRect, self)
# position the handles by centering them at the right corners
self.rotation_handle.setPos(self.rect().topLeft())
self.scaling_handle.setPos(self.rect().topRight())
for source in (self.rotation_handle, self.scaling_handle):
# install the *self* event filter on the handles
source.installSceneEventFilter(self)
source.setPen(QtGui.QPen(QtGui.QColor(255, 100, 0), 3))
def sceneEventFilter(self, source, event):
if event.type() == QtCore.QEvent.GraphicsSceneMouseMove:
# map the handle event position to the ellipse parent item; we could
# also map to "self", but using the parent is more consistent
localPos = self.parentItem().mapFromItem(source, event.pos())
if source == self.rotation_handle:
# create a temporary line to get the rotation angle
line = QtCore.QLineF(self.boundingRect().center(), localPos)
# add the current rotation to the angle between the center and the
# top left corner, then subtract the new line angle
self.parentItem().setRotation(135 + self.parentItem().rotation() - line.angle())
# note that I'm assuming that the ellipse is a circle, so the top
# left angle will always be at 135°; if it's not a circle, the
# rect width and height won't match and the angle will be
# different, so you'll need to compute that
# parentRect = self.parentItem().rect()
# oldLine = QtCore.QLineF(parentRect.center(), parentRect.topLeft())
# self.parentItem().setRotation(
# oldLine.angle() + self.parentItem().rotation() - line.angle())
elif source == self.scaling_handle:
# still assuming a perfect circle, so the rectangle is a square;
# the line from the center to the top right corner is used to
# compute the square side size, which is the double of a
# right-triangle cathetus where the hypotenuse is the line
# between the center and any of its corners;
# if the ellipse is not a perfect circle, you'll have to
# compute both of the catheti
hyp = QtCore.QLineF(self.boundingRect().center(), localPos)
size = sqrt(2) * hyp.length()
rect = QtCore.QRectF(0, 0, size, size)
rect.moveCenter(self.rect().center())
self.parentItem().setRect(rect)
self.setRect(rect)
# update the positions of both handles
self.rotation_handle.setPos(self.rect().topLeft())
self.scaling_handle.setPos(self.rect().topRight())
return True
elif event.type() == QtCore.QEvent.GraphicsSceneMousePress:
# return True to the press event (which is almost as setting it as
# accepted, so that it won't be processed further more by the scene,
# allowing the sceneEventFilter to capture the following mouseMove
# events that the watched graphics items will receive
return True
return super(myRect, self).sceneEventFilter(source, event)
class Basic(QtGui.QMainWindow):
# ...
def initUI(self):
# ...
self.circle = self.this_scene.addEllipse(QtCore.QRectF(40, 40, 65, 65), QtGui.QPen(QtCore.Qt.black))
mr = myRect(self.circle)
self.this_scene.addItem(mr)
I've been trying to draw many 'rects' efficiently in Qt (PySide), yet it still appears to lag drawing the entire 'grid' in the paint call of a QGraphicsItem.
class GridMapView(QObject, QGraphicsItem):
def __init__(self, gridMap, mapWidth, mapHeight, cellSize):
QObject.__init__(self)
QGraphicsItem.__init__(self)
self.setCacheMode(QGraphicsItem.ItemCoordinateCache)
self.gridMap = gridMap
self.cellSize = cellSize
self.width = mapWidth
self.height = mapHeight
self.setPos(-self.width/2, -self.height/2)
def boundingRect(self):
return QRectF(0, 0, self.width, self.height)
def paint(self, painter, option, widget):
painter.setPen(Qt.NoPen)
unknownBrush = QBrush(QColor('grey'))
freeBrush = QBrush(QColor('white'))
occupiedBrush = QBrush(QColor('black'))
cellRect = QRectF()
for ix, col in enumerate(self.gridMap):
for iy, cell in enumerate(col):
if cell == CellStates.UNKNOWN:
painter.setBrush(unknownBrush)
elif cell == CellStates.FREE:
painter.setBrush(freeBrush)
elif cell == CellStates.OCCUPIED:
painter.setBrush(occupiedBrush)
cellRect.setRect(ix*self.cellSize, iy*self.cellSize, self.cellSize, self.cellSize)
painter.drawRect(cellRect)
This is rendering a few thousand rects, and lags a lot. Setting the cache mode (and making sure not to move the view) doesn't appear to help.
My assumption was that painting the entire grid in one pass would be efficient, if it only gets redrawn when one cell changes.
Am I missing something fundamental here? Thanks.
Ensure you set QGraphicsView.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate) on the view containing the scene with that item. In either case, when you pass (hover) your mouse accross a GridMapView item, its paint() is called, so all the squares are repainted in your case.
An alternative implementation would be to make each square an individual QGraphicsItem so that, provided above viewport update mode is set on the view, only those items are repainted that need be.