Cairo Pango Widget

Posted by Colonel Patch on May 25, 2011

Here is a little demo I did to help get my head around using cairo and pango as I wanted to have a base for a flexible widget. It uses scaling ,translation and animation to bounce an image around the screen. There is also a routine for detecting mouse clicks so you can "tickle" the head!

 

## Program to show the use of cairo graphics
## and matrix operations for transforms

## author: Paul Heaton
##  email: p.m.heaton@reading.ac.uk


import pygtk
import gtk, gobject, cairo, pango
from gtk import gdk

class CairoScreen( gtk.DrawingArea ):
    
    def __init__(self):
        #superclass this Drawing Area as a custom widget
        super(CairoScreen,self).__init__()

        ## Connect expose event.
        self.connect ( "expose_event", self.do_expose_event )
        ## connect mouse callbacks:
        #self.connect ( "motion_notify_event", self._mouseMoved )
        self.connect ( "button_press_event", self._mouseClicked )
        ##   unmask events
        self.add_events ( gdk.BUTTON_PRESS_MASK |   gdk.BUTTON_RELEASE_MASK |   gdk.POINTER_MOTION_MASK )       

        ##Initiate timer at 25 fps.
        gobject.timeout_add( 40, self.tick )
       
    def tick ( self ):
        ## Invalidates the screen so the expose event fires.
        self.alloc = self.get_allocation ( )
        rect = gtk.gdk.Rectangle ( self.alloc.x, self.alloc.y, self.alloc.width, self.alloc.height )
        self.window.invalidate_rect ( rect, True )       
        return True # Cause timeout to tick again.



    def _mouseMoved ( self, widget, event ):
        self.mx = event.x
        self.my = event.y
        self.draw(self.width,self.height)
        #print "mx="+str(event.x)+"  my="+str(event.y)

    def _mouseClicked ( self, widget, event ):
        self.mx = event.x
        self.my = event.y
        #print "mx="+str(event.x)+"  my="+str(event.y)

    def do_expose_event( self, widget, event ):
        ## create a cairo resource
        self.cr = self.window.cairo_create( )
       
        ##Redraw screen at current screensize.
        self.draw( *self.window.get_size( ) )

