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


7 Usando Glade, con clases (modo derivado)

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

Recuerda que dará error al generar el proyecto.
  1. En primer lugar, en  [Proyecto->Configuración del proyecto] en la pestaña Bibliotecas (de la pestaña Configuración) introducimos:
    PKG_CHECK_MODULES( prueba, [libglademm-2.4 >= 2.6])
    AC_SUBST(prueba_CFLAGS)
    AC_SUBST(prueba_LIBS)
  2. En [Opciones-> opciones del compilador y el enlazador],en la pestaña Opciones, en el "frame" Opciones adicionales añadimos:
    $(prueba_CFLAGS) en Flags del compilador y,
    $(prueba_LIBS) en Flags del enlazador.
  3. [Construir->Auto Generar]
  4. Editamos el archivo configure del proyecto y reemplazamos gtkmm-2.0 por gtkmm-2.4
  5. [Construir->Auto Generar]


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).

REPETIMOS...
En Anjuta, selecciona [Proyecto->Editar GUI de la aplicación].

Seleccionamos windows1, editamos las siguientes propiedades:
Nombre-> miventanica
Título-> maestrodenada.com

Añadimos un VBox, aceptamos el tamaño (3) por defecto.

En la zona superior del VBox, insertamos una etiqueta (label) y modificamos las siguientes propiedades:
Nombre-> etiqueta
etiqueta-> Pulsa el botón de abajo
empaquetado. expandir->Sí

En la zona intermedia del VBox, añadimos una línea horizontal y
empaquetado. expandir->NO

En la zona inferior, ponemos una caja de botones horizontal (indicando 1 columna). Seleccionamos el botón y editamos las siguientes propiedades:
Nombre-> boton
etiqueta-> ¡AQUÍ!

Editamos los iconos del botón y la ventana (en propiedades) y nos queda algo así:



Por supuesto, Grabamos antes de salir.

Una "primera" solución... 

Tal y como hícimos con Gtk(con clase), uno tiene la tentación de simplificar al máximo la función main(), introduciendo casi todas las operaciones en el constructor de la clase.
#include <gtkmm.h>
#include <iostream>
#include <stdlib.h>
#include <libglademm/xml.h>


// DECLARACIÓN DE LA CLASE

class miventanica
{
     public:
         miventanica();
        virtual ~miventanica();
        Gtk::Window * yo;
    protected:
        int veces;
       
         Glib::RefPtr<Gnome::Glade::Xml> refXml;
        
         Gtk::Button* boton;
         Gtk::Label* etiqueta;

        // señales
        virtual void pulsa_contador();
  
};

// DEFINICIÓN DE LOS MÉTODOS DE LA CLASE

miventanica::miventanica()
{
          veces=0;
        refXml = Gnome::Glade::Xml::create("../gladeconclase.glade");
       
        refXml->get_widget("miventanica", yo);
        refXml->get_widget("boton", boton);
       
        boton->signal_clicked().connect(sigc::mem_fun(*this,&miventanica::pulsa_contador));
      
        refXml->get_widget("etiqueta", etiqueta);
   
}

// destructor
miventanica::~miventanica(){std::cout<<"Eso es todo amigo"<<std::endl;}
 
// función para manipular la señal
void miventanica::pulsa_contador(){
  
    char h[30];
    sprintf (h, "has pulsado %i veces el botón", ++veces);

    etiqueta->set_text(h);
}


int main(int argc, char *argv[])

       
    Gtk::Main programa(argc, argv);
  
    miventanica ventana;  
  
    Gtk::Main::run(*ventana.yo);
    return 0;
}
Así, funciona... 

Básicamente, hemos escondido el código de la práctica anterior en una clase. Hemos de destacar que la clase miventanica, en nuestro caso no hereda de Gtk::Window (no es una Gtk::Window), sino que contiene una Gtk::Window ( Gtk::Window yo). No es lo mismo "tener" que "ser" (ser un coche es distinto a tener un coche). Me he visto obligado a hacerlo así porque ninguna de las instrucciones siguientes eran reconocidas:

