Programación Linux en el Guille


Programación en Linux con KDevelop 1.0

Autor: Guillermo 'guille' Som
Inicio: Domingo, 17 de Octubre de 1999
Actualizado: 19/Oct/99


Crear diálogos personalizados (formularios) y gestionar eventos.

Confío que hayas leido la introducción al entorno de KDevelop y cómo añadir un control a una ventana y controlar la pulsación (click) sobre dicho botón; lo que ahora vamos a ver es cómo crear una ventana personalizada, en la que añadiremos varios controles y gestionaremos los eventos que se produzcan en los diferentes controles insertados en dicha ventana.

Para este ejemplo, crearemos un cuadro de diálogo en el que tendremos un listbox al cual añadiremos nombres y después guardaremos el contenido de esa lista en un fichero.

Pero hagamos las cosas paso a paso, para que se entienda.

Crear el proyecto y añadir un nuevo diálogo.

Empecemos creando un nuevo proyecto, ya vimos anteriormente cómo hacerlo, así que aquí sólo te dire lo estrictamente necesario, por ejemplo: el nombre del proyecto será KPrueba02, una vez creado tendremos tres ficheros: kprueba02.h, kprueba02.cpp y main.cpp. Realmente sólo necesitamos uno de ellos... por tanto, vamos a dejar el fichero con la función main(), es decir dejaremos solamente main.cpp y eliminaremos los otros dos. Para borrar esos dos ficheros, selecciona la solapa LFV, haz que se muestre el contenido de la carpeta "headers", selecciona kprueba02.h y pulsa el botón derecho del ratón, del menú que te muestra, selecciona la opción "Remove File from Disk...", es decir quitar del proyecto y borrar del disco... Te mostrará un aviso de que se va a borrar... aceptalo. Haz lo mismo con kprueba02.cpp; ahora sólo tendremos el fichero main.cpp. (Si te pregunta que si quieres volver a cargar el fichero desde el disco, dile que SI, aunque no habrá nada que cargar... pero al menos así borra el buffer de memoria)

Lo siguiente que vamos a hacer es crear un diálogo, en el cual podremos "dibujar" los diferentes controles que usaremos en la aplicación, para ello usaremos el "asistente" para diálogos, que no es ninguna virguería, pero algo es algo... peor es tener que posicionar manualmente, (mediante coordenadas), los controles...
El resultado final será como este que te muestro:




Pero vamos a ver cómo conseguirlo, ya que de eso es de lo que se trata, ¿verdad?

Crear un nuevo diálogo.

Veamos cómo hacerlo:
En el menú File, selecciona New... te mostrará el siguiente diálogo:

Selecciona Qt/KDE Dialog
En el nombre escribe KPrueba02, (el asistente añadirá automáticamente la extensión .kdevdlg)
Pulsa en OK
Te mostrará una ventana con un aviso, (siempre la muestra, salvo que quites la selección del checkbox que dice "Show again"), para cerrar esta ventana de aviso, pulsa en el botón "OKAY"
Te mostrará un "formulario" vacio, preparado para aceptar algunos de los widgets que el entorno pone a nuestra disposición... no son todos los posibles, pero...
En el panel de la izquierda hay varias solapas, pulsa en la primera "Widgets" y te mostrará los controles (widgets) que podemos colocar en la ventana:


Pulsa en la "A" y añadirá una etiqueta.
Pulsa sobre ella para seleccionarla
En el panel de la derecha verás que están las propiedades del objeto seleccionado.
En la carpeta "General" selecciona Text y escribe "Nombre:" (la etiqueta ahora mostrará lo que has escrito)
Añade el resto de los controles para que la apariencia sea como la mostrada más arriba.
La caja de texto es la que tiene tres asteriscos (*) y el nombre que te muestra el tooltip es "QLineEdit"
En el panel de propiedades asigna estos valores a estas propiedades:
En la carpeta C++ Code/VarName: txtName
En la carpeta General/hasFrame selecciona true
(no asignes el valor de hasFocus, ya que el código que genera es erróneo)
El tipo, Text y VarName del resto de los objetos es:
QPushButton, Aña&dir, cmdAdd
QPushButton, &Salir, cmdExit
QPushButton, &Guardar..., cmdSave
QPushButton, &Abrir..., cmdOpen
QListBox, "sin cambios", lstNames
QLabel, "sin cambios", lblStatus
QCheckBox, Múltiple selección, chkMulti
A la otra etiqueta, (la que está encima del listbox), dejale el nombre predeterminado y en el text escribe: "Nombres añadidos:"

