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.
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 DrawWindowo, 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:
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:
Aquí hay otro ejemplo de dibujo, en este caso obtenido del Tutorial de Cairo
É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
.
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 aLa
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.