refXml->get_widget ("miventanica", this);
refXml->get_widget ("miventanica", (Gtk::Window* )this);

[seguiré investigando aunque carezca de interés para mí]


Por lo que, nos vemos obligados a considerarla como un miembro de nuestra clase (una clase que tiene una ventana). Miembro que debe estar en la parte pública para poder acceder desde main (Gtk::Main::run (*ventana.yo))

... Sin embargo, no es muy buena opción, ¿?

En nuestro ejemplo, existe solo una ventana, ¿pero qué ocurre con proyectos que utilizan muchas ventanas (fuentes, colores, grabar archivos, configuración, impresión,...)? Si cada ventana tiene su clase (como es normal) y cada ventana "carga" el documento .glade, tendremos nuestra RAM almacenando innecesariamente el mismo documento varias veces:


La solución pasa por crear en main a refXml y que las clases tengan una referencia al mismo:

Para ello, debemos pasar la dirección de memoria del documento a la clase...

Modifiquemos nuestro archivo glade para que incluya una segunda ventana: un cuadro de diálogo, con un botón [aceptar]. Tal que así:


Editemos el nombre de la ventana y del botón:
ventana: Nombre-> dialogo
botón: Nombre-> botonAceptar

y SOBRETODO no se nos olvide poner dialogo como NO visible (en Propiedades- Comunes-Visible), porque de lo contrario no aparecerá cuando nosotros queramos, sino al principio.

Si prefieres puedes grabar el archivo gladeconclase.glade en el directorio reemplazando al generado por Anjuta.

A continuación os presento "MI" solución, que no se parece a la propuesta en la documentación de libglademm. Que yo sepa presenta como desventaja que las clases no son derivadas de clases Gtk, sino que contienen objetos Gtk, y a la hora de hacer referencia a las propiedades específicas de los objetos Gtk, debo manipularlos. [Probablemente tengan más problemas... con el tiempo irán saliendo]. Aquí está el código.
/* Created by Anjuta version 1.2.4a */
/*    This file will not be overwritten */

#include <gtkmm.h>
#include <iostream>
#include <stdlib.h>
#include <libglademm/xml.h>


// DECLARACIÓN DE LA CLASE
// //////////////////////// primera clase ///////////////////////////////
class miventanica
{
     public:
         miventanica(Glib::RefPtr<Gnome::Glade::Xml>);
        virtual ~miventanica();
        Gtk::Window * yo;
    protected:
        int veces;
       
         Glib::RefPtr<Gnome::Glade::Xml> refXml;
        
         Gtk::Button* boton;
         Gtk::Label* etiqueta;

         virtual void pulsa_contador();
  
};
/////////////////////////////////// segunda clase ////////////////////////////
class miventanica2
{
     public:
        miventanica2(Glib::RefPtr<Gnome::Glade::Xml>);
        virtual ~miventanica2();
        Gtk::Dialog * yo;
    protected:
            
         Glib::RefPtr<Gnome::Glade::Xml> refXml;
         Gtk::Button* botonAceptar;
         virtual void pulsa_botonAceptar();
};

// DEFINICIÓN DE LOS MÉTODOS DE LA CLASE
// constructor de la primera clase
miventanica::miventanica(Glib::RefPtr<Gnome::Glade::Xml> xxx)
{
          veces=0;
        refXml = xxx;
       
        refXml->get_widget("miventanica", yo);
        refXml->get_widget("boton", boton);
       
        boton->signal_clicked().connect(sigc::mem_fun(*this,&miventanica::pulsa_contador));
      
        refXml->get_widget("etiqueta", etiqueta);

}

// destructor
miventanica::~miventanica(){std::cout<<"Eso es todo amigo"<<std::endl;}
 
// función para manipular la señal
void miventanica::pulsa_contador(){
    if (veces==0){
        miventanica2 ventanita(refXml);
        ventanita.yo->run();
    }

    char h[30];
    sprintf (h, "has pulsado %i veces el botón", ++veces);

    etiqueta->set_text(h);
}
/////////////////////////////////////// 2ª clase
miventanica2::miventanica2(Glib::RefPtr<Gnome::Glade::Xml> xxx)
{
          refXml = xxx;
       
        refXml->get_widget("dialogo", yo);
        refXml->get_widget("botonAceptar", botonAceptar);
        botonAceptar->signal_clicked().connect(sigc::mem_fun(*this,&miventanica2::pulsa_botonAceptar));
}

