4.2 Ajustes, escalado y rango

Gtk2Hs tiene varios widgets que pueden ser ajustados visualmente por el usuario usando el ratón o el teclado, como los widgets rango, descritos en su sección. También hay algunos widgets que muestran una porción de un área de datos mayor, como el widget text y el widget viewport. Esta porción también puede ser ajustada por el usuario.

Una aplicación necesita ser capaz de reaccionar a los cambios que hace el usuario en el rango. Un modo de hacerlo podría ser haciendo que cada widget emitiera su propio tipo de señal cuando cambiase su ajuste. Pero puede que quisieras enlazar los ajustes de varios widgets juntos, de modo que al ajustar uno se ajustasen los otros. El ejemplo más claro de esto es la conexión de un scrollbar (barra de deslizamiento) a un panning viewport (ventana de mostrar imágenes con una subpantalla) o a un área de texto con scroll.

El objeto Adjustment (ajuste) se puede usar para almacenar los parámetros y valores de configuración de los widgets de rango, como los scrollbars y controles de escalado. Como Adjustment se deriva de GObject y Object, los ajustes pueden emitir señales, que pueden ser usadas no sólo para que tu programa reaccione a las entradas del usuario en los widgets ajustables, sino también para propagar los valores de ajuste de un modo transparente entre widgets ajustables.

Muchos de los widgets que usan objetos Adjustment, como ScrolledWindow, pueden crear sus propios objetos Adjustment, pero tu mismo los puedes crear con:

adjustmentNew :: Double        -- valor            - El valor inicial del rango
              -> Double        -- mínimo           - Valor mínimo del rango
              -> Double        -- máximo           - Valor máximo del rango
              -> Double        -- incrementoPaso   - El menor de dos posibles incrementos
              -> Double        -- incrementoPágina - El mayor de dos posibles incrementos
              -> Double        -- tamañoPágina     - El tamaño del área visible
              -> IO Adjustment

La función de creación necesita un valor para cada información contenida en el objeto: valor Es el valor inicial y debe estar contenido entre el maximo y el mínimo. máximo y mínimo fijan el margen del deslizador. Pulsando en las flechas se incrementa el valor en incrementPaso. Pulsando en el deslizador (slider) avanza un incrementoPágina. El tamañoPágina se necesita para determinar si el final del deslizador se mantiene en el rango. Puedes conseguir y establecer todos los parámetros de un Adjustment por métodos específicos o usando las funciones generales set y get en los atributos de Adjustment.

onValueChanged :: Adjustment -> IO () -> IO (ConnectId Adjustment)

es la señal emitida cuando el "valor" de un Adjustment cambia, y:

onAdjChanged :: Adjustment -> IO () -> IO (ConnectId Adjustment)

es la señal emitida cuando cambian uno o más de los otros atributos (diferentes de "valor") de un Adjustment.

Widgets de escala y rango

Los widgets de escalado se usan para permitir al usuario la selección visual y la manipulación del valor dentro de un rango especificado usando un deslizador. Podrías querer usar un widget escalable, por ejemplo, para ajustar el nivel de ampliación en una previsualización reducida de una imagen, o para controlar el brillo de un color o para especificar el número de minutos de inactividad antes de que un salvapantallas se active.

Las siguientes funciones crean widgets de escalado vertical y horizontal respectivamente:

vScaleNew :: Adjustment -> IO VScale

hScaleNew :: Adjustment -> IO Hscale

También hay dos constructores, que no utilizan el Adjustment:

vScaleNewWithRange :: Double ->. Double -> Double -> IO VScale

hScaleNewWithRange :: Double ->. Double -> Double -> IO Hscale

Los parámetros de tipo Double se refieren a los valores máximo, mínimo y el paso. El incremento del paso (preferiblemente en potencias de 10) es el valor en que se mueve la escala cuando se usan las teclas de cursor (flechas).

Las escalas horizontal y vertical son instancias de ScaleClass y su comportamiento común está definido en el módulo Graphics.UI.Gtk.Abstract.Scale.

Los widgets de escalado pueden mostrar su valor actual como un número junto al canal. (El área donde se mueve el deslizador) El comportamiento por defecto es mostrar el valor, pero puedes modificarlo con la función:

scaleSetDrawValue :: ScaleClass self => self -> Bool -> IO ()

