Dibujando con Cairo: Empezando...

Es posible dibujar figuras complejas en Gtk2Hs, tanto en la propia pantalla como en diferentes formatos de fichero, mediante la librería de dibujo de Cairo. Dibujar en Gtk2Hs no es muy diferente de dibujar en Cairo, pero no está de más desarrollar un tutorial específico para ello.

Puedes consultar el centro de documentación de Cairo.

Para dibujar usando los formatos de Cairo con Gtk2Hs necesitamos una sintaxis especial, ya sea pare realizar dibujos en pantalla, o en diferentes formatos de fichero, portable network graphics (png), portable document format (pdf), postscript (ps) o scaleable vector graphics (svg). El objetivo de este apéndice es explicar las funciones básicas que necesitarás.

1. Dibujar

En primer lugar debemos importar el módulo de dibujo de Cairo con import Graphics.Rendering.Cairo. La siguiente función define el dibujo de un triángulo:

myDraw :: Render ()
myDraw = do
    setSourceRGB 1 1 0
    setLineWidth 5

    moveTo 120 60
    lineTo 60 110
    lineTo 180 110
    closePath
    
    stroke

Esta función es de tipo Render () y a partir de la instrucción "do" puedes inferir que Render es una mónada. Fíjate que no se trata de una mónada IO, que es la usada en Graphics.UI.Gtk . El triángulo queda definido por las funciones moveTo, lineTo and closePath , que hacen lo que su nombre sugiere. (moveTo - mueve a, lineTo - línea a y closePath - cierraCamino). Sin embargo no dibujan nada, sino que definen el camino a seguir. El dibujo se realiza cuando ejecutamos la función stroke (golpe).

Al principio, como puedes ver, debe especificarse el color de las líneas setSourceRGB 1 1 0 y su ancho setLineWidth 5 .

Con todo esto ya se podría dibujar la figura. Para ello necesitamos un widget vacío, de tipo DrawingArea (área de dibujo). Sin embargo no se dibuja en el propio widget, el canvas en el ejemplo siguiente, sino en su DrawWindow (ventana de dibujo). Para lograrlo puedes usar:

widgetGetDrawWindow :: WidgetClass widget => widget -> IO DrawWindow
o, también, este más símple:
drawingAreaGetDrawWindow :: DrawingArea -> IO DrawWindow

Ahora puedes usar:

renderWithDrawable :: DrawableClass drawable => drawable -> Render a -> IO a

El dibujo debe realizarse en respuesta a un evento. Una posibilidad es un evento asociado a un botón, onButtonPress como el que vimos en el capítulo 6.2. !Esto ya funciona! Cada vez que se modifica el tamaño de la ventana, el dibujo desaparece y se vuelve a dibujar cuando vuelves a pulsar el botón. Sin embargo dispones de otro evento, el Expose (mostrar), que envía una señal cada vez que la ventana cambia de tamaño o se redibuja en la pantalla. Esto quizá se aproxime más a lo que buscas, así que usa:

onExpose canvas (\x -> do renderWithDrawable drawin myDraw
                          return (eventSent x))

Esto es lo que hemos desarrollado hasta ahora:

Appendix 1 Example a

Se ha incluído también un marco, para que se vea más bonito, aunque es innecesario. Fíjate en que la función widgetShowAll aparece antes de la función widgetGetDrawWindow en el código de abajo. Esto es necesario debido a que sólo se puede dibujar en una ventana visible!

import Graphics.UI.Gtk
import Graphics.Rendering.Cairo

main :: IO ()
main= do
     initGUI
     window <- windowNew
     set window [windowTitle := "Hello Cairo",
                 windowDefaultWidth := 300, windowDefaultHeight := 200,
                 containerBorderWidth := 30 ]

     frame <- frameNew
     containerAdd window frame
     canvas <- drawingAreaNew
     containerAdd frame canvas
     widgetModifyBg canvas StateNormal (Color 65535 65535 65535)

     widgetShowAll window 
     drawin <- widgetGetDrawWindow canvas
     onExpose canvas (\x -> do renderWithDrawable drawin myDraw
                               return (eventSent x))
    
     onDestroy window mainQuit
     mainGUI

myDraw :: Render ()
myDraw = do
    setSourceRGB 1 1 0
    setLineWidth 5

    moveTo 120 60
    lineTo 60 110
    lineTo 180 110
    closePath

    stroke

Quizá este ejemplo no sea realmente lo que buscas, ya que, aunque la figura es redibujada, no se ajusta al nuevo tamaño de la ventana principal. Para conseguir ese efecto necesitaríamos:

myDraw :: Double -> Double -> Render ()
myDraw w h = do
    setSourceRGB 1 1 1
    paint

    setSourceRGB 1 1 0
    setLineWidth 5

    moveTo (0.5 * w) (0.43 * h)
    lineTo (0.33 * w) (0.71 * h)
    lineTo (0.66 * w) (0.71 * h)
    closePath
    stroke

