Otra de las investigaciones que realicé en el marco de las mejoras al instalador de Canaima Popular 3.1, estuvo relacionada con la elaboración de animaciones y gráficos dentro de ventanas GTK utilizando python-cairo.

Cairo es una librería de renderizado de vectores que permite la creación de gráficos a través de objetos python que luego pueden ser renderizados en ventanas, botones, tablas o casi cualquier otro widget GTK. Incluso, es posible renderizar los gráficos en archivos de imagen PNG.

En esta oportunidad explicaré como realizar una barra de desplazamiento animada que permita obtener la posición del cursor dentro de un rango de valores. Opcionalmente, la barra de desplazamiento puede estar dividida en varios sectores o partes. La explicación de la creación de este widget se hará por partes, comenzando desde los elementos básicos, hasta armar el script python completo al final del artículo. Es decir, si quieres ahorrarte la explicación, puedes adelantarte hasta el final.

Instalación de herramientas

Para esta práctica necesitaremos python-gtk2 y python-cairo, los cuales instalaremos desde una Terminal de Root (Menú > Aplicaciones > Accesorios > Terminal de Root) con el siguiente comando:

aptitude install python-gtk2 python-cairo

Lienzo de trabajo

Lo primero que haremos será construir nuestro lienzo de trabajo. El lienzo no es más que una ventana GTK que nos permitirá visualizar los dibujos que vamos realizando. Los comentarios dentro del código irán describiendo cada una de las líneas.

# Declaramos una clase que hereda gtk.Window
# En otras palabras, la clase ahora se comporta como gtk.Window
class Ventana(gtk.Window):

ancho = 500
alto = 200
presionado = False
pos = []

def __init__(self):
# Inicializamos gtk.Window encima de las demás ventanas
gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
# Colocamos la ventana en el centro
gtk.Window.set_position(self, gtk.WIN_POS_CENTER_ALWAYS)
# Establecemos un título para la ventana
self.set_title('Ventana')
# Definimos el tamaño en pixeles de la ventana
self.set_size_request(self.ancho, self.alto)
# Deshabilitamos la redimensión de la ventana
self.set_resizable(False)
# Establecemos el margen interno a 5 pixeles
self.set_border_width(5)
# Conectamos los eventos de cerrado de la ventana a
# la salida del loop principal de gtk
self.connect("destroy", gtk.main_quit)
self.connect("delete-event", gtk.main_quit)

# Declaramos una Caja Vertical que nos permitirá incrustar objetos
# dentro de la ventana secuencialmente
self.contenedor = gtk.VBox()
# Incrustamos la caja vertical en la ventana
self.add(self.contenedor)

# Declaramos un Área de Dibujo
self.barra = gtk.DrawingArea()
# Definimos el tamaño en pixeles del área
# en donde se dibujará la barra de desplazamiento
self.barra.set_size_request(self.ancho - 10, self.alto - 100)
# Obtenemos el tamaño de la barra para utilizarlo después
self.bancho = self.barra.get_size_request()[0]
self.balto = self.barra.get_size_request()[1]
# Definimos los eventos a los cuales reaccionará el área
self.barra.set_events(
gtk.gdk.POINTER_MOTION_MASK
| gtk.gdk.POINTER_MOTION_HINT_MASK
| gtk.gdk.BUTTON_PRESS_MASK
| gtk.gdk.BUTTON_RELEASE_MASK
)
# Lo agregamos a la caja vertical
self.contenedor.add(self.barra)

# Declaramos una etiqueta de texto para mostrar información
self.texto = gtk.Label('100%')
# Definimos el tamaño
self.texto.set_size_request(self.ancho - 10, self.alto - 110)
# Lo agregamos a la caja vertical
self.contenedor.add(self.texto)

# Mostramos todo
self.show_all()

Bloque N°1: Ventana de pruebas.

Creando gradientes de colores

Comenzaremos haciendo un gradiente cairo que nos permitirá rellenar un rectángulo. Los colores en cairo se definen en un formato RGB particular que está compuesto de números decimales entre 0 y 1 en vez del modelo tradicional. Para evitar cálculos engorrosos, haremos una función que permita hacer gradientes basados en colores HTML (notación hexadecimal) porque son más fáciles de especificar. La siguiente función acepta un valor hexadecimal como #000000 (negro) o #FFFFFF (blanco) y retorna una tupla con los tres componentes RGB estándar (números enteros del 0 al 255).