El valor mostrado por un widget de escalado se redondea en un punto decimal por defecto, igual que el ajuste del campo valor. Esto se puede cambiar con:

scaleSetDigits :: ScaleClass self => self -> Int -> IO ()

Finalmente, el valor puede ser dibujado en diferentes posiciones relativas al canal:

scaleSetValuePos :: ScaleClass self => self -> PositionType -> IO ()

La PositionType (tipo de posición) se define como:

data PositionType = PosLeft | PosRight | PosTop | PosBottom

Scale hereda diferentes métodos de su clase base que es: Range.

Estableciendo la política de actualizaciones

La política de actualizaciones de un widget rango define los puntos a los que, durante la interacción con el usuario, cambiará el campo valor de su Adjustment y emitirá la señal onRangeValueChanged de su Adjustment. La política de actualizaciones esta definida por el UpdateType, que tiene tres constructores:

UpdateContinuous (Actualización continua )
Es el valor por defecto. La señal onRangeValueChanged se emite continuamente, p.ej., cuando el deslizador se mueve un valor mínimo.
UpdateDiscontinuous (Actualización discontinua)
La señal onRangeValueChanged sólo se emite cuando el deslizador se ha detenido y el usuario ha levantado el botón del ratón.
UpdateDelayed (Actualización retrasada)
La señal onRangeValueChanged se emite cuando el usuario levanta el botón del ratón o si el deslizador se para de mover durante un corto periodo de tiempo.

La política de actualizaciones de un widget range se puede establecer por:

rangeSetUpdatePolicy :: RangeClass self => self -> UpdateType -> IO ()

Obteniendo y estableciendo los ajustes

La obtención y el ajuste de un widget rango se hace sobre la marcha con:

rangeGetAdjustment :: RangeClass self => self -> IO Adjustment

rangeSetAdjustment :: RangeClass self => self -> Adjustment -> IO ()

rangeSetAdjustment no hace nada si le pasas el objeto de Adjustment que ya está usando, independientemente de que alguno de los valores de sus atributos haya cambiado. Si le pasas un nuevo código de Adjustment, desconectará el antiguo, si existía (posiblemente detruyéndolo), conectará las señales apropiadas al nuevo, y llamará a la función privada gtk_range_adjustment_changed(), que (o al menos se supone que...) recalculará el tamaño y/o posición del deslizador y lo dibujará si fuera necesario. Como se ha mencionado en la sección de ajustes, si quieres volver a usar el mismo Adjustment, cuando modificas sus valores directamente, deberías emitir la señal changed (cambiado) sobre él.

Interacción con teclado y ratón

Todos los widgets de rango de Gtk2Hs reaccionan a las pulsaciones de ratón más o menos de la misma manera. Pulsando del botón 1 del ratón en el canal originará que el ajuste de incrementoPaso se añada o sustraiga de su valor, y el deslizador se mueva de acuerdo a ello. Pulsando el botón 2 del ratón en el canal moverá el deslizador al punto en que se pulsó el botón. Pulsando el tercer botón en el canal o en las flechas del scroll causará que el valor del Adjustment cambie en el valor de incrementoPaso.

Nota: Esto no funciona en Linux Fedora 6 con la configuración estándar del ratón.

No se puede hacer que las barras de scroll tengan el foco, así que no disponen de atajos de teclado. Para los otros widgets de rango, (que sólo están activos cuando el widget tiene el foco) no diferencian entre widgets de rango vertical u horizontal.

Todos los widgets de rango se pueden usar con las flechas de izquierda, derecha, arriba y abajo, así como con las teclas AvPág y RePág. Las flechas mueven el deslizador arriba y abajo en incrementoPaso, mientras AvPág y RePág lo mueven en incrementoPágina. Inicio y Fin lo mueven al principio y al final del canal.

Ejemplo

Este ejemplo crea una ventana con tres widgets de rango conectados al mismo objeto Adjustment, y un par de controles para ajustar algunos de los parámetros mencionados antes, de modo que puedas ver como afecta al modo en que dichos widgets trabajan.

Range widgets example

