3
votes

I'm working on an application written in Cairo + Gtk. Please note that, due to retrocompatibility issues, I am forced to use Python as programming language, PyGTK as wrapper, and GTK libraries, v.2.24. No chance to use C/C++ and/or GTK3!

My app need to (re)draw a big amount of data on screen for each invocation of expose method(obviously).

I would simply give to users a chance to manually select some objects previously drawn with Cairo. Since I'm drawing on gtk.DrawingAreas, it seems that I must manually implement the rubber band selection functionality.

This is the question:

Is there any way to redraw the rubber band rectangle at every mouse move, avoiding to redraw all others objects on screen?

I would redraw only the selection rectangle for performance reasons.

Due to the big amount of graphical objects, my GUI is terribly slow. Unfortunately, despite several attempts, I have no choice: redraw all or redraw anything!

First thing that came in my mind: is there any way to overlay an intermediate level between the DrawingArea with most of data and the mouse cursor? by calling queue_draw_area() function there aren't performance gains.

A simple, self containing code example below: obviously, in this case Ionly use cairo to draw extremely simple graphic objects.

import gtk
from gtk import gdk

class Canvas(gtk.DrawingArea):

    # the list of points is passed as argument

    def __init__(self, points):
        super(Canvas, self).__init__()
        self.points = points
        self.set_size_request(400, 400)

        # Coordinates of the left-top angle of the selection rect
        self.startPoint = None

        self.endPoint = None

        # Pixmap to drawing rubber band selection
        self.pixmap = None

        self.connect("expose_event", self.expose_handler)
        self.connect("motion_notify_event", self.mouseMove_handler)
        self.set_events(gtk.gdk.EXPOSURE_MASK
                            | gtk.gdk.LEAVE_NOTIFY_MASK
                            | gtk.gdk.BUTTON_PRESS_MASK
                            | gtk.gdk.POINTER_MOTION_MASK
                            | gtk.gdk.POINTER_MOTION_HINT_MASK)

    # Method to paint lines and/or rubberband on screen

    def expose_handler(self, widget, event):

        rect = widget.get_allocation()
        w = rect.width
        h = rect.height
        ctx = widget.window.cairo_create()
        ctx.set_line_width(7)
        ctx.set_source_rgb(255, 0, 0)
        ctx.save()

        for i in range(0, len(self.points)):
            currPoint = self.points[i]
            currX = float(currPoint[0])
            currY = float(currPoint[1])
            nextIndex = i + 1
            if (nextIndex == len(self.points)):
                continue
            nextPoint = self.points[nextIndex]
            nextX = float(nextPoint[0])
            nextY = float(nextPoint[1])
            ctx.move_to(currX, currY)
            ctx.line_to(nextX, nextY)
        ctx.restore()
        ctx.close_path()
        ctx.stroke()

        # rubber band
        if self.pixmap != None:
            width = self.endPoint[0] - self.startPoint[0]
            height = self.endPoint[1] - self.startPoint[1]
            if width < 0 or height < 0:
                tempEndPoint = self.endPoint
                self.endPoint = self.startPoint
                self.startPoint = tempEndPoint

            height = self.endPoint[1] - self.startPoint[1]
            width = self.endPoint[0] - self.startPoint[0]
            widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL], self.pixmap, self.startPoint[0], self.startPoint[1], self.startPoint[0], self.startPoint[1], abs(width), abs(height))

    def mouseMove_handler(self, widget, event):
        x, y, state = event.window.get_pointer()
        if (state & gtk.gdk.BUTTON1_MASK):
            if (state & gtk.gdk.CONTROL_MASK):
                if self.startPoint == None:
                    self.startPoint = (x,y)
                    self.endPoint = (x,y)
                else:
                    self.endPoint = (x,y)
                tempPixmap = gtk.gdk.Pixmap(widget.window, 400, 400)

                height = self.endPoint[1] - self.startPoint[1]
                width = self.endPoint[0] - self.startPoint[0]

                gc = self.window.new_gc()
                gc.set_foreground(self.get_colormap().alloc_color("#FF8000"))

                gc.fill = gtk.gdk.STIPPLED
                tempPixmap.draw_rectangle(gc, True, self.startPoint[0], self.startPoint[1], abs(width), abs(height))

                self.pixmap = tempPixmap
                # widget.queue_draw_area()
                widget.queue_draw()
        else:
            if (self.pixmap != None):
                self.pixmap = None

                # ...do something...

                # widget.queue_draw_area(self.startPoint[0], self.startPoint[1], )
                widget.queue_draw()


li1 = [(20,20), (380,20), (380,380), (20,380)]

window = gtk.Window()
canvas = Canvas(li1)
window.add(canvas)
window.connect("destroy", lambda w: gtk.main_quit())
window.show_all()
gtk.main()

Thanks!

IT

2

2 Answers

2
votes

Some general tips when drawing with Gtk+/Cairo:

  • Draw only as much as neccessary. The idea is to keep track of the areas that have changed, and redraw only those. Gdk does this in parts automatically for you. When it calls expose (in Gtk3 draw), it applies a clipping mask so only the "invalidated" pixels are changed by your drawings.
  • You can tell Gdk which areas should be redrawn with GdkWindow.invalidate_rect. Right now your call to widget.queue_draw invalidates the whole window, so you draw too many pixels.
  • If you have complex elements that are in a non-invalidated area, you'd still draw / calculate them in expose - they'd just not make it to the screen. For this, you can check event.area (a GdkRectangle). If your elements dont intersect this area, you don't have to bother drawing them, as those pixels are clipped anyway.
    • Here lies a trade-off. Sometimes it saves a lot if you calculate whether an element is visible or not. Sometimes it's faster just to draw a few extra pixels if it saves a lot of geometry calculations. You have to decide case by case.
  • Expose may be called once for each invalidate_rect, but it's also possible that Gdk recognizes when you invalidate several small/overlapping rects, and just calls it once with a larger rect.
  • You shouldn't mix 'raw' Gdk and cairo calls. The Gdk calls (operating on gc) are going away in Gtk3. Now you say your writing this only in Gtk2, but if you want to reuse the code one day in another project, it'll help if you write it now with Cairo. There might be some performance differences, too.
  • You should be aware of anti-aliasing. By default, everything is antialiased in cairo (and I don't know whether you can turn it off). That's a good thing most of the time, but sometimes crisp pixels look nicer - and they are a lot faster, too. If you want non-antialiased rectangles, draw filled rectangles on integer coordinates, and stroked rectangles (1px lines) on half-integer coordinates.
  • In your concrete example, your Pixmap seems unneccessary. On thing you could try though is to render your stuff to a pixmap or cairo surface when it changes, then in expose copy the invalidated areas from the pixmap over, and draw the rubber band on top. However, I find that it's often easier and faster to just do the drawing directly. If you are doing this manual buffering, you might want to think about disabling the built-in double-buffering (GtkWidget.set_double_buffered) and the automatic drawing of the background (GtkWidget.set_app_paintable).

Here is a little program I make: rubberband.py. I took the code from a project of mine and added a couple of circles one can select. Hope you can use it as a starting point.

1
votes

Old thread I know, but one model might be copy the rendered surface to another buffer, and then redraw the invalidated area. An example of that technique for the Julia language can be found here. In practice that seems to achieve very good performance.