Programando en Ruby

La Guía de los "Programadores Pragmáticos"

Ruby Tk



El Ruby Application Archive contiene varias extensiones que proporcionan a Ruby interfaces gráficas de usuario (GUI), incluyendo extensiones para Tcl/Tk, GTK, OpenGL, y otros.

La extensión de Tk está incluida en la distribución oficial y funciona tanto en sistemas Unix como Windows. Para usarla, necesitas tener instalado Tk en tu sistema. Tk es un gran sistema, y hay libros enteros escritos sobre él, por lo que no perderemos ni tiempo ni recursos profundizando en Tk en si mismo, sino que nos concentraremos en cómo acceder a las características de Tk desde Ruby. Necesitarás algún libro de referencia para usar Tk con Ruby de manera efectiva. El binding que usaremos es muy similar al binding de Perl, por lo que quizá te apetezca hacerte con alguna copia de Learning Perl/Tk  o Perl/Tk Pocket Reference .

Tk funciona a través de un modelo de composición---ésto es, comienzas creando un componente contenedor (como por ejemplo TkFrame o TkRoot) y después crearías los componentes que los rellenen, como botones o etiquetas. Cuando todo estuviese listo para iniciar la GUI, llamarías a Tk.mainloop. El motor de Tk en ese momento tomaría el control del programa mostrando componentes y ejecutando código tuyo en respuesta a los eventos de la GUI.

Una aplicación Tk sencilla

Una aplicación Tk sencilla en Ruby podría tener un aspecto como este:

require 'tk'
root = TkRoot.new { title "Ex1" }
TkLabel.new(root) {
  text  'Hello, World!'
  pack  { padx 15 ; pady 15; side 'left' }
}
Tk.mainloop

Echemos un vistazo al código de una manera más detallada. Después de cargar el módulo de extensión tk, creamos un marco a nivel raíz usando TkRoot.new. Después creamos un componente etiqueta como hijo del marco raíz, estableciendo varias opciones de la propia etiqueta. Finalmente, ajustamos el tamaño del marco raíz y entramos en el bucle principal de eventos de la GUI.

Es una buena costumbre especificar la raíz explícitamente, pero podrías no hacerlo ---además de otras opciones adicionales---y reducir el código a tres líneas:

require 'tk'
TkLabel.new { text 'Hello, World!' }
Tk.mainloop

¡Ésto es todo lo que hay! Armado con alguno de los libros de Perl/Tk que indicamos al principio de este capítulo, ya podrías crear todas las sofisticadas GUIs que puedas necesitar. Pero de nuevo, si quieres conocer más detalles, aquí vienen.

Componentes

Crear componentes es sencillo. Coge el nombre del componente tal y como aparece en la documentación de Tk y añádele un Tk delante de él. Por ejemplo, los componentes Label, Button, y Entry pasarían a ser las clases TkLabel, TkButton, y TkEntry. Se crean instancias de los componentes mediante new, tal y como harías con cualquier otro objeto. Si no especificas un padre específico para un componente determinado, se añadirá por defecto al marco raíz. Normalmete especificaremos los padres de los componentes, así como muchas otras opciones---color, tamaño, y demás. También necesitaremos ser capaces de obtener información de nuestros componentes mientras nuestro programa se esté ejecutando estableciendo callbacks y compartiendo datos.

Estableciendo las opciones de los componentes

Si consultas cualquier manual de referencia de Tk (el escrito para Perl/Tk, por ejemplo), podrás comprobar que las opciones de los componentes se listan normalmente con un guión---como si se tratara de una opción de la línea de comandos. En Perl/Tk, las opciones se pasan a un componente mediante una Hash. Se puede hacer lo mismo en Ruby, pero también puedes pasar las opciones usando un bloque de código; el nombre de la opción se usa como nombre de método dentro del bloque y los argumentos de cada opción aparecen como argumentos en la llamada del método. Los componentes reciben un padre como primer argumento, seguido opcionalmente por una hash con las opciones o el bloque de código con las opciones. Por lo tanto, las dos formas siguientes son equivalentes.