Las tres escalas se sitúan de modo que la barra vertical está junto a las dos horizontales, una sobre la otra. Así que necesitamos una caja horizontal para la escala vertical y una caja vertical junto a ella para las escalas horizontales. Las escalas y las cajas deben empaquetarse con PackGrow de modo que las escalas actualicen su tamaño con el de la caja principal, que es una caja vertical en la ventana.

Todas las escalas se construyen con el mismo Adjustment, estableciendo el valor inicial en 0.0, el valor mínimo en 0.0, el máximo en 101.0, el incremento del paso en 0.1, el incremento de página en 1.0 y el tamaño de la página en 1.0.

  adj1 <- adjustmentNew 0.0 0.0 101.0 0.1 1.0 1.0

El usuario puede controlar cuando se muestran los valores de escalado con un checkButton. Esto se empaqueta en la caja principal y se establece para estar activo inicialmente. Un botón check es un botón toggle, así que cuando el usuario lo marca o lo desmarca, se envía la señal onToggled. Esto origina que la función toggleDisplay se evalúe. La función toggleDisplay queda definida así:

toggleDisplay :: ScaleClass self => CheckButton -> [self] -> IO ()
toggleDisplay b scls = sequence_ (map change scls) where
                         change sc = do st <- toggleButtonGetActive b
                                        scaleSetDrawValue sc st

La función tiene un parámetro tipo checkButton, y una lista de instancias de ScaleClass. Sin embargo, una lista sólo puede contener valores del mismo tipo, y vScale y hScale son tipos diferentes. Por lo tanto, usamos la función en listas de escalas horizontales o verticales, pero si la lista contuviera los dos tipos se originaría un error de tipado.

El usuario puede elegir el positionType mediante un widget del que todavía no hemos hablado, un ComboBox. Esto permite una selección de elecciones como se muestra más abajo. El primero en establecerse como activo se determina por un índice, 0 aquí (el primero).

makeOpt1 :: IO ComboBox
makeOpt1 = do
  cb <- comboBoxNewText
  comboBoxAppendText cb "TOP"
  comboBoxAppendText cb "BOTTOM"
  comboBoxAppendText cb "LEFT"
  comboBoxAppendText cb "RIGHT"
  comboBoxSetActive cb 0
  return cb

Un segundo comboBox permite que el usuario seleccione la política de actualizaciones, alguno de los tres constructores de UpdateType.

makeOpt2 :: IO ComboBox
makeOpt2 = do
  cb <- comboBoxNewText
  comboBoxAppendText cb "Continuous"
  comboBoxAppendText cb "Discontinuous"
  comboBoxAppendText cb "Delayed"
  comboBoxSetActive cb 0
  return cb

Las cajas combo (Combo box) por si mismas sólo muestran texto. Para seleccionar la posición, definimos:

setScalePos :: ScaleClass self => ComboBox -> self -> IO ()
setScalePos cb sc = do
    ntxt <- comboBoxGetActiveText cb
    let pos = case ntxt of
                (Just "TOP")    -> PosTop
                (Just "BOTTOM") -> PosBottom
                (Just "LEFT")   -> PosLeft
                (Just "RIGHT")  -> PosRight
                Nothing         -> error "setScalePos: no position set"
    scaleSetValuePos sc pos

setUpdatePol :: RangeClass self => ComboBox -> self -> IO ()
setUpdatePol cb sc = do
    ntxt <- comboBoxGetActiveText cb
    let pol = case ntxt of
                (Just "Continuous")    -> UpdateContinuous
                (Just "Discontinuous") -> UpdateDiscontinuous
                (Just "Delayed")       -> UpdateDelayed
                Nothing                -> error "setUpdatePol: no policy set"
    rangeSetUpdatePolicy sc pol

Aquí no hemos usado listas para gestionar las escalas verticales y horizontales, así que accedemos por separado a cada escala horizontal.

El número de precisión mostrado por las tres escalas debe ser manejado con otra escala, para la que usamos un nuevo objeto Adjustment. La precisión máxima es 5 y cada incremento es 1. La precisión de la propia escala de control se establece a 1.

  adj2 <- adjustmentNew 1.0 0.0 5.0 1.0 1.0 0.0

Cuando cambia el Adjustment de control, se emite la señal onValueChanged y se evalúa la función setDigits.

setDigits :: ScaleClass self => self -> Adjustment -> IO ()
setDigits sc adj = do val <- get adj adjustmentValue
                      set sc [scaleDigits := (round val)]