Si ves que los puntos del grid son demasiado grandes para poder ajustar los controles a tu gusto... puedes cambiar el tamaño de separación: En el menú View/Grid... selecciona la separación de los puntos, (con 5 se pueden situar bien)

Bien, ya tenemos los controles (widgets) situados en el formulario, ahora hay que "generar" el código para que podamos usarlo.
En el menú Build, selecciona Generate Sources..., (también puedes pulsar en el icono de la barra de herramientas, el que está debajo del menú Options)
Te mostrará un cuadro de diálogo, en ClassName escribe KPrueba02, (el resto de las casillas se rellena solas, dejale los valores)
Pulsa en OK.
Seguramente te mostrará un mensaje indicándote que se han modificado los ficheros... acepta los cambios, (pulsa en Yes)


Y si tienes la misma suerte que yo... seguramente el programa habrá "petado"...
Si es así, abre de nuevo el KDevelop y abre el proyecto que estabamos creando.
Los proyectos se guardan en la carpeta de tu directorio "Home", normalmente es /home/nombre_usuario, ( o /root), y después el nombre del proyecto, también puedes indicarle que lo guarde en otro sitio...



Si el programa ha seguido funcionando, haz que se muestre el código, en el menú View selecciona la primera opción.
Ahora verás que volvemos a tener los dos ficheros que borramos antes, (lo mismo es por esto por lo que falla...), además de un nuevo fichero: kprueba02data.cpp, este fichero se genera automáticamente y no deberías hacer cambios, ya que se borrarán cuando entres de nuevo en el diseñador de diálogos.

Si quieres hacer cambios en el formulario, simplemente pulsa en el fichero que está en la carpeta Dialogs o bien pulsa en la barra de herramientas en el icono correspondiente:

Si ya has terminado de diseñar el formulario, vamos a añadir el código necesario para hacer lo que queremos hacer:
Escribir un nombre en la caja de texto, añadirlo al listbox y cuando hayamos terminado guardar los datos.
Esos datos se podrán recuperar para poder seguir añadiendo más nombres.

A diferencia del Visual Basic, los eventos que queramos controlar, los tenemos que especificar de forma "manual", (mediante código), por tanto necesitamos saber que eventos (o señales) queremos detectar y a que funciones (o métodos) queremos conectarlos.

En principio, necesitamos detectar la pulsación en los cuatro botones, en el checkbox y cuando seleccionamos el listbox, (para que se muestre el nombre en la caja de texto, realmente será un dobleclick o pulsando intro cuando el listbox tenga el foco, ya que no he encontrado la forma de hacer que con una simple pulsación se sepa que elemento está seleccionado).

Para estas tareas vamos a crear las siguientes funciones:
cmdSalir_click
cmdAdd_click,
cmdSave_click
cmdOpen_click
chkMulti_click,
lstNames_click.

Estas funciones hay que indicarlas de manera especial en el fichero de cabecera, bajo el apartado "slots", como no necesitamos que ninguna otra clase sepa de estos eventos, podemos declararlos como privados, si quieres que las clases heredadas de ésta puedan saber de esos eventos, declaralos como public o protected.

Veamos el código del fichero kprueba02.h:

#ifndef KPRUEBA02_H
#define KPRUEBA02_H

//Generated area. DO NOT EDIT!!!(begin)
#include <qwidget.h>
#include <qlabel.h>
#include <qlineedit.h>
#include <qpushbutton.h>
//Generated area. DO NOT EDIT!!!(end)