TkLabel.new(parent_widget) {
  text    'Hello, World!'
  pack('padx'  => 5,
       'pady'  => 5,
       'side'  => 'left')
}
# or
TkLabel.new(parent_widget, text => 'Hello, World!').pack(...)

Una pequeña precaución que hay que tomar cuando se use el bloque de código: el ámbito de las variables no es el que podrías pensar que es. El bloque se evalua realmente en el contexto del objeto del componente, no en el del que llama. Ésto significa que las variables de instancia del que llama no estarán disponibles en el bloque, pero sí las variables locales del enclosing scope y las variables globales (not that you ever use those). Mostraremos cómo pasar las opciones usando los dos métodos en los siguientes ejemplos.

Las distancias (como en las opciones padx y pady en nuestros ejemplos) se asume que están en pixels, pero pueden especificarse en diferentes unidades usando los sufijos ``c'' (centímetros), ``i'' (pulgadas), ``m'' (milímetros), o ``p'' (puntos).

Obtener datos de los componentes

Podemos obtener información de los componentes con callbacks y con variables binding.

Los callbacks son muy sencillos de configurar. La opción command (mostrada en la llamada a TkButton en el siguiente ejemplo) recibe un objeto Proc, que será llamado cuando el callback se ejecute. Aquí se usa Kernel::proc para convertir el bloque {exit} a Proc.

TkButton.new(bottom) {
  text "Ok"
  command proc { p mycheck.value; exit }
  pack('side'=>'left', 'padx'=>10, 'pady'=>10)
}

También podemos asociar variables Ruby al valor de un componente Tk usando un proxy TkVariable. Mostraremos ésto en el siguiente ejemplo. Fíjate cómo se configura TkCheckButton; la documentación dice que la opción variable recibe una var reference como argumento. Para hacer ésto, creamos una referencia a variable Tk mediante TkVariable.new. Al acceder a mycheck.value obtendremos la cadena ``0'' o ``1'' dependiendo de si el cuadro de selección está activado o no. Puedes usar el mismo mecanismo para cualquier componente que soporte una var reference, como los radio buttons y los campos de texto.

mycheck = TkVariable.new

TkCheckButton.new(top) {   variable mycheck   pack('padx'=>5, 'pady'=>5, 'side' => 'left') }

Establecer/obtener opciones dinámicamente

Además de establecer las opciones de un componente cuando se crea, puedes reconfigurarlo mientras se está ejecutando. Todos los componentes disponen del método configure, que recibe una Hash o un bloque de código del mismo modo que new. Podemos modificar el primer ejemplo para cambiar el texto de la etiqueta en respuesta a pulsar en un botón:

lbl = TkLabel.new(top) { justify 'center'
  text    'Hello, World!';
  pack('padx'=>5, 'pady'=>5, 'side' => 'top') }
TkButton.new(top) {
  text "Cancel"
  command proc { lbl.configure('text'=>"Goodbye, Cruel World!") }
  pack('side'=>'right', 'padx'=>10, 'pady'=>10)
}

Ahora cuando el botón Cancel se pulse, el texto de la etiqueta cambiará inmediatamente de ``Hello, World!'' a ``Goodbye, Cruel World!''

También puedes solicitar a los componentes valores concretos de alguna opción mediante cget:

require 'tk'
b = TkButton.new {
  text     "OK"
  justify  'left'
  border   5
}
b.cget('text') » "OK"
b.cget('justify') » "left"
b.cget('border') » 5

Aplicación de muestra

Aquí tenemos un ejemplo un poco más largo, mostrando una aplicación genuina---un generador de ``Latín pig''. Escribe una frase como ``Ruby rules,'' y el botón ``Pig It'' la traducirá instantáneamente a Latín pig.

require 'tk'