## Derive a new cairo screen from CairoScreen class above
class AnimScreen ( CairoScreen ):
    """Add extravariables to hold object (imagesurface) and
        object variables to CairoScreen."""
    def __init__ ( self ):
        CairoScreen.__init__( self )

        # add your own image here!
        self.imagesurface = cairo.ImageSurface.create_from_png('baby_head_small.png')
        # add pango layout for on screen text
        self.pangolayout = self.create_pango_layout("text")
        self.pangolayout.set_text ("ooooh.\n That tickles!")
        # [FAMILY-LIST] [STYLE-OPTIONS] [SIZE] eg serif,monospace bold italic condensed 16
        fd=pango.FontDescription("sans serif bold italic 28")
        self.pangolayout.set_font_description(fd)
        alloc=self.pangolayout.get_pixel_size()
        self.fontx=alloc[0]
        self.fonty=alloc[1]
        print "fontxy=",self.fontx,", ",self.fonty
        self.fontw, self.fonth = self.pangolayout.get_pixel_size()


        #mouse pos passed from m events
        self.mx = -999999
        self.my = -999999
       
       
        ## x,y of object surface (left side of screen to start)
        self.x, self.y = -200, 0
        #save origin width and height
        self.iw=self.imagesurface.get_width()
        self.ih=self.imagesurface.get_height()

        ## move origin (mag dxdy=speed)
        self.dx,self.dy=2,1
        ## rx,ry is radius of thetaation from surface orgin
        self.rx, self.ry = 0, 0
        ## theta is angle counter
        self.theta = 0.0
        ## sx,sy is to scale surface
        self.sx, self.sy =1.0, 1.0
        ## sign variable for scaling and movement calcs
        self.sign=+1
       

    def setToCenter ( self ):
        """Shift 0,0 to be in the center of page."""
        matrix = cairo.Matrix ( 1, 0, 0, 1, self.width/2, self.height/2 )
        self.cr.transform ( matrix ) # Make it so...

       
    def draw( self, width, height ):
        ## width and height of shape or image passed
   
        ## shortcut
        cr = self.cr

        cr.identity_matrix  ( ) # reset context
       
        #stuff drawn here will be drawn over by next layer
        #so appearing behind
        self.drawbox ( cr )
        ## Set 0,0 to be in the center of page
        ## ie...
        ##  -y | -y
        ##  -x | +x
        ## ----0------
        ##  -x | +x
        ##  +y | +y

        #create a matrix path step to do transform
        matrix = cairo.Matrix ( 1, 0, 0, 1, width/2, height/2 )
        cr.transform ( matrix ) # and transform to image surface...

        cr.save ( ) # save toplevel  ie the above mask
       
        ## Create a matrix to change the imagesurface's position and theta rotation.
        transform_matrix = cairo.Matrix ( 1, 0, 0, 1, 0, 0 )

        #get current allocation of CairoScreen
        x,y,w,h=self.get_allocation()

        ## with origin in center wall bounce is at +or- width/4 or height/4
        ## not sure why /4 !!
        ## remember matrix is in center of image
        if self.x < -w/4 or self.x+self.imagesurface.get_width()/2 > w/4:
            self.dx *= -1
        if self.y < -h/4 or self.y + self.imagesurface.get_height()/2> h/4:
            self.dy *=-1
           

        ## move transform_matrix to x,y of imagesurface
        cairo.Matrix.translate ( transform_matrix, self.x, self.y )

        ## rotate a little
        self.theta += 0.05

        # move it all to point of rotation
        ## in this case the center of the image/shape
        #and apply transform
        cairo.Matrix.translate( transform_matrix , self.x, self.y )
        ## Now, rotate the matrix
        cairo.Matrix.rotate( transform_matrix, self.theta ) # Do the rotation
         # move it back again
        cairo.Matrix.translate( transform_matrix, -self.x, -self.y)
      
        ## change size a bit
        self.sx += self.sign *  0.005
        if  self.sx < 0.5 or self.sx > 1.5:
            self.sign *= -1
        self.sy = self.sx

        # and Scale the matrix
        cairo.Matrix.scale( transform_matrix, self.sx, self.sy ) # Now scale it all

        ## commit all matrix ops to context
        ## ie use the mask path above in cr when doing drawing ops
        # ie Draw it
        cr.transform ( transform_matrix )


        ## move the image surface x and y a bit
        self.x += self.dx
        self.y += self.dy
       
        ## anything drawn now is under influence of transform_matrix.
        ## until a restore is called
        self.drawhead ( cr ,self.x,self.y)
        x1=self.x
        y1=self.y
        x2=self.iw
        y2=self.ih
        self.cr.rectangle( x1, y1, x2, y2 ) # Same as the shape of the imagemap
        cr.save() # save level 1 (bouncing head)       
               
        # Detect mouse hit
        cr.identity_matrix ( ) # Reset the matrix to 0,0 in TLH Corner.
        hit = cr.in_fill ( self.mx, self.my ) # Use Cairo's built-in hit test
        cr.new_path ( ) # stops the hit shape from being drawn
        #self.drawcross ( cr , self.mx, self.my)
       
        cr.restore ( ) # restore level 1.      
       
        cr.restore ( )  # restore to toplevel

        ## Draw a crosshair to identify 0,0
        self.drawcross ( cr )
       
        if hit:
            #print "HIT!", self.x, self.y
            playout = self.pangolayout
           
            cr.set_source_rgb ( 0, 0, 1 )
            #could have offset here
            cr.move_to ( self.mx-width/2,self.my-height/2 )
            #cr.move_to((0 - fontw - 4), (0- fonth ))
            cr.update_layout(playout)
            cr.show_layout(playout)
           


    def drawhead(self,cr, x=0, y=0):
        cimg = self.imagesurface
        cr.set_source_surface(cimg, x, y)
        cr.paint() # draw image surface
       
       
    def drawcross ( self, cr ,x=0, y=0):
        ## Default 0,0 in the center
        cr.set_source_rgb ( 0, 0, 0 )
        cr.move_to ( x,y+10 )
        cr.line_to ( x, y-10 )
        cr.move_to ( x-10, y )
        cr.line_to ( x+10, y )
        cr.stroke ( )
       
    def drawbox ( self, cr):
        ## 0,0 TLHC
        x,y,w,h=self.get_allocation()
        cr.set_source_rgb ( 1, 0, 0 )
        cr.move_to ( x,y )
        cr.line_to ( w/2-w/4, h/2-h/4 )
        cr.line_to ( w/2+w/4, h/2-h/4 )
        cr.line_to ( w, y )
       
        cr.move_to ( x,h )
        cr.line_to ( w/2-w/4, h/2+h/4 )
        cr.line_to ( w/2+w/4, h/2+h/4 )
        cr.line_to ( w, h )
       
        cr.move_to ( w/2-w/4, h/2-h/4 )
        cr.line_to ( w/2-w/4, h/2+h/4 )
       
        cr.move_to (  w/2+w/4, h/2-h/4 )
        cr.line_to ( w/2+w/4, h/2+h/4 )
       
        cr.stroke ( )


def run( Widget ):
    window = gtk.Window( )
    window.set_size_request(800,600)   
    window.set_position(gtk.WIN_POS_CENTER)
    window.connect( "delete-event", gtk.main_quit )
    widget = Widget( )
    widget.show( )
    window.add( widget )
    window.present( )
    gtk.main( )

if __name__=="__main__":
    run( AnimScreen )

Comments:

Leave a Reply



(Your email will not be publicly displayed.)

Please type the letters and numbers shown in the image.Captcha Code