Ahora el dibujo siempre se ajustará a los bordes definidos por los parámetros. Además hemos fijado el color de fondo con la función paint, en vez de con widgetModify . La función paint pinta con el color actual todo lo que esté dentro del área de dibujo (clip region). Fíjate en que setSourceRGB no sólo emplea un valor de tipo Double con un rango entre 0 y 1, en vez de un valor de tipo Int entre 0 y 65535, sino que además usa una mónada de tipo Render en vez de una mónada de tipo IO .

Para dibujar la figura adaptable, necesitamos obtener el tamaño del área de dibujo cada vez que cambie.

widgetGetSize :: WidgetClass widget => widget -> IO (Int, Int)

Así que el código para dibujar se convierte en:

onExpose canvas (\x -> do (w,h) <- widgetGetSize canvas
                          drw <- widgetGetDrawWindow canvas
                          renderWithDrawable drw (myDraw (fromIntegral w) (fromIntegral h))
                          return (eventSent x))

Como el resto del código del ejemplo permanece como antes, no lo listamos. Este sería el resultado de modificar el ancho de la ventana:

Appendix 1 Example b

Aquí hay otro ejemplo de dibujo, en este caso obtenido del Tutorial de Cairo

Appendix 1 Example 3

Éste es el listado:

import Graphics.UI.Gtk  hiding (fill)
import Graphics.Rendering.Cairo

main :: IO ()
main= do
     initGUI
     window <- windowNew
     set window [windowTitle := "Hello Cairo 4",
                 windowDefaultWidth := 300, windowDefaultHeight := 200,
                 containerBorderWidth := 15 ]

     frame <- frameNew
     containerAdd window frame
     canvas <- drawingAreaNew
     containerAdd frame canvas

     widgetShowAll window 
     onExpose canvas (\x ->  do (w,h) <- widgetGetSize canvas
                                drawin <- widgetGetDrawWindow canvas
                                renderWithDrawable drawin 
                                    (myDraw (fromIntegral w)(fromIntegral h))
                                return (eventSent x))
    
     onDestroy window mainQuit
     mainGUI

myDraw :: Double -> Double -> Render ()
myDraw w h = do
           setSourceRGB 1 1 1
           paint

           setSourceRGB 0 0 0
           moveTo 0 0
           lineTo w h
           moveTo w 0
           lineTo 0 h
           setLineWidth (0.1 * (h + w))
           stroke

           rectangle 0 0 (0.5 * w) (0.5 * h)
           setSourceRGBA 1 0 0 0.8
           fill

           rectangle 0 (0.5 * h) (0.5 * w) (0.5 * h)
           setSourceRGBA 0 1 0 0.6
           fill

           rectangle (0.5 * w) 0 (0.5 * w) (0.5 * h)
           setSourceRGBA 0 0 1 0.4
           fill

Comprueba que es igual que el ejemplo anterior, excepto por el dibujo. Introduce la función setSourceRGBA que no sólo establece el color sino que también establece la transparencia, como un valor entre 0 and 1. El ejemplo también emplea la función rectangle (rectángulo) y un método fill (llenar) que "llena" las figuras cerradas con el color y la transparencia especificadas.

Nota: Debido a que existe un conflicto de nombres con una antigua librería de Gtk2Hs, debes o ocultar fill en la importación de Graphics.UI.Gtk, o usar el nombre completo Graphics.Rendering.Cairo.fill .

2. Escribir en ficheros

Es muy sencillo guardar un dibujo en un formato png, pdf, ps o svg. Más que guardar deberíamos hablar de renderizar, ya que cada formato necesita su propia renderización. La función es:

renderWith:: MonadIO m => Surface -> Render a -> m a
La Surface (superficie) es el lugar en quye aparecerá el dibujo. Es estos casos no se trata de la pantalla sino de algo que debes proporcionar. Hay cuatro funciones diferentes, una por cada tipo de fichero.
withImageSurface
:: Format	                -- formato de píxeles en la superficie a crear
-> Int	                        -- ancho de la superficie, en píxeles
-> Int	                        -- alto de la superficie, en píxeles
-> (Surface -> IO a)	        -- una acción que podría usar la superficie. La superficie es sólo válida para esta acción.
-> IO a	

Para los ficheros en formato "portable network graphics (png)" usamos esta función. El tipo de datos Format tiene cuatro constructores posibles, FormatARGB32, FormatRGB24, FormatA8, FormatA1 . En el ejemplo inferior usamos el primero. La acción que emplea una Surface como su argumento suele ser la función renderWith seguida de una función para escribir en el fichero. Así para el formato png esta podría ser la función:

surfaceWriteToPNG
:: Surface	                    -- una Superficie
-> FilePath	                    -- el nombre del fichero en el que vamos a escribir
-> IO ()

Así, la receta para escribir en dibujo en un fichero en formato png podría ser:

withImageSurface  FormatARGB32 pnw pnh (\srf -> do renderWith srf (myDraw (fromIntegral pnw) (fromIntegral pnh))
                                                   surfaceWriteToPNG srf "myDraw.png")

donde pnw y pnh son el ancho y el alto (tipos Int) .

Para guardar el dibujo en un fichero en formato pdf, puedes usar:

withPDFSurface
:: FilePath	               -- un nombre de fichero para la salida PDF (debes poder escribir)
-> Double	               -- ancho de la superficie, en puntos (1 punto == 1/72.0 inch)
-> Double	               -- alto de la superficie, en puntos (1 punto == 1/72.0 inch)
-> (Surface -> IO a)	       -- una acción que pueda usar la superficie. La superficie sólo vale para esta acción.
-> IO a

Esta función emplea diferentes parámetros que la anterior, aunque es muy parecida. La receta para guardar el fichero es ahora:

withPDFSurface "myDraw.pdf" pdw pdh (\s ->  renderWith s $ do myDraw pdw pdh
                                                              showPage )

Fíjate en que la función showPage . GHC y GHCi dará el código como válido pero el lector de pdf no será capaz de leer el resultado, indicando que que no hay ninguna página. La documentación de la API indica que el ancho y el largo se establecen en puntos (y tipo Double ), así que debes comprobar si esto funciona en la práctica.

Para guardar un archivo Postscript:

withPSSurface
:: FilePath	                 -- un nombre de fichero para la salida PS (debes poder escribir)
-> Double	                 -- ancho de la superficie, en puntos (1 punto == 1/72.0 inch)
-> Double	                 -- alto de la superficie, en puntos (1 punto == 1/72.0 inch)
-> (Surface -> IO a)	         -- una acción que pueda usar la superficie. La superficie sólo vale para esta acción.
-> IO a

Para salvarlo puedes usar la misma receta que antes o la versión más reducida:

withPSSurface "myDraw.ps" psw psh (flip renderWith (myDraw psw psh >> showPage))

Por último, para salvarlo en el formato "scaleable vector graphics", debes usar la misma sintaxis, pero con withSVGSurface . Así que esto sería:

withSVGSurface "myDraw.svg" pgw pgh (flip renderWith $ myDraw pgw pgh >> showPage)

A continuación he puesto un ejemplo que guarda el último dibujo mostrado arriba en los cuatro formatos (con diferentes tamaños):

import Graphics.UI.Gtk  hiding (fill)
import Graphics.Rendering.Cairo

main :: IO ()
main= do
     initGUI
     window <- windowNew
     set window [windowTitle := "Save as...",
                 windowDefaultWidth := 300, windowDefaultHeight := 200] 

     let pnw = 300
         pnh = 200
     withImageSurface 
       FormatARGB32 pnw pnh (\srf -> do renderWith srf (myDraw (fromIntegral pnw) (fromIntegral pnh))
                                        surfaceWriteToPNG srf "myDraw.png")


     let pdw = 720
         pdh = 720
     withPDFSurface "myDraw.pdf" pdw pdh (\s ->  renderWith s $ do myDraw pdw pdh
                                                                   showPage )
     
     let psw = 360
         psh = 540
     withPSSurface 
        "myDraw.ps" psw psh (flip renderWith (myDraw psw psh >> showPage))

     let pgw = 180
         pgh = 360
     withSVGSurface 
        "myDraw.svg" pgw pgh (flip renderWith $ myDraw pgw pgh >> showPage)
         
     putStrLn "Press any key to quit..."
     onKeyPress window (\x -> do widgetDestroy window
                                 return (eventSent x))

     widgetShowAll window
     onDestroy window mainQuit
     mainGUI

myDraw :: Double -> Double -> Render ()
myDraw w h = do
           setSourceRGB 1 1 1
           paint

           setSourceRGB 0 0 0
           moveTo 0 0
           lineTo w h
           moveTo w 0
           lineTo 0 h
           setLineWidth (0.1 * (h + w))
           stroke

           rectangle 0 0 (0.5 * w) (0.5 * h)
           setSourceRGBA 1 0 0 0.8
           fill

           rectangle 0 (0.5 * h) (0.5 * w) (0.5 * h)
           setSourceRGBA 0 1 0 0.6
           fill

           rectangle (0.5 * w) 0 (0.5 * w) (0.5 * h)
           setSourceRGBA 0 0 1 0.4
           fill

Nota: Por favor, consulta la documentación de la API del Graphics.Rendering.Cairo y los tutoriales y ejemplos de Cairo para usos más avanzados. La distribución de Gtk2Hs también viene con varios ejemplos interesantes.