#include <qdialog.h>

/**
  *@author Guillermo 'guille' Som
  */

class KPrueba02 : public QDialog  {
   Q_OBJECT
public: 
        KPrueba02(QWidget *parent=0, const char *name=0);
        ~KPrueba02();

protected: 
        void initDialog();
        //Generated area. DO NOT EDIT!!!(begin)
        QLabel *QLabel_1;
        QLineEdit *txtName;
        QPushButton *cmdAdd;
        //Generated area. DO NOT EDIT!!!(end)

private slots:
        // funciones que se usarán para los eventos
        void cmdExit_click();
        void cmdAdd_click();
        void cmdSave_click();
        void cmdOpen_click();
        void lstNames_click( const char *);
        void chkMulti_click();

private:
        // flag para saber si se ha modificado el contenido de la lista
        bool modificado;
};

#endif


Parte de este código ha sido generado por el asistente de los diálogos, está indicado en el código. Lo que tedrás que añadir son las declaraciones que hay en "private slots" y la declaración de la variable modificado, (que usaremos para saber si se ha guardado o no el contenido del listbox)

Ahora nos queda escribir el código que hará que funcione nuestro programa.
Selecciona el fichero kprueba02.cpp y escribe el siguiente código, (aquí si que tendrás que escribir más, ya que el asistente sólo crea la declaración de las funciones del constructor y destructor), creo que con los comentarios sabrás que es lo que hace cada función... de todas formas, después te comentaré las partes que crea conveniente resaltar.

/***************************************************************************
                          kprueba02.cpp  -  description
                             -------------------
    begin                : Mon Oct 18 1999
    copyright            : (C) 1999 by Guillermo 'guille' Som
    email                : mensaje@elguille.info
        ---------------------------------------------------------------------------
        En este ejemplo, se tratan los siguientes temas:
        --interceptar eventos al pulsar en botones
        --añadir elementos a un listbox
        --uso del cuadro de diálogo abrir y guardar
        --preguntar mediante MsgBox (MessageBox)
        --guardar el contenido de un listbos en un fichero de disco
        --leer un fichero de disco y asignar el contenido al listbox
        --seleccionar un elemento de la lista
        --interceptar la doble-pulsación en un listbox
        --interceptar la pulsación en un checkbox
        --cambiar el modo multiselect del listbox

        --El formulario se ha diseñado con el "diseñador de diálogos"

        Por defecto, el orden de tabulación está asignado según se van añadiendo
        los controles al formulario, para cambiar ese orden de tabulación:
        ve seleccionando los controles de forma descendente, (desde el último al primero),
        pulsa el botón derecho del ratón y selecciona LowerToBotton.
        (seguramente habrá otra forma, pero...)

        También puedes cambiar el orden de creación de los controles en el fichero
        PROJECTdata.cpp, aunque este fichero se "regenera" cada vez que modificas el formulario.
        ---------------------------------------------------------------------------

 ***************************************************************************/
#include "kprueba02.h"
#include <qmessagebox.h>
#include <qfiledialog.h>
#include <qtextstream.h>


KPrueba02::KPrueba02(QWidget *parent, const char *name) : QDialog(parent,name,true){
        initDialog();

        // Asignar valores iniciales...
        modificado=false;

        // Asignamos el "Buddy", es decir el control que recibirá el foco
        // si se pulsa la letra subrayada (aceleradora)
        // Si no se asigna un "buddy", se mostrará el signo &
        // por tanto, sería recomendable asignar el nombre en el código,
        // ya que así nos aseguramos de que tendrá un control asociado a la tecla resaltada
        QLabel_1->setText("&Nombre:");
        QLabel_1->setBuddy(txtName);

        // Borramos el contenido del listbox, aunque inicialmente está vacio
        lstNames->clear();

        // Borramos el contenido de la etiqueta de información
        lblStatus->clear();

        // Damos el foco inicial al textbox
        txtName->setFocus();


        // Conectamos el click del botón Salir con la acción de salir del programa
        connect( cmdExit, SIGNAL(clicked()), SLOT(cmdExit_click()) );
        // idem del botón Añadir
        connect( cmdAdd, SIGNAL(clicked()), SLOT(cmdAdd_click()) );
        // idem para el botón de Guardar
        connect( cmdSave, SIGNAL(clicked()), SLOT(cmdSave_click()) );
        // idem para el botón de Abrir
        connect( cmdOpen, SIGNAL(clicked()), SLOT(cmdOpen_click()) );
        // Conectamos el evento de selección de un elemento del listbox
        connect( lstNames, SIGNAL(selected( const char *)), SLOT(lstNames_click( const char *)) );
        // Conectamos el evento click del checkbox
        connect( chkMulti, SIGNAL(clicked()), SLOT(chkMulti_click()) );
}