def hex_to_rgb(hx):
# Preparamos una lista vacía
r = []
# Removemos el caracter '#' de la notación HTML
h = hx.lstrip('#')
# Iteramos de dos en dos hasta la longitud de caracteres del color (6)
for i in range(0, 6, 2):
# Añadimos a la lista la conversión de cada par hexadecimal
r.append(int(h[i:i + 2], 16))
# Retornamos la tupla (R,G,B) (notación estándar)
return tuple(r)

Bloque N°2: Conversión hexadecimal a RGB estándar.

La siguiente función permitirá generar un objeto de gradiente lineal en cairo. Un objeto de gradiente lineal necesita la longitud del gradiente (alto en pixeles), un color de inicio y un color de final (en formato RGB de cairo). Se utilizará la función hex_to_rgb (Bloque N°2) para convertir los colores de inicio y fin de notación HTML a formato RGB estándar.

def gradiente(alto, start, end):
# Declaramos el objeto de gradiente lineal
grad = cairo.LinearGradient(0.0, 0.0, 0.0, alto)
# Generamos la tupla de color de inicio (inicio, R, G, B)
s = (0.0,) + hex_to_rgb(start)
# Generamos la tupla de color final (fin, R, G, B)
e = (1.0,) + hex_to_rgb(end)

# Iteramos por el inicio y fin
for i in s, e:
# Convertimos RGB estándar a RGB de cairo
rgb = float(i[0]), float(i[1] / 255.0), float(i[2] / 255.0), float(i[3] / 255.0)
# Agregamos el punto de color al objeto de gradiente lineal
grad.add_color_stop_rgb(*rgb)

# Retornamos el objeto de gradiente
return grad

Bloque N°3: Creación del gradiente.

Ahora, para hacernos la vida un poco más fácil, he creado la siguiente función que condensa una serie de colores conocidos. Permite crear un gradiente utilizando la función gradiente (Bloque N°3) y un conjunto de colores preseleccionados que generan un sutil gradiente para cada uno.

def crear_color(clr, alto):
if clr == 'naranja':
grad = gradiente(alto, '#ff5d2e', '#ff912e')
elif clr == 'azul-claro':
grad = gradiente(alto, '#2460c8', '#2e7bff')
elif clr == 'azul':
grad = gradiente(alto, '#1b4794', '#2460c8')
elif clr == 'azul-oscuro':
grad = gradiente(alto, '#102b58', '#1b4794')
elif clr == 'verde-claro':
grad = gradiente(alto, '#00b900', '#00ff00')
elif clr == 'verde':
grad = gradiente(alto, '#008100', '#00b900')
elif clr == 'verde-oscuro':
grad = gradiente(alto, '#003800', '#008100')
elif clr == 'marron-oscuro':
grad = gradiente(alto, '#382720', '#895f4d')
elif clr == 'marron':
grad = gradiente(alto, '#895f4d', '#e49e80')
elif clr == 'marron-claro':
grad = gradiente(alto, '#e49e80', '#ffcfbb')
elif clr == 'rojo':
grad = gradiente(alto, '#650000', '#cc0000')
elif clr == 'morado':
grad = gradiente(alto, '#45374f', '#806794')
elif clr == 'morado-claro':
grad = gradiente(alto, '#806794', '#b994d5')
elif clr == 'amarillo':
grad = gradiente(alto, '#e89900', '#e8d000')
elif clr == 'blanco':
grad = gradiente(alto, '#ffffff', '#ffffff')
elif clr == 'aguamarina':
grad = gradiente(alto, '#7dfcfe', '#7dfcfe')
elif clr == 'negro':
grad = gradiente(alto, '#000000', '#000000')
elif clr == 'gris':
grad = gradiente(alto, '#b8b598', '#b8b598')

return grad

Bloque N°4: Asignación del gradiente según nombre del color.

Dibujando rectángulos de bordes redondeados

