chris-kuhr
chris-kuhr

Reputation: 365

PyGTK Custom Widget doesnt redraw

I have a problem updating a custom GTK+ widget: VUWidget. A Generator class is updating the level property of the class Section, whose subclass has a VUWidget property. The Generator class updates the values of the level properties correctly.

import pygtk  
pygtk.require("2.0")  
import gtk, gtk.gdk  
import cairo  

#=========================================================================

class VUWidget(gtk.DrawingArea):
    __gtype_name__ = 'VUWidget'

    _BACKGROUND_RGB = (0., 0., 0.)
    _LEVEL_GRADIENT_BOTTOM_ORGBA = (1, 0, 1, 0, 1)
    _LEVEL_GRADIENT_TOP_ORGBA = (0, 1, 0, 0, 1)

    #_____________________________________________________________________

    def __init__(self, section):
        gtk.DrawingArea.__init__(self)      
        self.section = section      

        self.connect("configure_event", self.on_configure_event)
        self.connect("expose-event", self.OnDraw)
        self.section.connect("changed-value", self.ValueChanged)

        self.set_size_request(30,100)       
        self.realize()
        self.show()
    #_____________________________________________________________________


    def ValueChanged(self, widget, level):
        #print ("Callback %f" % self.section.GetLevel()) 

        rect = self.get_allocation()                
        self.window.invalidate_rect(rect, False)
        return False
    #_____________________________________________________________________


    def GenerateBackground(self):       
        rect = self.get_allocation()
        ctx = cairo.Context(self.source)

        ctx.set_line_width(2)
        ctx.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
        pat = cairo.LinearGradient(0.0, 0.0, 0, rect.height)
        pat.add_color_stop_rgba(*self._LEVEL_GRADIENT_BOTTOM_ORGBA)
        pat.add_color_stop_rgba(*self._LEVEL_GRADIENT_TOP_ORGBA)
        ctx.rectangle(0, 0, rect.width, rect.height)
        ctx.set_source(pat)

        ctx.fill()
    #_____________________________________________________________________


    def on_configure_event(self, widget, event):
        self.source = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.allocation.width, self.allocation.height)
        self.GenerateBackground()

        return self.OnDraw(widget, event)
    #_____________________________________________________________________


    def OnDraw(self, widget, event):        
        ctx = self.window.cairo_create()
        ctx.save()

        rect = self.get_allocation()

        ctx.rectangle(0, 0, rect.width, rect.height)
        ctx.set_source_rgb(*self._BACKGROUND_RGB)
        ctx.fill()

        ctx.rectangle(0, rect.height * (1. - self.section.GetLevel()), rect.width, rect.height)
        ctx.clip()

        ctx.set_source_surface(self.source, 0, 0)   
        ctx.paint()

        ctx.restore()

        return False        
    #_____________________________________________________________________


    def Destroy(self):
        del self.source
        self.destroy()  
    #_____________________________________________________________________  

#=========================================================================

the signal is implemented in the class Section and is emitted correctly

__gsignals__ = {
    'changed-value' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_FLOAT,))
    }

Regards Ck

Upvotes: 1

Views: 1190

Answers (1)

XORcist
XORcist

Reputation: 4367

Here's one way to do it, I subclass this and provide a custom draw method. If there are any changes to the state, I call invalidate, forcing the widget to re-draw through the expose-event. If you need a different context, you can provide it in the on_expose_event method.

So basically, do all your drawing in one place only. If the widget should display something different, set the new state and re-render. Makes it easy to maintain.

#cairovehicle.py

import gtk

class CairoVehicle(gtk.DrawingArea):
    def __init__(self):
        gtk.DrawingArea.__init__(self)
        self.connect("expose-event", self.on_expose_event)

    def get_background(self):
        """Serves as a caching solution."""
        return self.__bg

    def set_background(self, pattern):
        """Serves as a caching solution."""
        self.__bg = pattern

    def get_brush(self):
        """Serves as a caching solution."""
        return self.__brush

    def set_brush(self, pattern):
        """Serves as a caching solution."""
        self.__brush = pattern

    def on_expose_event(self, widget, event):
        context = self.window.cairo_create()

        # Restrict cairo to the exposed area
        context.rectangle(*event.area)
        context.clip()

        self.width, self.height = self.window.get_size()

        self.draw(context)

    def on_configure_event(self, widget, event):
        """Override this in case you want to do something when 
           the widget's size changes."""

        return super(CairoVehicle, self).on_configure_event(widget, event)

    def invalidate(self):
        """Force a re-rendering of the window."""

        rect = self.get_allocation()

        # Compensate for parent offset, if any.
        parent = self.get_parent()
        if parent:
            offset = parent.get_allocation()
            rect.x -= offset.x
            rect.y -= offset.y

        self.window.invalidate_rect(rect, False)

    def draw(self, context):
        """Override this."""

        # do custom drawing here

        raise NotImplementedError()

    def make_grid(self, context, fgcolor, bgcolor, gapwidth, linewidth, 
                                                        width, height):

         context.set_source_rgba(*bgcolor)
         context.rectangle(0, 0, width, height)
         context.fill()

         context.set_source_rgba(*fgcolor)
         context.set_line_width(linewidth)

         # uneven linewidths lead to blurry displaying when set on integer
         # coordinates, so in that case move coordinates away by half a
         # pixel.
         adjust = 0.5 if linewidth % 2 else 0
         i = 1
         j = 1

         while gapwidth*i-adjust < width:
             context.move_to(gapwidth*i-adjust, 0)
             context.line_to(gapwidth*i-adjust, height)
             context.stroke()
             i += 1

         while gapwidth*j-adjust < height:
             context.move_to(0, gapwidth*j-adjust)
             context.line_to(width, gapwidth*j-adjust)
             context.stroke()
             j += 1


class Grid(CairoVehicle):
    def draw(self, context):
        context.push_group()
        self.make_grid(context, fgcolor=(0, 0, 0, 1), bgcolor=(1, 1, 1, 1),
                    gapwidth=20, linewidth=1, width=self.width,
                    height=self.height)
        self.set_background(context.pop_group())
        context.set_source(self.get_background())
        context.paint()


if __name__ == "__main__":
    w = gtk.Window()
    w.connect("destroy", gtk.main_quit)
    w.show()

    cv = Grid()
    cv.show()
    w.add(cv)
    gtk.main()

Supplemental response to comment:

Gtk.Window and Gdk.Window are conceptually different. The first is a container that can have exactly one other widget (other container or any other widget). The second is used for drawing stuff on the display and capture events. The gdk.Window is constructed in a widget's "realize" event. Before that, it is just None in PyGtk. IMHO, it is inadvisable (don't even know if it's doable) to draw on a gtk.Window.window. Custom drawing should be done in the gtk.DrawingArea.window. That is what DrawingArea is there for.

References:

https://mail.gnome.org/archives/gtk-app-devel-list/1999-January/msg00138.html
GtkDrawingArea - how to make it drawable?

Upvotes: 3

Related Questions