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.
I recently created a program that will create QgraphicsEllipseItems whenever the mouse is clicked. That part works! However, it's not in the exact place where my cursor is. It seems to be slightly higher than where my mouse cursor is. I do have a QGraphicsRectItem created so maybe the two items are clashing with each other and moving off of one another? How can I get these circles to be placed on top of the rectangle item? Here's the code
class MyView(QtGui.QGraphicsView):
def __init__(self):
QtGui.QGraphicsView.__init__(self)
self.scene = QtGui.QGraphicsScene(self)
self.item = QtGui.QGraphicsRectItem(400, 400, 400, 400)
self.scene.addItem(self.item)
self.setScene(self.scene)
def paintMarkers(self):
self.cursor = QtGui.QCursor()
self.x = self.cursor.pos().x()
self.y = self.cursor.pos().y()
self.circleItem = QtGui.QGraphicsEllipseItem(self.x,self.y,10,10)
self.scene.addItem(self.circleItem)
self.circleItem.setPen(QtGui.QPen(QtCore.Qt.red, 1.5))
self.setScene(self.scene)
class Window(QtGui.QMainWindow):
def __init__(self):
#This initializes the main window or form
super(Window,self).__init__()
self.setGeometry(50,50,1000,1000)
self.setWindowTitle("Pre-Alignment system")
self.view = MyView()
self.setCentralWidget(self.view)
def mousePressEvent(self,QMouseEvent):
self.view.paintMarkers()
Much thanks!
There are two issues with the coordinates you are using to place the QGraphics...Items. The first is that the coordinates from QCursor are global screen coordinates, so you need to use self.mapFromGlobal() to convert them to coordinates relative to the QGraphicsView.
Secondly, you actually want the coordinates relative to the current QGraphicsScene, as this is where you are drawing the item. This is because the scene can be offset from the view (for example panning around a scene that is bigger than a view). To do this, you use self.mapToScene() on the coordinates relative to the QGraphicsView.
I would point out that typically you would draw something on the QGraphicsScene in response to some sort of mouse event in the QGraphicsView, which requires reimplementing things like QGraphicsView.mouseMoveEvent or QGraphicsView.mousePressEvent. These event handlers are passed a QEvent which contains the mouse coordinates relative to the view, and so you don't need to do the global coordinates transformation I mentioned in the first paragraph in these cases.
Update
I've just seen your other question, and now understand some of the issue a bit better. You shouldn't be overriding the mouse event in the main window. Instead override it in the view. For example:
class MyView(QtGui.QGraphicsView):
def __init__(self):
QtGui.QGraphicsView.__init__(self)
self.scene = QtGui.QGraphicsScene(self)
self.item = QtGui.QGraphicsRectItem(400, 400, 400, 400)
self.scene.addItem(self.item)
self.setScene(self.scene)
def paintMarkers(self, event):
# event position is in coordinates relative to the view
# so convert them to scene coordinates
p = self.mapToScene(event.x(), event.y())
self.circleItem = QtGui.QGraphicsEllipseItem(0,0,10,10)
self.circleItem.setPos(p.x()-self.circleItem.boundingRect().width()/2.0,
p.y()-self.circleItem.boundingRect().height()/2.0)
self.scene.addItem(self.circleItem)
self.circleItem.setPen(QtGui.QPen(QtCore.Qt.red, 1.5))
# self.setScene(self.scene) # <-- this line should not be needed here
# Note, I've renamed the second argument `event`. Otherwise you locally override the QMouseEvent class
def mousePressEvent(self, event):
self.paintMarkers(event)
# you may want to preserve the default mouse press behaviour,
# in which case call the following
return QGraphicsView.mousePressEvent(self, event)
Here we have not needed to use QWidget.mapFromGlobal() (what I covered in the first paragraph) because we use a mouse event from the QGraphicsView which returns coordinates relative to that widget only.
Update 2
Note: I've updated how the item is created/placed in the above code based on this answer.
I am using python 2.7 and the most up to date version of wxpython for it (3.0).
I am trying to draw a rectangle that is not filled. When I run the code below, it draws a red rectangle with the proper thickness, but the inside is white and not my background color (black).
def __init__(self, parent):
wx.Frame.__init__(self, parent, title="Maze")
self.SetBackgroundColour('black')
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnPaint(self, event=None):
self.dc = wx.PaintDC(self)
self.dc.Clear()
self.dc.SetPen(wx.Pen(wx.RED, 2))
self.dc.DrawRectangle(50, 50, 50, 50)
I don't really understand why I am having this problem and any help would be greatly appreciated. I apologize if this is a trivial answer, as I am new to wxpython.
DrawRectangle is drawed with the current pen and filled with the current brush. So you must also call self.dc.SetBrush.
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.
I am using QPainter within a QWidget to draw a bunch of ellipses on a black background as follows:
paint = QPainter()
paint.begin(self)
paint.setBrush(Qt.black)
paint.drawRect(event.rect())
brush = ...
paint.setBrush(brush)
paint.drawEllipse(center, rad, rad)
After a bunch of ellipses were drawn, and then I want to detect a mouse click on one of such an existing ellipse. I did not find any obvious in the documentation for QPainter.
In case there is something else to be used instead of QPainter, please provide an example that shows my above example in the other framework.
You will need to detect the custom area yourself as follows:
def mousePressEvent(self, event):
''' You will have to implement the contain algorithm yourself'''
if sel.fo(even.pos()):
self.myMethod()
QGraphicsEllipseItem.contains()
Alternatively, you could look into the QGraphicsEllipseItem because it has the contains-logic implemented and offered.
def mousePressEvent(self, event):
if self.contains(event.pos()):
self.myMethod()
and you create your object with the corresponding parameters:
scene = QGraphicsScene()
ellipseItem = MyGraphicsEllipseItem(centerx, centery, rad, rad)
scene.addItem(ellipseItem)
view = QGraphicsView(scene)
view.show()
scene.setBackgroundBrush(Qt.black)