Los rectángulos de bordes redondeados nos ayudarán a dibujar nuestra barra de desplazamiento. rectangulo_red es una función que construye un rectángulo redondeado con un fondo en gradiente. Acepta los siguientes parámetros: cr corresponde a una sesión cairo abierta (lo explicaremos maś adelante), area son las coordenadas de las dimensiones del rectángulo, r es el radio de los arcos redondeados del rectángulo y clr es el nombre del color de fondo con el que colorearemos el rectángulo. Mayor explicación en los comentarios del código.

def rectangulo_red(cr, area, r, clr):
# area debe ser una lista de cuatro items:
# x1, y1: coordenada en pixeles de la esquina superior izquierda del rectángulo
# x2, y2: coordenada en pixeles de la esquina inferior derecha del rectángulo
x1, y1, x2, y2 = area
# clr es el nombre del color, por ejemplo, 'azul'
# y2 - y1 es el cálculo del alto que se utilizará para el gradiente
color = crear_color(clr, y2 - y1)
# Las próximas cuatro líneas corresponden al dibujo de los cuatro arcos
# de las esquinas del rectángulo. Los cuatro parámetros se explican así:
# cr.arc(A, B, C, D)
# A: Coordenada horizontal del centro del arco
# B: Coordenada vertical del centro del arco
# C: Coordenada cartesiana del ángulo de apertura del arco
# D: Coordenada cartesiana del ángulo donde termina el arco.
cr.arc(x1 + r, y1 + r, r, 2 * (math.pi / 2), 3 * (math.pi / 2))
cr.arc(x2 - r, y1 + r, r, 3 * (math.pi / 2), 4 * (math.pi / 2))
cr.arc(x2 - r, y2 - r, r, 0 * (math.pi / 2), 1 * (math.pi / 2))
cr.arc(x1 + r, y2 - r, r, 1 * (math.pi / 2), 2 * (math.pi / 2))
# Establecemos el color
cr.set_source(color)
# Cerramos los trazos de los arcos
cr.close_path()
# Rellenamos el rectángulo con el gradiente
cr.fill()

Bloque N°5: Creación del rectángulo.

Al utilizar la función rectángulo_red, habremos creado un rectángulo dentro de una sesión cairo (cr).

Interactividad y movimiento

La interactividad se obtiene a través del uso apropiado de los eventos dentro del área de dibujo. Es decir, si asociamos determinadas funciones con los eventos, lograremos que el usuario pueda interactuar con nuestra ventana GTK.

La asociación de eventos con funciones introduce un inmenso panorama de posibilidades, pero por ahora basta saber que las funciones necesitan recibir un parámetro widget (el que lo referencia) y un parámetro evento (que contiene información acerca del evento que la invocó).

La función dibujar es una función de asistencia que nos permitirá dibujar el estado inicial de nuestro dibujo, y además refrescar el lienzo cada vez que se invoque. La idea es conectar el evento de exposición (expose-event) con esta función para que el dibujo se mantenga refrescado. Dibujaremos varios cuadros que se describirán en una lista y al final dibujaremos un rectángulo vertical que podrá moverse de lado a lado, simulando un selector. Adicionalmente, le pasaremos información a la función acerca de la ventana donde se encuentra y los cuadros que debe dibujar.

def dibujar(widget, evento, ventana):
lista = []
# La posición del selector se calcula restando el espacio libre después
# del sector al espacio total
selector = ventana.total - ventana.libre
# Este factor es el factor de redimensión de cada uno de los cuadros segun
# la posición del selector
factor = selector / ventana.total

# Refrescamos el lienzo del dibujo
ventana.lienzo = ventana.barra.window.cairo_create()

# Iteramos a través de cada uno de los cuadros
# Cada cuadro debe venir en la forma [inicio, fin, color]
for j in ventana.cuadros:
# Recalculamos el tamaño de cada cuadro según el factor
lista.append([j[0] * factor, j[1] * factor, j[2]])

# Agregamos un cuadro gris que estará después del selector
lista.append([selector, ventana.total, 'gris'])

# Iteramos por cada uno de los cuadros
for c in lista:
# Radio de los cuadros
r = 5
# Los cuadros inician en el tope superior
y1 = 0
# Y terminan en el tope inferior
y2 = ventana.balto
# En c tenemos el punto inicial del cuadro, el punto final y el color
a1, a2, clr = c
# Recalculamos la posición de x1 y x2 según el factor de ancho de la
# ventana con respecto al ancho "bancho" que es el ancho máximo de los cuadros
x1 = a1 * ventana.bancho / ventana.total + 1
x2 = a2 * ventana.bancho / ventana.total - 1