class PigBox   def pig(word)     leadingCap = word =~ /^A-Z/     word.downcase!     res = case word       when /^aeiouy/         word+"way"       when /^([^aeiouy]+)(.*)/         $2+$1+"ay"       else         word     end     leadingCap ? res.capitalize : res   end

  def showPig     @text.value = @text.value.split.collect{|w| pig(w)}.join(" ")   end

  def initialize     ph = { 'padx' => 10, 'pady' => 10 }     # common options     p = proc {showPig}

    @text = TkVariable.new     root = TkRoot.new { title "Pig" }     top = TkFrame.new(root)     TkLabel.new(top) {text    'Enter Text:' ; pack(ph) }     @entry = TkEntry.new(top, 'textvariable' => @text)     @entry.pack(ph)     TkButton.new(top) {text 'Pig It'; command p; pack ph}     TkButton.new(top) {text 'Exit'; command {proc exit}; pack ph}     top.pack('fill'=>'both', 'side' =>'top')   end end

PigBox.new Tk.mainloop

Sidebar: Geometry Management

En el código de ejemplo de este capítulo verás referencias al método pack en los componentes. Es una llamada muy importante, desde el momento en que no la pones---quítala y no podrás ver el componente. pack es un comando que le dice al gestor de geometría que coloque el componete de acuerdo a las restricciones que le especifiquemos. Los gestores de geometría reconocen tres comandos:

Comando Posición especificada
pack Flexible, posición basada en restricciones
place Posición absoluta
grid Posición respecto a una tabla (fila/columna)

Como pack es el comando mas usado, será el que utilicemos en nuestros ejemplos.

Asociar eventos

Nuestros componentes están expuestos al mundo real; reciben clicks, el ratón se mueve sobre ellos, el usuario lleva el foco hasta ellos; todas estas cosas, y muchas más, generan eventos que podemos capturar. Puedes crear una asociación entre un evento de un componente en particular y un bloque de código usando el método bind del componente.

Por ejemplo, supongamos que hemos creado un botón que muestra una imagen. Nuestra intención es que la imagen cambie cuando el ratón del usuario esté sobre el botón.

image1 = TkPhotoImage.new { file "img1.gif" }
image2 = TkPhotoImage.new { file "img2.gif" }

b = TkButton.new(@root) {   image    image1   command  proc { doit } }

b.bind("Enter") { b.configure('image'=>image2) } b.bind("Leave") { b.configure('image'=>image1) }

Primero, creamos dos objetos a partir de imágenes GIF que se encuentran en el disco usando TkPhotoImage. Después creamos un botón (nombrado de forma muy clara como ``b''), el cual muestra la imagen image1. A continuación asociamos el evento ``Enter'' para que la imagen mostrada por el botón cambie dinámicamente a image2, y el evento ``Leave'' para volver a la imagen image1.

Este ejemplo muestra los sencillos eventos ``Enter'' y ``Leave''. Pero la cadena indicando el evento evento que se le pasa bind puede estar compuesta de varias subcadenas separadas por dashes, siguiendo el orden modificador-modificador-tipo-detalle. Los modificadores están listados en la referencia de Tk e incluyen Button1, Control, Alt, Shift, y demás. Tipo es el nombre del evento (tomado de los convenios para nombres de X11) e incluye eventos como ButtonPress, KeyPress, y Expose. Detalle es un número entre 1 y 5 para botones o un keysym para entrada por teclado. Por ejemplo, un binding que se ejecute cuando se suelte el botón 1 del ratón mientras la tecla control está pulsada, podría especificarse del siguiente modo:

Control-Button1-ButtonRelease
o
Control-ButtonRelease-1

El evento en si mismo puede contener ciertos campos como el tiempo del evento y las posiciones x e y. bind puede pasar estos elementos al callback, usando event field codes. Se usan siguiendo la especificaciones de printf. Por ejemplo, para obtener las coordenadas x e y en un movimiento de ratón, deberás especificar la llamada a bind con tres parámetros. El segundo parámetro es el Proc para el callback, y el tercero es el event field string.

canvas.bind("Motion", proc{|x, y| do_motion (x, y)}, "%x %y")

Canvas