// El destructor de la clase
KPrueba02::~KPrueba02(){
}

void KPrueba02::cmdExit_click(){
        QString s;
        // Informar de que no se ha guardado el contenido de la lista
        if( modificado )
                s="La lista se ha modificado y no está guardada...\n\n";
        else
                s="";
        
        if( QMessageBox::warning(this, "Salir de Prueba de Diálogos",
                                 s+"¿Seguro que quieres terminar?"," Si "," No ","",1,0)==0 )
                exit(0);
}

void KPrueba02::cmdAdd_click(){
        // Cuando se pulsa en el botón Añadir...
        // se añade el contenido del textbox a la lista

        // Sólo añadirlo si no está vacio
        QString s = txtName->text();

        if( !s.isEmpty() ){
                // añadimos el nombre a la lista
                lstNames->insertItem( txtName->text() );
                // Convertimos el número en una cadena
                s.setNum( lstNames->count() );
                // mostramos el número de elementos en la lista
                lblStatus->setText( "Nombres en la lista: " + s );
                // seleccionamos todo el contenido del textbox
                txtName->selectAll();
                modificado = true;
        }else
                lblStatus->setText("No se admiten cadenas vacias");
}

void KPrueba02::cmdSave_click(){
        // Cuando se pulsa en Guardar

        // Sólo si hay elementos en la lista
        if( lstNames->count() ){
                // Preguntar el nombre del fichero
                QString sFile = QFileDialog::getSaveFileName("./","*.txt",this);
                // Si se ha seleccionado un nombre
                // (cuando se pulsa en cancelar se devuelve una cadena vacia)
                if( !sFile.isEmpty() ){
                        // Asignar el fichero seleccionado
                        QFile f(sFile);
                        // Abrirlo para escribir en él
                        f.open(IO_WriteOnly);
                        // Guardar cada elemento de la lista
                        uint i;
                        for( i=0; i<lstNames->count(); i++ ){
                                // Añadir un retorno de carro y cambio de línea... al estilo MS-DOS
                                QString c = lstNames->text(i) + QString("\n\r");
                                // writeBlock espera una cadena y el número de caracteres
                                f.writeBlock( c, c.length() );
                                lblStatus->setText("guardando... "+c);
                        }
                        // cerrar el fichero
                        f.close();
                        modificado = false;
                        lblStatus->setText("Datos guardados en: " + sFile);
                }else
                        lblStatus->setText("No has seleccionado un nombre");

        }else
                lblStatus->setText("No hay elementos que guardar!");
}