# Si el ancho de un cuadro llega a ser menor a 12px, cambiamos el
# espaciado y el radio para conservar estética
if x2 - x1 < 12:
x1 = x1 - 1
x2 = x2 + 1
r = 0

# Componemos el área del cuadro
area = [x1, y1, x2, y2]
# Dibujamos el cuadro
rectangulo_red(ventana.lienzo, area, r, clr)

# Armamos el cuadro correspondiente al selector
y1_sel = 10
y2_sel = ventana.balto - 10
x1_sel = (selector * ventana.bancho / ventana.total) - 5
x2_sel = (selector * ventana.bancho / ventana.total) + 5

# Dibujamos el selector
ventana.sel = [x1_sel, y1_sel, x2_sel, y2_sel]
rectangulo_red(ventana.lienzo, ventana.sel, 3, 'negro')

Bloque N°6: Creación del dibujo en el lienzo.

Necesitamos saber en qué momento se presiona y en qué momento se suelta el selector. Para ello utilizaremos una variable asociada a la ventana (ventana.presionado), la cuál se establecerá True o False convenientemente. La función presionar estará asociada al evento button-press-event, el cual se acciona cada vez que se hace click dentro del lienzo. Para este evento, la variable evento.x y evento.y van a contener el valor de las coordenadas del cursor dentro del lienzo de dibujo; esto nos permitirá saber el momento en que se haga click dentro del área asociada al selector (establecida en ventana.sel). Por otro lado, la función soltar estará asociada al evento button-release-event, que establecerá ventana.presionado a False cuando se suelte el click del ratón.

def presionar(widget, evento, ventana):
if evento.x >= ventana.sel[0] and \
evento.x <= ventana.sel[2] and \
evento.y >= ventana.sel[1] and \
evento.y <= ventana.sel[3]:
ventana.presionado = True

def soltar(widget, evento, ventana):
ventana.presionado = False

Bloque N°7: Funciones para presionar y soltar.

Luego haremos una función que nos permita redibujar los cuadros de acuerdo al movimiento del selector, para ello la conectaremos al evento motion-notify-event. Para este evento, la variable evento.x y evento.y van a tener el valor de la posición del cursor dentro del lienzo de dibujo; esto nos permitirá saber el momento en que el cursor pase por encima del selector, en donde podremos cambiar el tipo de cursor que se muestra. Por otro lado, la variable ventana.presionado nos permitirá saber si el cursor se encuentra presionado, momento en el cuál procederemos a actualizar la posición y tamaño de los cuadros mediante la función queue_draw(), la cual invoca el evento expose-event, que ejecuta la función dibujar().

def redibujar(widget, evento, ventana):
# Si el cursor está dentro del área del selector
if evento.x >= ventana.sel[0] and \
evento.x <= ventana.sel[2] and \
evento.y >= ventana.sel[1] and \
evento.y <= ventana.sel[3]:
# Establecemos el cursor tipo mano
cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
ventana.barra.window.set_cursor(cursor)
else:
# De loc ontrario, establecemos el cursor tipo normal
cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
ventana.barra.window.set_cursor(cursor)

# Si se ha presionado el selector ...
if ventana.presionado == True:
# ... y el cursor se encuentra dentro de los límites del lienzo
if evento.x <= ventana.bancho and evento.x >= 0:
# Calculamos la posición del selector de acuerdo a la proporción
x = float(evento.x * ventana.total / ventana.bancho)
# Calculamos el espacio a la derecha del selector
ventana.libre = ventana.total - x
# Establecemos el porcentaje
ventana.texto.set_label(str(int(x))+'%')
# Mandamos a redibujar
ventana.barra.queue_draw()

Bloque N°8: Redibujar de acuerdo a la posición del selector.

Por último, declararemos nuestras variables, ventana y conectaremos cada evento con sus funciones.

if __name__ == "__main__":
# Instanciamos la ventana
ventana = Ventana()
# Esta variable define cuánto es la unidad máxima posible para los cuadros.
# A partir de este valor se calculará el factor de tamaño de los cuadros.
# Puede entenderse mejor si se interpreta como porcentaje (100%).
ventana.total = 100.0
# Esta variable define cuanto espacio libre habrá a la derecha del selector
ventana.libre = 0.0
# Construímos el arreglo de cuadros, tomando en cuenta el valor máximo
ventana.cuadros = [
[0.0, 20.0, 'verde'],
[20.0, 60.0, 'rojo'],
[60.0, 100.0, 'amarillo']
]