miventanica2::~miventanica2(){}

void miventanica2::pulsa_botonAceptar(){ yo->hide();}

//////////////////////////// MAIN //////////////////////////////

int main(int argc, char *argv[])

       
    Gtk::Main programa(argc, argv);
    Glib::RefPtr<Gnome::Glade::Xml> refXml;
  try
  {
    refXml = Gnome::Glade::Xml::create("../gladeconclase.glade");
  }
  catch(const Gnome::Glade::XmlError& ex)
  {
    std::cerr << ex.what() << std::endl;
    return 1;
  }
  
    miventanica ventana(refXml);  
  
    Gtk::Main::run(*ventana.yo);
    return 0;
}

Explicación:


La función main():


 En este caso, hemos utilizado try ... catch. Estas son dos palabras claves que permiten al código informar de los errores, y manipularlos sin necesidad de abortar el programa.
Cuando verificamos con un if ( xxxx ), donde xxxx es un puntero al que hemos asignado una parte de la memoria dinámica, esta operación solo comprueba si el puntero era NULL, operación fallida, o no lo era (asignación correcta). Pero carecemos de las razones por las que se ha producido el error, lo cual en programas extensos puede ser desalentador.
Con try...catch, throw, la clase exception, podemos generar excepciones y gestionarlas, informando de ellas. La clase exception, de la cual heredará Gnome::Glade::XmlError, tiene una función virtual what() que devuelve una cadena de texto (para informar del tipo de error).

El programa intenta ejecutar el contenido de try, el método Gnome::Glade::Xml::create(), si hubiera algún problema éste lanzará la excepción correspondiente  (con throw -cosa que aquí no se ve-) , la excepción se gestiona en catch, que no hace sino imprimir la cadena del error correspondiente y salir del programa de forma errónea (return 1;).

miventanica ventana(refXml);

Llegado este punto, refXml se habrá creado correctamente. Con esta instrucción, creo el objeto ventana, llamando a la función miventanica::miventanica (Glib::RefPtr<Gnome::Glade::Xml> xxx)
refXml (de main) es pasado como xxx al constructor, allí la instrucción refXml=xxx; asigna a refXml (de la clase) el  valor de xxx, o sea el valor refXml de main.

pulsar_contador():

Dentro de la función pulsar_contador si veces es igual a 0 [la primera vez] se crea una clase que contiene el cuadro de diálogo de la misma forma que con ventana y se lanza. Ventanita que se cierra al pulsar en [Aceptar] o en la cruz del cuadro de diálogo.

 Si la clase fuese derivada de Gtk::Dialog, tan sólo sería necesario ventanita->run() o ventanita->hide(), pero como no lo es, es preciso ventanita.yo->run() o ventanita.yo->hide()

Pero veamos como se pueden utilizar clases derivadas (la propuesta de gtk.org):
Existe un método en Gnome::Glade::Xml:

get_widget_derived("nombre_glade_widget", puntero_clase_derivada);

que permite asignar una widget de un archivo glade a una clase derivada, pero impone dos obligaciones al constructor:
  • recibir un puntero básico de las classes Gtk: BaseObjectType* 
  • recibir una instancia de Gnome::Glade::Xml (como arriba).
Veamos como quedaría nuestro ejemplo:
/* Created by Anjuta version 1.2.4a */
/*    This file will not be overwritten */

#include <gtkmm.h>
#include <iostream>
#include <stdlib.h>
#include <libglademm/xml.h>


// DECLARACIÓN DE LA CLASE
// //////////////////////// primera clase ///////////////////////////////
class miventanica:public Gtk::Window
{
     public:
         miventanica(BaseObjectType* www, const Glib::RefPtr<Gnome::Glade::Xml>& refGlade);

        virtual ~miventanica();
       