Tk proporciona un componente Canvas en el cual puedes dibujar y producir salida en PostScript. Aquí mostramos un sencillo código (adaptado de la distribución) que dibujará líneas rectas. Pinchando y manteniendo pulsado el botón 1 comenzará una línea, que será ``redibujada'' a medida que se mueve el ratón. Cuando se suelte el botón 1, la línea se dibujará en esa posición. Al pulsar el botón 2 del ratón se volcará una representación PostScript del canvas de dibujo, más adecuado para la impresión.

require 'tk'

class Draw   def do_press(x, y)     @start_x = x     @start_y = y     @current_line = TkcLine.new(@canvas, x, y, x, y)   end   def do_motion(x, y)     if @current_line       @current_line.coords @start_x, @start_y, x, y     end   end   def do_release(x, y)     if @current_line       @current_line.coords @start_x, @start_y, x, y       @current_line.fill 'black'       @current_line = nil     end   end   def initialize(parent)     @canvas = TkCanvas.new(parent)     @canvas.pack     @start_x = @start_y = 0     @canvas.bind("1", proc{|e| do_press(e.x, e.y)})     @canvas.bind("2", proc{ puts @canvas.postscript({}) })     @canvas.bind("B1-Motion", proc{|x, y| do_motion(x, y)}, "%x %y")     @canvas.bind("ButtonRelease-1",                  proc{|x, y| do_release (x, y)}, "%x %y")   end end

root = TkRoot.new{ title 'Canvas' } Draw.new(root) Tk.mainloop

Después de unos cuantos clicks de ratón, obtendrás una obra maestra instantánea:

Missing screenshots/canvas.ps

``We couldn't find the artist, so we had to hang the picture....''

Scrolling

A no ser que tengas pensado dibujar dibujos muy pequeños, el ejemplo anterior puede que no sea de mucha utilidad. TkCanvas, TkListbox, y TkText pueden configurarse para usar barras de scroll de modo que puedas trabajar en un pequeño subconjunto de un ``gran dibujo''.

La comunicación entre una barra de scroll y un componente es bidireccional. Mover la barra de scroll implica que la vista del componente cambie; pero si la vista del componente cambie por otros medios, la barra de scroll tiene que cambiar también para reflejar la nueva posición.

Ya que no hemos trabajado mucho con listas todavía, nuestro ejemplo de scrolling usará una lista de texto con scrolling. En el siguiente fragmento de código, empezaremos creando una TkListbox normal y corriente. Después crearemos la TkScrollbar. El callback de las barras de scroll (establecido con command) llamarán al método yview del componente lista, que cambiará el valor de la porción visible en la dirección y.

Después de que el callback se configure, hacemos la asociación inversa: cuando la lista se de cuenta de la necesidad de hacer scroll, estableceremos el rango apropiado en la barra de scroll usando TkScrollbar#set. Usaremos el mismo código en un programa completamente funcional en el siguiente capítulo.

list_w = TkListbox.new(frame, 'selectmode' => 'single')

scroll_bar = TkScrollbar.new(frame,                   'command' => proc { |*args| list_w.yview *args })

scroll_bar.pack('side' => 'left', 'fill' => 'y')

list_w.yscrollcommand(proc { |first,last|                              scroll_bar.set(first,last) })

Solo una cosa más

Podriamos continuar con Tk durante cientos de páginas, pero eso ya será en otro libro. El siguiente programa es nuestro ejemplo final de Tk---un sencillo visor de imágenes GIF. Podrás seleccionar un nombre de un archivo GIF de una lista con scroll, y una versión en miniatura de la imagen se mostrará. Solo quedan unos pocos detalles más que nos gustaría puntualizar.

¿Has visto alguna vez una aplicación que crea un ``cursor ocupado'' y luego se olvida de devolverlo a su estado normal? En Ruby hay un sencillo truco que previene estas situaciones. ¿Recuerdas como File.new usaba un bloque para asegurarse de que el archivo se cerraba después de usarlo? Podemos hacer algo similar con el método busy, tal y como se muestra en el siguiente ejemplo.