Aquí usamos las funciones generales set y get sobre los atributos; podríamos haber usado funciones específicas también. fíjate en que el valor de tipo Double del valor de ajuste, se debe redondear a un valor de tipo Integral.

Usamos otra escala horizontal para gestionar el tamaño de página de las tres escalas del ejemplo. Cuando lo ponemos a 0.0, las escalas pueden alcanzar su máximo inicial de 100.0 y cuando lo ponemos a 100.0 las escalas quedan fijas en su menor valor. Esto implica el ajuste del adjustment por una señal de onValueChanged desde un tercer adjustment mediante este trozo de código:

  onValueChanged adj3 $ do val <- adjustmentGetValue adj3
                           adjustmentSetPageSize adj1 val

La función main es:

import Graphics.UI.Gtk

main :: IO ()
main = do
  initGUI
  window  <- windowNew
  set window [windowTitle := "range controls",
              windowDefaultWidth := 250]
  mainbox <- vBoxNew False 10
  containerAdd window mainbox
  containerSetBorderWidth mainbox 10

  box1 <- hBoxNew False 0
  boxPackStart mainbox box1 PackGrow 0
  adj1 <- adjustmentNew 0.0 0.0 101.0 0.1 1.0 1.0
  vsc  <- vScaleNew adj1
  boxPackStart box1 vsc PackGrow 0

  box2 <- vBoxNew False 0
  boxPackStart box1 box2 PackGrow 0

  hsc1 <- hScaleNew adj1
  boxPackStart box2 hsc1 PackGrow 0
  hsc2 <- hScaleNew adj1
  boxPackStart box2 hsc2 PackGrow 0

  chb <- checkButtonNewWithLabel "Display Value on Scale Widgets"
  boxPackStart mainbox chb PackNatural 10
  toggleButtonSetActive chb True

  box3   <- hBoxNew False 10
  boxPackStart mainbox box3 PackNatural 0
  label1 <- labelNew (Just "Scale Value Position:")
  boxPackStart box3 label1 PackNatural 0
  opt1   <- makeOpt1
  boxPackStart box3 opt1 PackNatural 0

  box4   <- hBoxNew False 10
  boxPackStart mainbox box4 PackNatural 0
  label2 <- labelNew (Just "Scale Update Policy:")
  boxPackStart box4 label2 PackNatural 0
  opt2   <- makeOpt2
  boxPackStart box4 opt2 PackNatural 0

  adj2 <- adjustmentNew 1.0 0.0 5.0 1.0 1.0 0.0

  box5   <- hBoxNew False 0
  containerSetBorderWidth box5 10
  boxPackStart mainbox box5 PackGrow 0
  label3 <- labelNew (Just "Scale Digits:")
  boxPackStart box5 label3 PackNatural 10
  dsc    <- hScaleNew adj2
  boxPackStart box5 dsc PackGrow 0
  scaleSetDigits dsc 0

  adj3 <- adjustmentNew 1.0 1.0 101.0 1.0 1.0 0.0

  box6   <- hBoxNew False 0
  containerSetBorderWidth box6 10
  boxPackStart mainbox box6 PackGrow 0
  label4 <- labelNew (Just "Scrollbar Page Size:")
  boxPackStart box6 label4 PackNatural 10
  psc    <- hScaleNew adj3
  boxPackStart box6 psc PackGrow 0
  scaleSetDigits psc 0

  onToggled chb $ do toggleDisplay chb [hsc1,hsc2]
                     toggleDisplay chb [vsc]

  onChanged opt1 $ do setScalePos opt1 hsc1
                      setScalePos opt1 hsc2
                      setScalePos opt1 vsc

  onChanged opt2 $ do setUpdatePol opt2 hsc1
                      setUpdatePol opt2 hsc2
                      setUpdatePol opt2 vsc

  onValueChanged adj2 $ do setDigits hsc1 adj2
                           setDigits hsc2 adj2
                           setDigits vsc  adj2

  onValueChanged adj3 $ do val <- adjustmentGetValue adj3
                           adjustmentSetPageSize adj1 val

  widgetShowAll window
  onDestroy window mainQuit
  mainGUI

Las funciones no estándar usadas en el listado ya han sido listadas antes.