    protected:
        int veces;
       
         Glib::RefPtr<Gnome::Glade::Xml> refXml;
        
         Gtk::Button* boton;
         Gtk::Label* etiqueta;

        // señales
        virtual void pulsa_contador();
  
};
/////////////////////////////////// segunda clase ////////////////////////////
class miventanica2:public Gtk::Dialog
{
     public:
         miventanica2(BaseObjectType* www, const Glib::RefPtr<Gnome::Glade::Xml>& refGlade);
        virtual ~miventanica2();
       
    protected:
            
         Glib::RefPtr<Gnome::Glade::Xml> refXml;
         Gtk::Button* botonAceptar;
         virtual void pulsa_botonAceptar();
};

// DEFINICIÓN DE LOS MÉTODOS DE LA CLASE
// constructor de la primera clase
miventanica::miventanica(BaseObjectType* www, const Glib::RefPtr<Gnome::Glade::Xml>& refGlade):Gtk::Window(www),refXml(refGlade)
{
          veces=0;
        refXml->get_widget("boton", boton);
       
        boton->signal_clicked().connect(sigc::mem_fun(*this,&miventanica::pulsa_contador));
      
        refXml->get_widget("etiqueta", etiqueta);

}

// destructor
miventanica::~miventanica(){std::cout<<"Eso es todo amigo"<<std::endl;}
 
// función para manipular la señal
void miventanica::pulsa_contador(){
    if (veces==0){
        miventanica2* ventanita;
        refXml->get_widget_derived("dialogo", ventanita);
        ventanita->run();
    }
    char h[30];
    sprintf (h, "has pulsado %i veces el botón", ++veces);

    etiqueta->set_text(h);
}
/////////////////////////////////////// 2ª clase
miventanica2::miventanica2(BaseObjectType* www, const Glib::RefPtr<Gnome::Glade::Xml>& refGlade):Gtk::Dialog(www),refXml(refGlade)
{
           
        refXml->get_widget("botonAceptar", botonAceptar);
        botonAceptar->signal_clicked().connect(sigc::mem_fun(*this,&miventanica2::pulsa_botonAceptar));
}

miventanica2::~miventanica2(){}

void miventanica2::pulsa_botonAceptar(){ hide();}

//////////////////////////// MAIN //////////////////////////////

int main(int argc, char *argv[])

       
    Gtk::Main programa(argc, argv);
    Glib::RefPtr<Gnome::Glade::Xml> refXml;
  try
  {
    refXml = Gnome::Glade::Xml::create("../gladeconclase.glade");
  }
  catch(const Gnome::Glade::XmlError& ex)
  {
    std::cerr << ex.what() << std::endl;
    return 1;
  }
  
   miventanica* ventana;
 
   refXml->get_widget_derived("miventanica", ventana);  
   if(ventana){
    Gtk::Main::run(*ventana);
   }
    return 0;
}

Puedes observar varias cosas:
  1. Dentro de la clase, podemos utilizar get_widget() normalmente, para los widget contenidos en la ventana.
  2. Observa las diferencias entre la declaración del constructor, donde se específican los argumentos y la definición del constructor donde tras dos puntos: se añaden los constructores que se llamarán con anterioridad al código.
  1. ¿ y el "Eso es todo, amigo" ?. No se ha ejecutado el destructor, esto implica que la memoria ocupada no ha sido liberada. Podemos comprobarlo: abre un terminal y ejecuta  cat /proc/meminfo , observa el valor de MemFree; luego ejecuta nuestro programa y ciérralo, vuelve a comprobar la memoria (es menor), afortunadamente nuestro programa es pequeño.
Para mí, es un error, pues como muchas cosas en la vida (el que la hace, la paga; el que la tira va por ella,...), el encargado de reservar la memoria, debe ser el que la libere. Sin embargo, está formula no lo hace. Debemos liberarla nosotros.

Afortunadamente, es algo sencillo. Justo antes de return 0; debemos añadir delete ventana; y después de ventanita->run y antes de la llave }, delete ventanita;

Y así, O.K.


Glademm, Dejando solito a Glade...
Inicio