GUIs con Haskell en entornos Windows (III)

GUIs con Haskell en entornos Windows (III)

25 enero 2020 0 Por Juan Martin

GtkBuilder y Data Stores

Otra forma de crear ventanas es a traves de GtkBuilder y la aplicacion Glade. Y veremos tambien como funcionan los Data Stores para conectar componentes graficos a fuentes de datos.

Componentes Necesarios

En las dos entradas anteriores (primera parte y segunda parte) vimos como crear aplicaciones GTK+ desde codigo, sin el uso de asistentes. En este veremos como hacerlo con un diseniador de ventanas.

Para poder crear de forma visual nuestras ventanas con Gtk necesitaremos del programa Glade, que nos podemos descargar desde https://glade.gnome.org/, ademas de la instalacion que ya teniamos para el desarrollo en Haskell.

Glade Interface

Aspecto del diseniador Glade

El Proyecto

Crearemos un proyecto con stack new con y anadiremos las siguientes dependencias en el cabal


build-depends:
      base
    , transformers
    , text
    , haskell-gi-base
    , gi-gtk

con las opciones de compilacion que ya vimos en los anteriores proyectos de la serie


ghc-options: -threaded -rtsopts -optl-mwindows -with-rtsopts=-N

Tambien crearemos una carpeta, de nombre ui en nuestra carpeta de proyecto para almacenar los ficheros generados con Glade.

GtkBuilder

En el proyecto que acompania al articulo he creado una ventana con un componente Entry, un ComboBox y un TreeView, ademas de un boton para salir.

Esto lo he realizado con el programa Glade y guardado en formato GtkBuilder en la mencionada carpeta ui de nuestro proyecto.

Empezamos inicializando Gtk con una llamada a Gtk.init Nothing para despues incializar los Stores de datos (que veremos mas adelante). Despues cargamos la ventana con GtkBuilder


-- Creamos un GtkBuilder y leemos el fichero que hemos creado con Glade
builder <- Gtk.builderNew
Gtk.builderAddFromFile builder "ui/form.ui"

-- Nos hacemos con algunos componentes. builderGetObject devuelve un GObject que hay que castear
win <- Gtk.builderGetObject builder "window" >>= unsafeCastTo Gtk.Window . fromJust
salir <- Gtk.builderGetObject builder "salir" >>= unsafeCastTo Gtk.Button . fromJust
combobox <- Gtk.builderGetObject builder "combo" >>= unsafeCastTo Gtk.ComboBox . fromJust
listbox <- Gtk.builderGetObject builder "listbox" >>= unsafeCastTo Gtk.TreeView . fromJust

y conseguimos unas referencias a los componentes que vamos a usar de forma programatica.

builderNew crea una nueva instancia de GtkBuilder mientras que builderAddFromFile carga el fichero especificado en el builder.
Para poder acceder a los diversos componentes usamos builderGetObject que nos devolvera el componente especificado por el texto en forma de GObject y que debemos convertir al tipo necesario a traves de unsafeCastTo. builderGetObject devuelve un Maybe Object que hay que sacar del Maybe con fromJust y realizar el cast con unsafeCastTo. En caso de que no encuentre el componente no dara error de compilacion sino que en tiempo de ejecucion os saldra una ventana de mensajes con un texto que pondra fromJust: Nothing para a continuacion salir del programa al cerrar el dialogo.

El Modelo

Ciertos componentes Gtk pueden coger sus datos a traves de un modelo de acceso a datos MVC. La parte del modelo se controla a traves de dos Interfaces, TreeModel y ListModel que estan implementados en dos clases, TreeStore y ListStore. Estas ultimas son la que gestionan el almacenamiento, en memoria, de los datos del modelo mientras que las otras sirven como interfaz entre el modelo y el componente.
A esto hay que aniadirle la parte de la vista, generalmente instancias de CellRenderer o clases derivadas.
Los componentes mas simples como GtkEntry o GtkTextView no disponen de soporte para los modelos de datos aunque esto no es impedimento para tenerlos sincronizados con un dataset en caso de ser necesario.

Para nuestro ejemplo hemos creado cuatro funciones para crear los datos de ejemplo.

-- Funcion para crear una fila para el modelo de la combo
setComboRow :: Gtk.TreeStore -> Text -> IO ()
setComboRow store value = do
    -- Creamos un iterador
    iter <- Gtk.treeStoreAppend store Nothing
    -- Creamos un valor GValue
    gval <- newGValue gtypeString -- Asignamos el valor set_string gval (Just value) -- y lo guardamos en el modelo Gtk.treeStoreSetValue store iter 0 gval return () -- Funcion para crear una fila para el modelo de la Lista setListRow :: Gtk.TreeStore -> Int32 -> Text -> IO ()
setListRow store ivalue value = do
    iter <- Gtk.treeStoreAppend store Nothing

    gvali <- newGValue gtypeInt
    -- No hay equivalente a Int en el API
    set_int gvali $ CInt ivalue

    gval <- newGValue gtypeString
    set_string gval (Just value)
       
    Gtk.treeStoreSetValue store iter 0 gvali
    Gtk.treeStoreSetValue store iter 1 gval
    
    return ()