# Conectamos las funciones a los eventos
ventana.barra.connect("expose-event", dibujar, ventana)
ventana.barra.connect("button-press-event", presionar, ventana)
ventana.barra.connect("button-release-event", soltar, ventana)
ventana.barra.connect("motion-notify-event", redibujar, ventana)

# Iniciamos el loop principal de gtk
gtk.main()
# Salimos apropiadamente
sys.exit()

Bloque N°7: Llamado de la ventana y conexión de eventos con funciones.

Resultados

Bueno, finalmente toca juntar todos los bloques en un sólo archivo de texto. Lo guardamos como “barra.py” y lo ejecutamos en una Terminal de Usuario (Menú > Aplicaciones > Accesorios > Terminal de Usuario) de la siguiente manera:

cd /ruta/a/la/carpeta/
python barra.py

Y a continuación se muestra el script completo. Espero que les sea de utilidad.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Importamos las librerías
import gtk, sys, cairo, math

# Declaramos una clase que hereda gtk.Window
# En otras palabras, la clase ahora se comporta como gtk.Window
class Ventana(gtk.Window):

ancho = 500
alto = 200
presionado = False
pos = []

def __init__(self):
# Inicializamos gtk.Window encima de las demás ventanas
gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
# Colocamos la ventana en el centro
gtk.Window.set_position(self, gtk.WIN_POS_CENTER_ALWAYS)
# Establecemos un título para la ventana
self.set_title('Ventana')
# Definimos el tamaño en pixeles de la ventana
self.set_size_request(self.ancho, self.alto)
# Deshabilitamos la redimensión de la ventana
self.set_resizable(False)
# Establecemos el margen interno a 5 pixeles
self.set_border_width(5)
# Conectamos los eventos de cerrado de la ventana a
# la salida del loop principal de gtk
self.connect("destroy", gtk.main_quit)
self.connect("delete-event", gtk.main_quit)

# Declaramos una Caja Vertical que nos permitirá incrustar objetos
# dentro de la ventana secuencialmente
self.contenedor = gtk.VBox()
# Incrustamos la caja vertical en la ventana
self.add(self.contenedor)

# Declaramos un Área de Dibujo
self.barra = gtk.DrawingArea()
# Definimos el tamaño en pixeles del área
# en donde se dibujará la barra de desplazamiento
self.barra.set_size_request(self.ancho - 10, self.alto - 100)
# Obtenemos el tamaño de la barra para utilizarlo después
self.bancho = self.barra.get_size_request()[0]
self.balto = self.barra.get_size_request()[1]
# Definimos los eventos a los cuales reaccionará el área
self.barra.set_events(
gtk.gdk.POINTER_MOTION_MASK
| gtk.gdk.POINTER_MOTION_HINT_MASK
| gtk.gdk.BUTTON_PRESS_MASK
| gtk.gdk.BUTTON_RELEASE_MASK
)
# Lo agregamos a la caja vertical
self.contenedor.add(self.barra)

# Declaramos una etiqueta de texto para mostrar información
self.texto = gtk.Label('100%')
# Definimos el tamaño
self.texto.set_size_request(self.ancho - 10, self.alto - 110)
# Lo agregamos a la caja vertical
self.contenedor.add(self.texto)

# Mostramos todo
self.show_all()

def hex_to_rgb(hx):
# Preparamos una lista vacía
r = []
# Removemos el caracter '#' de la notación HTML
h = hx.lstrip('#')
# Iteramos de dos en dos hasta la longitud de caracteres del color (6)
for i in range(0, 6, 2):
# Añadimos a la lista la conversión de cada par hexadecimal
r.append(int(h[i:i + 2], 16))
# Retornamos la lista [R,G,B] (notación estándar)
return tuple(r)

def gradiente(alto, start, end):
# Declaramos el objeto de gradiente lineal
grad = cairo.LinearGradient(0.0, 0.0, 0.0, alto)
# Generamos la tupla de color de inicio (inicio, R, G, B)
s = (0.0,) + hex_to_rgb(start)
# Generamos la tupla de color final (fin, R, G, B)
e = (1.0,) + hex_to_rgb(end)