void KPrueba02::cmdOpen_click(){
        // Cuando se pulsa en Abrir

        // Si se ha modificado la lista, preguntar si se guarda...
        if( modificado ){
                if( QMessageBox::warning(this,"Abrir - Prueba de Diálogos",
                                        "¡ATENCION! La lista se ha modificado y no está guardada\n\n¿Quieres guardarla?",
                                        " Si ", " No ",0,0)==0 )
                        cmdSave_click();
        }
        // Borrar el contenido de la lista
        lstNames->clear();

        // Preguntar el nombre del fichero a abrir
        QString sFile = QFileDialog::getOpenFileName("./","*.txt",this);
        // Si se ha seleccionado un nombre
        // (cuando se pulsa en cancelar se devuelve una cadena vacia)
        if( !sFile.isEmpty() ){
                // Asignar el fichero seleccionado
                QFile f(sFile);
                // Abrirlo para lectura
                f.open(IO_ReadOnly);
                
                // Leer del fichero y asignarlo al listbox
                QString s;
                // Asignamos el fichero a un TextStream para poder "navegar" por el contenido
                QTextStream t(&f);
                
                // Mientras no lleguemos al final del fichero...
                while( !t.eof() ) {
                        // Leemos la siguiente línea
                        s = t.readLine();
                        // Quitamos los espacios del principio y final
                        s = s.stripWhiteSpace();
                        // Si no es una cadena vacia...
                        if( !s.isEmpty() )
                                // Lo añadimos a la lista
                                lstNames->insertItem(s);
                }
                // cerrar el fichero
                f.close();
                modificado = false;
                // Convertimos el número de elementos de la lista en una cadena de caracteres
                s.setNum( lstNames->count() );
                lblStatus->setText("Nombres en la lista: " + s );
        }else
                lblStatus->setText("No has seleccionado un nombre");
}

void KPrueba02::lstNames_click(const char *e){
        // Cuando se hace dobleclick (o se pulsa intro) en el listbox
        // La "señal" selected del ListBox tiene dos formas,
        // una devolviendo el contenido del elemento seleccionado,
        // (que es la que estamos usando aquí),
        // y devolviendo el número de índice del elemento seleccionado

        // Asignar el elemento pulsado a la caja de texto
        txtName->setText(e);    
}

void KPrueba02::chkMulti_click(){
        // Cuando se pulsa en el checkbox
        // cambiamos el modo de multiselección
        lstNames->setMultiSelection(chkMulti->isChecked());
}

Explicando lo visto.

Creo que prácticamente esta casi todo explicado en los comentarios, al menos lo que yo puedo explicarte... además que una vez vistas las instrucciones a usar, el resto parece fácil... si al problema de enfrentarte a un lenguaje de programación que no es el tuyo (C++), le añades el tener que usar funciones y controles que no se "comportan" como estás acostumbrado... pues al final para hacer un programilla simple, te complicas más de la cuenta...
No se el nivel de conocimientos de C/C++ que tienes, ni si alguna vez has programado para el entorno XWindow de Linux, pero para mi es una novedad, tanto lo uno como lo otro... no es que el lenguaje me sea del todo desconocido, pero la verdad es que hasta ahora no lo había puesto en práctica... salvo alguna que otra pruebecilla con el C++Builder y el Visual C++

Así que, te voy a explicar lo que considero más "distinto" de lo que hasta ahora estaba acostumbrado: la interceptación de "eventos" (o señales) de los diferentes controles (widgets).

Aunque esto ya lo he comentado anteriormente, lo repito para que se te quede "grabado", ya que considero que es importante saber desenvolverse con los eventos, ya que en los entornos gráficos es esto lo más importante, ya que no nos obliga a programar de forma lineal, sino que, según lo que haga el usuario, así tendrá que responder nuestra aplicación.

La "madre del cordero" es la declaración de las funciones que interceptarán esos eventos en una "sección" especial del fichero de cabecera: bajo la sección slots
Después está la forma de indicarle al compilador cómo se conectan esas funciones con los eventos indicados, mediante connect.

Es decir, tomamos el objeto que nos interesa "controlar", averiguamos el evento o señal que queremos interceptar y le decimos que función debe usar... de esta "tarea" talvez la más complicada sea la de averiguar el evento a interceptar... si ya sé que se supone que en la documentación está... pero, si sabes lo que yo de C++, pues lo mismo se te escapan algunas cosas... por ejemplo aún no se si se puede detectar la pulsación de una tecla en un listbox, según lo que he leido en la documentación sería mediante el "evento" keyPressEvent, pero resulta que ese "evento" no existe en la clase QListBox, al menos no se puede "conectar"... cuando lo averigüe te lo cuento...

