Capítulo II: Programa Gráfico en C++.


3 Interactuando... manejo de señales.

Nuestra ventana es más o menos bonita [sobre gustos...], pero no hace absolutamente nada. En este capítulo vamos a aprender como interactuar con el usuario.

Básicamente, existen dos grandes sistemas de control de la acción de un usuario. Una puede asemejarse a cuando queremos hablar con una persona y le llamamos por teléfono, la segunda puede parecerse a cuando esperamos la llamada de esa persona y hasta que no suena el timbre seguímos realizando operaciones.

La programación de consola se asemeja más al primer tipo. Por ejemplo, si iniciamos una sesión en modo consola (Ctrl + Alt + F1, y Ctrl+ Alt+F7 para volver al modo gráfico  )nos pide la identificación del usuario (login) mediante el uso del teclado (el ratón puedes moverlo lo que te de la gana que no hace nada). El programa "llama" al teclado y está "escuchando" al usuario, y mientras todo el sistema parado. En muchos manuales de C++ se dedica algún apartado a cin (std::cin>>variable; [también de "iostream" como std::cout<<), que es el objeto básico de C++ para la entrada de datos.

Sin embargo, la programación gráfica se parece más a la persona que está esperando  una llamada. Si te acuerdas vimos el monitor del sistema para observar que habíamos creado un proceso, junto al nombre del proceso se observa el estado del mismo, el cual en la mayoría de los programas es "Durmiendo", esto es están esperando una llamada del usuario, una señal;  a la llegada de la misma, el programa procesa la misma y a dormir nuevamente, con ello, se evita que un programa monopolice el uso del procesador y puedan realizarse otras gestiones.

Cada componente visual emite una/varias señales diferentes, por ejemplo, los botones (Gtk::Button) tienen entre otras una señal cuando se hace "click" con el ratón sobre ellas.  Lo que hay que hacer es conectar esa señal con el código que queramos que se ejecute, esto se hace tal que así:


boton.signal_clicked().connect(sigc::ptr_fun(&funcion));


Práctica II-3: En Anjuta, crea un proyecto nuevo, selecciona programa proyecto Gtkmm 2.0 (por defecto el lenguaje de programación es  C++), como nombre practica_II_3  (o el que se te antoje).

Recuerda que dará error al generar el proyecto. Edita el archivo configure en la carpeta correspondiente /home/[usuario]/Projects/practica_II_3 y reemplaza las alusiones a gtkmm-2.0 por gtkmm-2.4 (que es la librería que nosotros hemos instalado).

Si al generar el proyecto no te aparece el archivo main.cc, sino otros distintos (nombre_programa.cc, windows1.cc, windows1.hh, windows1_glade.cc,  windows1_glade.hh ) tienes instalado el programa glademm que genera dichos archivos.  
Para seguir las siguientes instrucciones deberías desinstalarlo (provisionalmente).

Modificamos el archivo de main.cc para que quede tal que así:
/* Created by Anjuta version 1.2.4a */
/*    This file will not be overwritten */

#include <iostream>
#include <gtkmm.h>

int veces=0;
void pulsar_boton();

int main(int argc, char *argv[])
{  
   
    Gtk::Main programa(argc, argv);

    Gtk::Window miventanica;
    Gtk::VBox principal;
    Gtk::HSeparator linea;
    Gtk::Label etiqueta("Pulsa abajo");
    Gtk::Button contador("¡AQUÍ!");
   
    contador.signal_clicked().connect(sigc::ptr_fun(&pulsar_boton));
    miventanica.add(principal);
   
   
    principal.pack_start(etiqueta);
    principal.pack_start(linea);
    principal.pack_start(contador);
   
    principal.show();
    etiqueta.show();
     contador.show();   
    linea.show();
   
    miventanica.set_title("maestrodenada.com");
    miventanica.resize(260,110);
   

    Gtk::Main::run(miventanica);
   
    return 0;
}
void pulsar_boton()
{
  std::cout<< "botón pulsado " << ++veces<< " veces"<<std::endl;   
   
}


Explicación:

Al código de prácticas anteriores, le hemos añadido la conexión entre la señal de clickar el botón con la función pulsar_botón definida al final. Función que lo único que hace es incrementar el valor de la variable entera veces y escribir por la consola un mensaje sobre el número de veces pulsado el botón...

Explicar la instrucción:
 contador.signal_clicked().connect(sigc::ptr_fun(&pulsar_boton));
es bastante complicado, casi mejor aprenderlo de memoría, pero intentemoslo...

... C++ permite emular clases genéricas, esto es, clases que manipulan información independientemente del tipo de datos que se usen. Por ejemplo, imagina que desarrollas un algoritmo para ordenar datos; a buen seguro que el procedimiento es igual tanto si se aplica para números enteros, números reales, carácteres,... en C++ puede diseñarse una clase genérica que ordene la información y luego aplicarla a distinto tipo de elementos...

En Gtk se ha creado una clase, un tipo de dato, encargado de organizar y gestionar las señales. Si conoces algo del funcionamiento de Internet, el concepto de proxy puede orientarte. Algunos ordenadores tienen conexión directa con Internet, mientras que otros utilizan un servidor proxy que hace de intermediario, permitiendo la conexión de varios ordenadores aun cuando solo se disponga de una salida.  Esto es lo que se hace en Gtk, utilizar un intermediario que manipula las señales. Este intermediario tiene un método que se denomina connect(), que establece una relación entre una señal y un functor. ¿QUÉEEE? ciertamente, para un mayor control del sistema, esta clase no opera con funciones directamente, sino con funciones convertidas en objetos, también llamados functor. Esa es la misión de sigc::ptr_fun(), convertir la función en functor.  
En rojo, aparece un método de la clase Gtk::Button que devuelve un objeto de Glib::SignalProxy0, nuestro intermediario.

Para nosotros es suficiente, saber que le indica al ordenador que cuando alguien  pulse el botón, realice el código de la función señalada.


Cuidado que vienen curvas... los ámbitos y los punteros.

BLA BLA BLA, y el programa es muy cutre, eso de salir por consola,... Lo suyo es que aunque sea lo mismo, los mensajes vayan saliendo en nuestra etiqueta.

Podíamos probar a que nuestro programa modifique el texto de la etiqueta de la siguiente forma, pero...:
 
/* Created by Anjuta version 1.2.4a */
/*    This file will not be overwritten */

#include <iostream>
#include <gtkmm.h>

int veces=0;//declaración e inicialización de veces
void pulsar_boton();

int main(int argc, char *argv[])
{  
   
    Gtk::Main programa(argc, argv);

    Gtk::Window miventanica;
    Gtk::VBox principal;
    Gtk::HSeparator linea;
    Gtk::Label etiqueta ("Pulsa abajo");//declaración de etiqueta
    Gtk::Button contador("¡AQUÍ!");
   
    contador.signal_clicked().connect(sigc::ptr_fun(&pulsar_boton));
    miventanica.add(principal);
   
   
    principal.pack_start(etiqueta);
    principal.pack_start(linea);
    principal.pack_start(contador);
   
    principal.show();
    etiqueta.show();
     contador.show();   
    linea.show();
   
    miventanica.set_title("maestrodenada.com");
    miventanica.resize(260,110);
   

    Gtk::Main::run(miventanica);
   
    return 0;
}
void pulsar_boton()
{
    char h[30];
    sprintf (h, "has pulsado %i veces el botón", ++veces);

    etiqueta.set_text(h);
       
}



... la compilación del programa nos da error porque etiqueta no es conocido en el ámbito de pulsar_botón.
¿Quéeee?

Al ver la definición básica de las funciones, vimos un ejemplo con dos funciones, main y suma, que tenían una variable con el mismo nombre ['x'], pero que se trataban de dos variables distintas...
Normalmente, las variables declaradas dentro de una función son conocidas dentro de esa función, pero no fuera, son denominadas variables locales. Por ejemplo, etiqueta es declarada en main (variable local de main), pero no en pulsar_boton de ahí el error.

Si declaramos etiqueta dentro de pulsar_boton tal que así...
void pulsar_boton()
{
    Gtk::Label etiqueta ("etiqueta2");
    char h[30];
    sprintf (h, "has pulsado %i veces el botón", ++veces);
    etiqueta.set_text(h);
      
}


... el programa compila perfectamente pero en la ejecución no se observa nada,...
 ELEMENTAL mi querido Watson, no es igual la etiqueta de pulsar_boton que la etiqueta de main. Son dos "etiqueta"s distintas.

Una variable local se crea (se habilita espacio en la memoria) con el inicio de la función y se destruye (se libera la memoria reservada) con el final

Si observas el programa, verás que existe una variable entera veces que está declarada antes de main y fuera de esta, variable que es utilizada por la función pulsar_boton. Las variables declaradas antes y, por tanto, fuera de las funciones son variables globales conocidas por todas las funciones.

¿Podría ser una solución llevar la declaración de etiqueta antes de main?  Probemos...
/* Created by Anjuta version 1.2.4a */
/*    This file will not be overwritten */

#include <iostream>
#include <gtkmm.h>


Gtk::Label etiqueta ("Pulsa abajo");//declaración de etiqueta delante de main
int veces=0;
void pulsar_boton();

int main(int argc, char *argv[])
{  
   
    Gtk::Main programa(argc, argv);

    Gtk::Window miventanica;
    Gtk::VBox principal;
    Gtk::HSeparator linea;
    
    Gtk::Button contador("¡AQUÍ!");
   
    contador.signal_clicked().connect(sigc::ptr_fun(&pulsar_boton));
    miventanica.add(principal);
   
   
    principal.pack_start(etiqueta);
    principal.pack_start(linea);
    principal.pack_start(contador);
   
    principal.show();
    etiqueta.show();
     contador.show();   
    linea.show();
   
    miventanica.set_title("maestrodenada.com");
    miventanica.resize(260,110);
   

    Gtk::Main::run(miventanica);
   
    return 0;
}
void pulsar_boton()
{
    char h[30];
    sprintf (h, "has pulsado %i veces el botón", ++veces);
    etiqueta.set_text(h);
       
}
... compila a la perfección, pero al ejecutarlo ¡MY GOD! ¡¡¡qué hemos hecho!!!!

La siguiente explicación no es muy ortodoxa,[ supongo ], pero espero permita dar una idea al lector ...



... Un ordenador tiene una memoria RAM que es la utilizada por los procesos cuando estos se ejecutan. Esto es, del disco se cargan en la memoria RAM y se procesan por el procesador, valga la redundancia.
La memoria RAM en Unix tiene dos  grandes divisiones la pila y el heap. En la pila se organiza la información como en una pila [de ahí el nombre] de libros o documentos, uno encima de otro. En el montón (heap), la información se  organiza más como una estantería.

Al compilar el archivo, el compilador asigna la memoria de la PILA (stack)  acorde a las variables globales definidas y el código (todo son 0 y 1 a fin de cuentas). La variable veces es entera, esto es, tiene un tamaño fijo (4 bytes), tamaño que permanece invariable durante todo el programa sea cual sea la asignación que se le realice.

Los objetos de Gtk, y esto es mi suposición, tienen un tamaño variable [Sería impensable que tuvieran un tamaño estándar, no es posible que una ventana como la nuestra solicitará los mismos recursos que la de el programa Gimp -programa original de las librerías Gtk]. Así, que al igual que las cadenas de texto, los objetos de Gtk solicitarán memoria en función de la complejidad del mismo. Cuando nosotros compilamos el programa, este asigna la memoria específicada en la declaración del objeto, que será la mínima. A la hora de ejecutar el programa, éste tratará de inicializar nuestro objeto, lo cual implicará el aumento de su tamaño. Pero el aumento de tamaño no puede realizarse en este punto de la RAM, dando un error en la gestión del proceso (error 11, segmentation fault ). En la pila, no puede aumentarse el tamaño de nuestro objeto, porque habría que reorganizar todos los datos de la pila; imaginate una serie de carpetas apiladas, de repente, encuentras documentos que pertenecen a la segunda carpeta, para introducir esos documentos debes levantar todas las carpetas superiores.

Para solucionar el problema podemos decir que existen en C++ (y en C) unas variables muy poderosas (y peligrosas) que permiten la manipulación de la memoria denominados punteros.  A su vez en todo proceso de Unix existe otro espacio en la memoria Ram, denominado montón (HEAP), que es el utilizado para la gestión dinámica de la memoria, esto es, la utilización durante la ejecución del programa; de tal suerte, que no es igual la memoria que pueda gestionar un editor de texto cuando escribimos un pequeño poema a cuando escribimos el Quijote.

Un puntero es una variable que almacena una dirección de memoria, esta dirección de memoria puede "apuntar" al comienzo de un objeto almacenado en otro lugar de la memoria. Si durante la ejecución del programa nuestro objeto aumenta o varia de tamaño, nuestra variable puntero seguirá siendo igual y "apuntando" al mismo sitio.



Un puntero se declara así:  nombre_tipo * nombre_variable;

nombre_variable almacena una dirección de memoria.

En principio, un puntero es una dirección de memoria, todos los punteros son direcciones de memoria. Pero los desarrolladores de C++ fueron buenos chicos y nos permitieron declarar el tipo de objeto al que apuntaba esa dirección de memoria para que a la hora de manipularlos advertirnos de posibles errores en tiempo de compilación (durante la programación), y es que los errores con punteros pueden ser difíciles de localizar.

* nombre_variable devuelve el contenido (0 y 1) que comienza en la dirección contenida nombre_variable, considerándola del tipo nombre_tipo. Esto es, si declaro int * x;  la variable x almacena una dirección de memoria, si en el programa escribo *x, el ordenador leerá a partir de la dirección de memoria contenida en x, 4 bytes (lo que ocupa un int) y devolverá el valor - los 0 y 1- como si se tratará de un entero.

& nombre_variable devuelve la dirección de memoria de la variable nombre_variable. Para asignar a una variable puntero una dirección de un objeto, debemos utilizar el operador &.

Veamos un ejemplo:

#include <iostream>
int main()
{
    int * v; // variable puntero a entero
    int p=7;// variable entera
    v=&p; // v apunta a p
    std::cout<<" v = "<< v <<std::endl;
    std::cout<<" *v = "<< *v <<std::endl;
    std::cout<<" p = "<< p <<std::endl;
    std::cout<<" &p = "<< &p <<std::endl;
   // observa que la variable v almacena una dirección,
   //pero ella  como cualquier otra está alojada en otra dirección.
   //  v es distinto de &v
    std::cout<<"&v = "<<&v<<std::endl;  
    return 0;
}

Comprendiendo esto, ya podemos pasar a los dos estilos básicos de programación con Gtk...
Interactuando...programación básica de Gtk
Inicio