# Iteramos por el inicio y fin
for i in s, e:
# Convertimos RGB estándar a RGB de cairo
rgb = float(i[0]), float(i[1] / 255.0), float(i[2] / 255.0), float(i[3] / 255.0)
# Agregamos el punto de color al objeto de gradiente lineal
grad.add_color_stop_rgb(*rgb)

# Retornamos el objeto de gradiente
return grad

def crear_color(clr, alto):
if clr == 'naranja':
grad = gradiente(alto, '#ff5d2e', '#ff912e')
elif clr == 'azul-claro':
grad = gradiente(alto, '#2460c8', '#2e7bff')
elif clr == 'azul':
grad = gradiente(alto, '#1b4794', '#2460c8')
elif clr == 'azul-oscuro':
grad = gradiente(alto, '#102b58', '#1b4794')
elif clr == 'verde-claro':
grad = gradiente(alto, '#00b900', '#00ff00')
elif clr == 'verde':
grad = gradiente(alto, '#008100', '#00b900')
elif clr == 'verde-oscuro':
grad = gradiente(alto, '#003800', '#008100')
elif clr == 'marron-oscuro':
grad = gradiente(alto, '#382720', '#895f4d')
elif clr == 'marron':
grad = gradiente(alto, '#895f4d', '#e49e80')
elif clr == 'marron-claro':
grad = gradiente(alto, '#e49e80', '#ffcfbb')
elif clr == 'rojo':
grad = gradiente(alto, '#650000', '#cc0000')
elif clr == 'morado':
grad = gradiente(alto, '#45374f', '#806794')
elif clr == 'morado-claro':
grad = gradiente(alto, '#806794', '#b994d5')
elif clr == 'amarillo':
grad = gradiente(alto, '#e89900', '#e8d000')
elif clr == 'blanco':
grad = gradiente(alto, '#ffffff', '#ffffff')
elif clr == 'aguamarina':
grad = gradiente(alto, '#7dfcfe', '#7dfcfe')
elif clr == 'negro':
grad = gradiente(alto, '#000000', '#000000')
elif clr == 'gris':
grad = gradiente(alto, '#b8b598', '#b8b598')

return grad

def rectangulo_red(cr, area, r, clr):
# area debe ser una lista de cuatro items:
# x1, y1: coordenada en pixeles de la esquina superior izquierda del rectángulo
# x2, y2: coordenada en pixeles de la esquina inferior derecha del rectángulo
x1, y1, x2, y2 = area
# clr es el nombre del color, por ejemplo, 'azul'
# y2 - y1 es el cálculo del alto que se utilizará para el gradiente
color = crear_color(clr, y2 - y1)
# Las próximas cuatro líneas corresponden al dibujo de los cuatro arcos
# de las esquinas del rectángulo. Los cuatro parámetros se explican así:
# cr.arc(A, B, C, D)
# A: Coordenada horizontal del centro del arco
# B: Coordenada vertical del centro del arco
# C: Coordenada cartesiana del ángulo de apertura del arco
# D: Coordenada cartesiana del ángulo donde termina el arco.
cr.arc(x1 + r, y1 + r, r, 2 * (math.pi / 2), 3 * (math.pi / 2))
cr.arc(x2 - r, y1 + r, r, 3 * (math.pi / 2), 4 * (math.pi / 2))
cr.arc(x2 - r, y2 - r, r, 0 * (math.pi / 2), 1 * (math.pi / 2))
cr.arc(x1 + r, y2 - r, r, 1 * (math.pi / 2), 2 * (math.pi / 2))
# Establecemos el color
cr.set_source(color)
# Cerramos los trazos de los arcos
cr.close_path()
# Rellenamos el rectángulo con el gradiente
cr.fill()

def dibujar(widget, evento, ventana):
lista = []
# La posición del selector se calcula restando el espacio libre después
# del sector al espacio total
selector = ventana.total - ventana.libre
# Este factor es el factor de redimensión de cada uno de los cuadros segun
# la posición del selector
factor = selector / ventana.total

# Refrescamos el lienzo del dibujo
ventana.lienzo = ventana.barra.window.cairo_create()