Vamos a ver un ejemplo de esto de conectar los eventos con funciones declaradas por nosotros, y para no hablar del código que ya está mostrado anteriormente, vamos a ver cómo detectar cada pulsación de tecla en una caja de texto:

// Evento Change para txtNombre
connect( txtNombre, SIGNAL(textChanged(const char *)), SLOT(txtNombre_change(const char *)) );

Es decir, le indicamos el objeto (control o widget, como prefieras llamarlo) del que queremos interceptar un mensaje, en este caso un objeto llamado txtNombre.
Después le indicamos mediante la macro SIGNAL() el evento (o señal) que nos interesa, en este caso textChanged, el cual tiene un parámetro que es del tipo char * (una cadena de caracteres)
Por último le indicamos que función de nuestro objeto debe llamar, en este ejemplo es: txtNombre_change, esta función recibirá los mismos parámetros que la "señal" que queremos interceptar, por tanto también hay que declarar un parámetro del tipo char *

Aquí está la declaración de nuestra función:

void NombreClase::txtNombre_change( const char *t ){
// Mostrar el texto conforme se va escribiendo
lblInfo->setText( t );
}

Por supuesto, esta función está declarada en el fichero de cabecera como "xxx slots:", donde xxx puede ser public, protected o private, según la visibilidad que queramos que tenga, te vuelvo a recordar que private sólo es visible dentro de la clase, protected es visible en las clases derivadas de esta clase, (es casi como las funciones declaradas con Friend en mi querido Visual Basic) y por último las declaradas como public que serán vistas en cualquier parte que usen nuestra clase. Esto no sólo es válido para los slots, sino también para cualquier función o variable declarada en la clase.

Me estoy planteando hacer unas "entregas" sobre algunas características del lenguaje y de cómo funcionan algunas clases, ya que, por ejemplo, saber cómo funciona la clase del tipo QString podemos saber que algunas funciones están incorporadas en la propia clase, por ejemplo para saber si la cadena está vacia se usa el método isEmpty, lo mismo hacemos para quitar los "blancos" de una cadena, ya que se usa stripWhiteSpace o para contar el número de caracteres que esa cadena contiene: length... como puedes comprobar, no sólo hay que saber cómo funciona el lenguaje en el que trabajas, para, por ejemplo hacer un bucle, sino también saber cómo funcionan las diferentes clases... o mejor dicho, que funciones tienen para que podamos usarlas...

No me enrollo más, que al final me estoy alargando y no te digo nada que no sepas ya.
Para terminar un par de "ejercicios".
Como te comenté antes, no he encontrado la forma de detectar cuando se pulsa una tecla, (que no sea intro), en un listbox, mi intención era que al pulsar la tecla suprimir se pudieran borrar elementos del listbox... así que, se me ha ocurrido que sería interesante que nuestra aplicación tuviese un botón que nos permita borrar los elementos seleccionados de la lista.
Otra mejora podría ser sustituir el elemento seleccionado por el contenido de la caja de texto.
Y para terminar con los ejercicios (o proposiciones no indecentes), sería que se nos permitiera usar por defecto un nombre de fichero, de esa forma, cada vez que esa opción esté marcada y pulsemos en abrir o guardar, se use ese fichero en lugar de tener que preguntarnos el nombre...

La solución a estos "ejercicios" los puedes ver si pulsas este link... si el link no existe es que aún no están publicados, pero seguro que los publico, ya que una cosa que no me gusta es que te digan que hagas esto o aquello y al final no puedas comprobar si lo has hecho bien o no... aunque si lo haces y funciona se supone que están bien hechos... en fin... algunas veces me debería callar.

Nos vemos.
Guillermo

Nerja, 19 de Octubre de 1999
(el "Moon Phase Indicator for KDE", el indicador de de las fases de la luna que tengo en mi barra de tareas, indica que la luna tiene 10 días de vida... desde la última luna nueva, se entiende)


Volver al índice de KDevelop


Volver a la sección de Linux

índice de el Guille