Este programa también muestra algunas operaciones simples de TkListbox---añadir elementos a la lista, configurar un callback para cuando el botón del ratón se suelte,[Probablemente desearás que sea cuando el botón se suelte, no cuando se pulse, ya que el componente queda seleccionado cuando el botón se pulsa.] y obtener la selección actual.

Hasta ahora hemos usado TkPhotoImage solamente para mostrar iconos directamente, pero también podrías usarlo para hacer zoom, submuestras, y mostrar porciones de imágenes. Aquí usaremos la característica de hacer submuestras para escalar la imagen al visualizarla.

require 'tk'

def busy   begin     $root.cursor "watch" # Set a watch cursor     $root.update # Make sure it updates  the screen     yield # Call the associated block   ensure     $root.cursor "" # Back to original     $root.update   end end

$root = TkRoot.new {title 'Scroll List'} frame = TkFrame.new($root)

list_w = TkListbox.new(frame, 'selectmode' => 'single')

scroll_bar = TkScrollbar.new(frame,                   'command' => proc { |*args| list_w.yview *args })

scroll_bar.pack('side' => 'left', 'fill' => 'y')

list_w.yscrollcommand(proc { |first,last|                              scroll_bar.set(first,last) }) list_w.pack('side'=>'left')

image_w = TkPhotoImage.new TkLabel.new(frame, 'image' => image_w).pack('side'=>'left') frame.pack

list_contents = Dir["screenshots/gifs/*.gif"] list_contents.each {|x|   list_w.insert('end',x) # Insert each file name into the list } list_w.bind("ButtonRelease-1") {   index = list_w.curselection[0]   busy {     tmp_img = TkPhotoImage.new('file'=> list_contents[index])     scale   = tmp_img.height / 100     scale   = 1 if scale < 1     image_w.copy(tmp_img, 'subsample' => [scale,scale])     tmp_img = nil # Be sure to remove it, the     GC.start      # image may have been large   } }

Tk.mainloop

Finalmente, una nota sobre la recolección de basura---imagínate que abrimos un par de archivos GIF muy grandes que teníamos por ahí perdidos[¡Eran documentos técnicos! ¡De verdad!] mientras probamos el código. No nos gustaría tener que cargar con estas imágenes en memoria más del tiempo necesario, por lo que establecemos las referencias a las imágenes a nil y llamamos inmediatamente al recolector de basura para borrar lo que sea necesario.

Translating from Perl/Tk Documentation

Ésto es todo, ya solo depende de ti. En su mayor parte, podrás trasladar de forma sencilla la documentación dada para Perl/Tk a Ruby. Pero existen ciertas excepciones; algunos métodos no están implementados, y existe funcionalidad extra que no está documentada. Mientras no salga un libro Ruby/Tk, lo mejor que puedes hacer es preguntar en el grupo de noticias o leer código fuente.

Pero en general, es bastante sencillo saber como se pueden hacer las cosas. Recuerda que las opciones pueden pasarse como una hash, o en un bloque de código, y que el ámbito del bloque de código está dentro del TkWidget usado, no en la instancia de tu clase.

Creación de objetos

Perl/Tk:  $widget = $parent->Widget( [ option => value ] )
Ruby:     widget = TkWidget.new(parent, option-hash)
          widget = TkWidget.new(parent) { code block }

Puede que no quieras guardar el valor devuelto por un componente recién creado, pero ahí está por si lo deseas. No olvides llamar a pack en los componentes (o cualquiera de las otras llamadas de geometría), o no se mostrarán.

Opciones

Perl/Tk:  -background => color
Ruby:     'background' => color
          { background color }

Recuerda que el ámbito del bloque del código es diferente.

Referencias a variables

Perl/Tk:  -textvariable => \$variable
          -textvariable => varRef
Ruby:     ref = TkVariable.new
          'textvariable' => ref
          { textvariable ref }

Usa TkVariable para asociar una variable Ruby al valor de un componente. Luego podrás usar los métodos de acceso de value en TkVariable (TkVariable#value y TkVariable#value=) para operar sobre el contenido del componente directamente.


Extraído del libro "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. liberado bajo los términos de la Open Publication License V1.0.
La referencia está disponible en: download.