# Iteramos a través de cada uno de los cuadros
# Cada cuadro debe venir en la forma [inicio, fin, color]
for j in ventana.cuadros:
# Recalculamos el tamaño de cada cuadro según el factor
lista.append([j[0] * factor, j[1] * factor, j[2]])

# Agregamos un cuadro gris que estará después del selector
lista.append([selector, ventana.total, 'gris'])

# Iteramos por cada uno de los cuadros
for c in lista:
# Radio de los cuadros
r = 5
# Los cuadros inician en el tope superior
y1 = 0
# Y terminan en el tope inferior
y2 = ventana.balto
# En c tenemos el punto inicial del cuadro, el punto final y el color
a1, a2, clr = c
# Recalculamos la posición de x1 y x2 según el factor de ancho de la
# ventana con respecto al ancho "bancho" que es el ancho máximo de los cuadros
x1 = a1 * ventana.bancho / ventana.total + 1
x2 = a2 * ventana.bancho / ventana.total - 1

# Si el ancho de un cuadro llega a ser menor a 12px, cambiamos el
# espaciado y el radio para conservar estética
if x2 - x1 < 12:
x1 = x1 - 1
x2 = x2 + 1
r = 0

# Componemos el área del cuadro
area = [x1, y1, x2, y2]
# Dibujamos el cuadro
rectangulo_red(ventana.lienzo, area, r, clr)

# Armamos el cuadro correspondiente al selector
y1_sel = 10
y2_sel = ventana.balto - 10
x1_sel = (selector * ventana.bancho / ventana.total) - 5
x2_sel = (selector * ventana.bancho / ventana.total) + 5

# Dibujamos el selector
ventana.sel = [x1_sel, y1_sel, x2_sel, y2_sel]
rectangulo_red(ventana.lienzo, ventana.sel, 3, 'negro')

def presionar(widget, evento, ventana):
if evento.x >= ventana.sel[0] and \
evento.x <= ventana.sel[2] and \
evento.y >= ventana.sel[1] and \
evento.y <= ventana.sel[3]:
ventana.presionado = True

def soltar(widget, evento, ventana):
ventana.presionado = False

def redibujar(widget, evento, ventana):
# Si el cursor está dentro del área del selector
if evento.x >= ventana.sel[0] and \
evento.x <= ventana.sel[2] and \
evento.y >= ventana.sel[1] and \
evento.y <= ventana.sel[3]:
# Establecemos el cursor tipo mano
cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
ventana.barra.window.set_cursor(cursor)
else:
# De loc ontrario, establecemos el cursor tipo normal
cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
ventana.barra.window.set_cursor(cursor)

# Si se ha presionado el selector ...
if ventana.presionado == True:
# ... y el cursor se encuentra dentro de los límites del lienzo
if evento.x <= ventana.bancho and evento.x >= 0:
# Calculamos la posición del selector de acuerdo a la proporción
x = float(evento.x * ventana.total / ventana.bancho)
# Calculamos el espacio a la derecha del selector
ventana.libre = ventana.total - x
# Establecemos el porcentaje
ventana.texto.set_label(str(int(x))+'%')
# Mandamos a redibujar
ventana.barra.queue_draw()

if __name__ == "__main__":
# Instanciamos la ventana
ventana = Ventana()
# Esta variable define cuánto es la unidad máxima posible para los cuadros.
# A partir de este valor se calculará el factor de tamaño de los cuadros.
# Puede entenderse mejor si se interpreta como porcentaje (100%).
ventana.total = 100.0
# Esta variable define cuanto espacio libre habrá a la derecha del selector
ventana.libre = 0.0
# Construímos el arreglo de cuadros, tomando en cuenta el valor máximo
ventana.cuadros = [
[0.0, 20.0, 'verde'],
[20.0, 60.0, 'rojo'],
[60.0, 100.0, 'amarillo']
]

# Conectamos las funciones a los eventos
ventana.barra.connect("expose-event", dibujar, ventana)
ventana.barra.connect("button-press-event", presionar, ventana)
ventana.barra.connect("button-release-event", soltar, ventana)
ventana.barra.connect("motion-notify-event", redibujar, ventana)

# Iniciamos el loop principal de gtk
gtk.main()
# Salimos apropiadamente
sys.exit()