-- Funcion para crear datos para el modelo de la combo
modelCombobox :: IO Gtk.TreeStore
modelCombobox = do
    store <- Gtk.treeStoreNew [gtypeString]    
    setComboRow store "Uno"
    setComboRow store "Dos"
    setComboRow store "Tres"
    
    return store

-- Funcion para crear datos para el modelo de la Lista
modelListbox :: IO Gtk.TreeStore
modelListbox = do
    -- Vamos a usar dos columnas
    store <- Gtk.treeStoreNew [gtypeInt, gtypeString]    
    setListRow store 1 "Uno"
    setListRow store 2 "Dos"
    setListRow store 3 "Tres"

    return store

setComboRow y setListRow crean una fila en cada modelo, basado en una estructura del tipo GtkTreeIter que referencia un nodo dentro del modelo. Hay otro similar, GtkTreePath que hace lo mismo referenciando ademas de la fila la columna aportando un mayor nivel de granularidad. La unica diferencia entre ambas funciones es que setListRow genera dos columnas en lugar de una.
modelComboBox y modelListBox crean el store a traves de treeStoreNew y llaman a las dos funciones anteriores para crear los datos de ejemplo.

La estrategia de setXXXRow es basica. Creamos una nueva entrada con treeStoreAppend, creamos los valores en forma de GValue y los aniadimos al iterador obtenido al crear la nueva entrada, asignando el valor a la columna deseada con treeStoresetValue.

Como mencionamos anteriormente, despues del Gtk.init inicializamos los modelos


-- Generamos los datos de ambos modelos
comboModel <- modelCombobox >>= toTreeModel
listModel <- modelListbox >>= toTreeModel

Y con ello ya tenemos casi todas la piezas en el tablero.

La Vista

Ya hemos visto como crear la parte del Modelo. Ahora pasaremos a ver como crear la parte de la Vista para los componentes que la requieren, esto es, GtkTreeView y GtkIconview. GtkListBox puede crearse desde el codigo pero no puede ser aniadido desde el editor Glade aunque es mas practico usar un GtkTreeView y usarlo como componentes para listas.

La Vista necesita de dos componentes adicionales a los ya vistos, el modelo y el propio componente, a saber: una vista de columna (GtkViewColumn) y un renderer para dicha vista.
El renderer es el elemento que dibuja nuestros datos en el componente y la vista de columna es el componente que organiza las columnas que se van a mostrar.

GtkComboBox no observa este comportamiento y en lugar de la vista de columna utiliza directamente los renderers a traves de la implementacion de GtkCellLayout y sus metodos packStart y packEnd.

En nuestro caso el codigo queda asi


-- Ajustamos el render del Combo

-- Primero creamos un renderer para Texto
cell <- Gtk.cellRendererTextNew

-- Aniadimos los renderers al Layout y le decimos a que propiedad vamos a enlazar el modelo
cellLayoutPackStart combobox cell True
cellLayoutAddAttribute combobox cell "text" 0

-- Seleccionamos la primera fila del modelo
Gtk.comboBoxSetActive combobox 0


Creamos un renderer para texto. Los hay ademas para graficos (GtkCellRendererPixBug) y para un boton interruptor (toggle, GtkCellRendererToggle).
Con cellLayoutAddAttribute le decimos a que columna va a enlazar que propiedad, en este caso su propiedad text.

La lista tiene algo mas de miga


-- Primero creamos los renderers
cellInt <- Gtk.cellRendererTextNew
cellText <- Gtk.cellRendererTextNew

-- Creamos las columnas
tvcol1 <- Gtk.treeViewColumnNew
tvcol2 <- Gtk.treeViewColumnNew

-- Titulos de las columnas
set tvcol1 [#title := "Indice"]
set tvcol2 [#title := "Texto"]

-- Aniadimos los renderers a las columnas
Gtk.treeViewColumnPackStart tvcol1 cellInt True
Gtk.treeViewColumnPackStart tvcol2 cellText True
    
-- Le decimos a que propiedad vamos a enlazar el modelo
Gtk.treeViewColumnAddAttribute tvcol1 cellInt "text" 0
Gtk.treeViewColumnAddAttribute tvcol2 cellText "text" 1
    
-- Aniadimos la columna a la Lista
Gtk.treeViewAppendColumn listbox tvcol1
Gtk.treeViewAppendColumn listbox tvcol2

En lugar de aniadir los renderers directamente al componente en el caso de la lista los gestionamos con GtkTreeViewColumn que terminamos aniadiendo al componente con treeViewAppendColumn.

Finalmente asigamos los modelos a los componentes


-- Asignamos los modelos
set combobox [#model := comboModel]
set listbox [#model := listModel]

Conclusiones y Proximos pasos

Como hemos visto a lo largo de la serie es posible desarrollar aplicaciones graficas en Haskell para Windows sin mucho esfuerzo. Aunque la documentacion sobre Gtk esta ejemplificada para el lenguaje C no es muy dificil seguirle la pista con la documentacion de las librerias Haskell que hay en hackage.
En cuanto a usar el GtkBuilder o crear los componentes por codigo, eso va en gustos. Si la apariencia es importante Glade nos puede dar ese toque final sin complicarnos la vida. Si no lo es hacerlo por codigo supone unas pocas lineas mas de codigo.

En una proxima entrega veremos la libreria gi-gtk-declarative y la forma de hacer GUIs a la Haskell way

Un saludo y hasta la proxima.