He aquí un compendio de preguntas y problemas varios que he visto una y otra vez en proyectos finales. Espero que les sirva. Si algo no se entiende o hay algún tópico que agregar/completar, pueden escribir a zaskar_84<arroba>yahoo.com.ar.
Esto se publica bajo licencia Creative Commons (CC BY-SA). Por Pablo Novara, con colaboraciones de Pablo Abratte. Última actualización: 28/02/2017.
Respuestas:
¿Tengo que dividir mi código en muchos archivos cpp y h? ¿Puedo escribir todo junto en el mismo archivo?
Separar en cpp y h tiene varios objetivos:
Prolijidad y ordenamiento: los archivos largos se hacen cada vez mas largos con el mantenimiento y terminan siendo engorroso. Es conveniente separarlos por clases y/o por funcionalidad. Cuando se trabaja con orientación a objetos lo más aconsejable es colocar cada clase en un archivo (en realidad dos, cpp y h) diferente. Cuando se trata de funciones, se suelen agrupar por tópicos (por ejemplo, las que se utilizan para manejo de archivos en un archivo, las de tratamiento de cadenas en otro, etc). Si se trabaja mayormente por clases y se tiene una pocas funciones auxiliares se pueden agrupar en un archivo utiles.h/.cpp o similar.
Compilación más rápida: Si esta todo en los hs y se compila un solo cpp hay que compilar toooodo ante cualquier cambio. Si está bien separado, cuando se cambia algo, solo se compila el cpp que cambia, o si cambia un .h los cpps que lo incluyan, pero no todos. Luego se reenlazan los objetos nuevos con los otros objetos que no cambiaron y así es muuuucho mas rapido compilar y probar cambios cuando crece el proyecto
Para vender una biblioteca sin que sea opensource, o evitar que la modifiquen: cuando se entrega a alguien una biblioteca, el .h se tiene que dar sí o sí al cliente, sino no la puede usar, pero el cpp no hace falta, ya que se le da el objeto ya compilado y listo. Si el .h tuviera todo se le está dando el fuente completo, mientras que de la otra forma se le dá una especie de índice de qué puede hacer la biblioteca (.h) pero se oculta el cómo (.cpp).
Si dos cpps incluyen un h y este tiene alguna definicion (algo que no sea prototipo, como la implementacion de un metodo/funcion, o una variable global/estatica), entoces dos objetos van a tener lo mismo y el linker del 99% de los compiladores no va a saber que hacer y va a dar error. Solo en el caso de algunos productos de Borland y para variables globales esto enlaza, pero entonces tenemos variables globales que no son realmente globales, sino que cada cpp tiene su copia, y aunque se llama igual y proviene del mismo .h son diferentes, y eso no tiene mucho sentido (para hacerlo esto bien se usa la palabra static fuera de las clases si la ven por ahi, nunca lo usamos en poo). (ver multiple definition of....)
En algunos casos es inevitable: si una clase A tiene un métod A::a1(B *x) que recibe una instancia de la clase B, y esta clase B tiene un método B::b1(A *x) que recibe una instancia de A hay problemas. Si definimos primero A, al declarar el método que recibe B el compilador se queja de que no conoce B, y al revés lo mismo. La salida es hacer una declaración adelantada de una clase (por ejemplo "class B;") con lo cual podemos ahora declarar punteros a B (por eso los métodos que nombré antes reciben punteros), pero no podemos usarlos porque no definimos qué métodos tiene esa clase B, solo que existe. Entonces, lo que se hacen son definir las clases con solo los prototipos de los métodos, cada una en su h utilizando declaraciones adelantadas de la otra, y luego hacer los includes completos en los .cpp. Aquí estamos obligados a que una de las dos clases tenga un cpp.
La respuesta rápida es que en los .h debería ir todo lo que no ocupa lugar en el ejecutable, sino que solo está para darle indicaciones al compilador sobre cómo son las cosas. Por ejemplo, el prototipo de una función es un aviso al compilador para que no se queje cuando la invoquemos y sepa cómo se usa, pero lo que realmente se compila es su implementación, por esto el prototipo va en el .h y su implementación en el .cpp. De igual forma, la declaración de una clase con sus atributos y prototipos de métodos solo sirve para indicarle al compilador que forma tendrá cuando la necesite, va en el .h; pero declarar un objeto de esa clase o implementar una función sí ocupan lugar, así que van en los cpps. Además, si declaramos algo que realmente se compile en un .h, y después hacemos un #include de ese .h desde dos .cpps diferentes del mismo proyecto tenemos compilado dos veces lo mismo, por ejemplo, la misma función, así que a la hora de enlazar no sabrá cual de las dos elegir y generará un error similar a "multiple definition of 'foo'...".
La excepción a esta regla aparece con los templates. No se debe colocar la implementación de los templates en su propio .cpp y querer compilarlo, porque hasta que no se invoque (cosa que se hará desde otro .cpp y en la compilación del primero no interviene) no se sabe qué especializaciones se necesitan. Por esto, al compilar el .cpp del template probablemente no compile en realidad ninguna implementación. Los templates entonces se suelen colocar enteros en el .h para que cada .cpp que lo incluye genere en su compilación la especialización que necesita.
El .h suele estar rodeado por directivas de preprocesador como "#ifndef ALGO", "#define ALGO", "#endif". Esto se hace para evitar que el archivo se incluya dos veces. Por ejemplo, si se tiene una clase ListaClientes y otra ListaProductos que manejan los clientes y productos de un software de gestión a través de listas STL, es probable que ambas clases incluyan a la cabecera <list> para declarar objetos list entre sus atributos y argumentos. Entonces, si un programa cliente usa las dos clases, hace los dos #includes e incluye indirectamente dos veces la cabecera que contiene la declaración de la clase string. Si el compilador la procesara dos veces generaría un error. Si la cabecera tiene la estructura de directivas de preprocesador mencionada, la primera vez el "#ifndef _LIST_H_" resulta verdadero (pregunta si no está definida la constante de preprocesador _LIST_H_), y entonces define la constante y procesa la clase, mientras que la segunda el #ifndef resulta falso porque la constante ya fué definida en la primer pasada, y entonces saltea el contenido del archivo evitando el error. El nombre de la constante se define con las mismas reglas que un nombre de clase o variable, pero se suelen colocar en mayúsculas por convención.
En el .cpp suelen estar los #includes necesarios, incluyendo el include del .h homónimo. Siempre es preferible colocar los #include en el .cpp para que la compilación sea más rápida. Por ejemplo, si una clase maneja archivos en sus métodos, pero no tiene ningún atributo ni argumento de tipo fstream, el include de dicha clase puede colocarse en el .cpp. Pero si una clase tiene un atributo de tipo vector, el #include debe colocarse sí o sí en el .h para poder definir el atributo. La excepción ocurre cuando el atributo o argumento es un puntero, en cuyo caso basta con una forward declaration de la clase (ejemplo: "class Persona;") para poder declarar el puntero. Por último, siempre conviene colocar primero los includes de bibliotecas externas al proyecto (entre < y >) y luego los correspondientes a archivos del proyecto (entre comillas).
¿Cómo elegir qué tipo de archivos (binarios o de texto) y de qué forma utilizarlos en mi proyecto?
Depende de los datos que maneje el programa. Hay que considerar dos aspectos: el tipo de archivo, y la forma de lectura y escritura del mismo.
Las bases de datos en las que se trabaja frecuentemente usualmente se guardan en archivos binarios, ya que es más rápido y fácil encontrar y modificar registros. Los archivos de texto se usan generalmente para guardar configuraciones o generar informes. Sin embargo, en un binario los campos deben tener longitud fija. Si se requiere longitud variable se debe utilizar archivos de texto, o más de un archivo binario, o una combinación de ambos. Por ejemplo, para guardar una lista de pacientes de una clínica (nombre, dni, telefono, obra social) es conveniente utilizar un binario. Sin embargo, no es conveniente incluir el historial del paciente en el binario. Una solución es guardar cada historial en un archivo de texto separado, y colocar en el struct que se guarda en el binario con los datos del paciente, el nombre de su archivo de historial si es necesario (podría ser por ejemplo, su dni mas la extensión ".txt", en cuyo caso no hace falta guardarlo).
Otra aspecto importante a considerar es si la base de datos estará en memoria, o se trabajará directamente desde el archivo. Cuando son bases de tamaños razonables sobre las cuales hay que hacer muchas operaciones frecuentes, conviene tenerlas en memoria (por ejemplo en un vector stl) para que su acceso sea más rápido y más fácil. Entonces, el programa debe leer la base sólo una vez al inicio y luego trabajará con la información del vector. La unica contra es que si la información se actualiza solo en memoria y el archivo se escribe una vez al finalizar el programa, se puede perder información (si se corta la luz, el programa falla, etc) por lo que es conveniente reescribir el archivo volcando el contenido del vector luego de una operación importante. Por otro lado, cuando la base de datos crece indefinidamente (ejemplo, historial de alquileres de un videoclub, registros de venta de un almacen, etc) puede resultar cada vez más costoso este mecanismo (más tiempo de lectura, más uso de memoria), por lo que en esos casos es aconsejable trabajar directamente sobre el archivo.
Supongamos por ejemplo un videoclub. .. la lista de socios y peliculas se manejaria con un vector, porque es mas rapido trabajar en memoria que en archivos y mas facil para el programador utilizando contenedores stl (agregar, quitar, borrar, ordenar, etc). La cantidad de clientes y películas es más o menos conocida y no crece indiscriminadamente con el tiempo. Ademas, son datos que se requieren a cada rato. Por otro lado, los registros de alquileres no se cargarían en memoria, porque si se guardan todos, ese archivo va a a crecer por siempre, ya que todos los dias agregaria numerosas entradas. En ese caso cargar todo en un vector o en una lista es innecesario (puede haber años de alquileres acumulados), puede ocupar mucha memoria y es lento. Ahi se usaría el archivo directamente cuando se requiera una búsqueda. Opcionalmente, se podría llevar un control paralelo donde solo se cargue en memoria los alquielres pendientes de devolución y las películas reservadas que son los datos que se necesitan con frecuencia. En todos los casos se utilizarían archivos binarios.
Un problema importante acerca del uso de archivos binarios aparece cuando se requiere borrar un registro. Si se utiliza un vector u otro tipo de contenedor para trabajar en memoria, el problema no aparece ya que al guardar los datos se reescribe completamente el archivo. Sin embargo, si se trabaja directamente sobre el archivo hay dos problemas: cuando se elimina un item de un vector, se deben desplazar todos los items posteriores una posición hacia arriba, y este trabajo es muy lento en un archivo; el tamaño del archivo no se puede reducir, por lo que siempre queda un registro basura al final. Una solución es agregar en el struct del dato que se guarda una bandera (booleano) que indique si el dato es útil o no. Cuando se solicita borrar un dato, simplemente se le invierte la bandera, lo cual es rápido y simple. No hay necesidad de mover datos ni cambiar el tamaño del archivo. Sin embargo, a la hora de listar resultados o buscar registros, se debe tener en cuenta que los registros que tienen su bandera en falso deben ser ignorados. El único problema de este enfoque es que los registroos borrados siguen ocupando lugar. Se puede incluir en el programa una opción para "compactar" la base de datos eliminando así estos elementos innecesarios. Esta operación es lenta y requiere el uso de archivos auxiliares, pero no es necesario ejecutarla frecuentemente (a veces una vez al mes, o una vez al año, según el nivel de uso del software).
¿Dónde tiene que guardar los archivos mi programa?
Los archivos que utiliza o genera un programa nunca deben guardarse en rutas fijas. Ejemplos de rutas completas son: "C:\archivos de programa\mi_super_programa\datos.txt" (¿qué pasa si windows está en inglés y la carpeta es "Program files"?, ¿o si está en otra unidad que no sea C?) ó "D:\documents and settings\Pepe\Escritorio\informe.txt" (Sólo sirve para usuarios de nombre Pepe, y para versiones de Windows que usen "Documents and Settings"). Lo correcto es poner rutas relativas, o pedirle al sistema que nos indique la ruta completa.
Rutas relativas: las rutas relativas son las que no comienzan con "/", "\" o con "X:". Parecen incompletas, y de hecho el sistema completa la parte que falta (el comienzo) con la carpeta de ejecución del programa. Si la ruta es "datos\clientes.txt" y ejecutamos un programa con el explorador que está en d:\programa, la ruta completa para ese caso sería "d:\programa\datos\clientes.txt", pero si ejecutamos el mismo programa desde "c:\program files\Prog", la ruta completa que utilizará será "c:\program files\Prog\datos\clientes.txt". Entonces, utilizando rutas relativas (en la mayoría de los casos ponemos símplemente el nombre del archivo), los datos se leen y se guardan desde la carpeta del programa, o subcarpetas de esta, sin importar donde se encuentra instalado. Hay que aclarar dos cosas: 1) cuando corremos el programa desde un IDE, éste nos puede permitir modificar la "Carpeta de trabajo" y hace que sea una diferente a la que contiene el ejecutable (por ejemplo, en ZinjaI por defecto la carpeta de trabajo es la carpeta del proyecto, aunque el ejecutable se encuentre en la subcarpeta Debug), y lo mismo puede suceder cuando se utiliza un ícono de acceso directo; 2) esta puede ser una mala idea ya que el programa podría ser instalado por un administrador en carpetas donde los usuarios comunes no tiene permisos para crear o modificar archivos (ver item siguiente para mejor solución).
Rutas del sistema: Usualmente un sistema operativo prevee una carpeta para cada usuario donde éste debería guardar sus datos, configuraciones, documentos, etc. En GNU/Linux, esta carpeta es /home, mientras que en windows en X:\Users ó X:\Documents and Settings. El programa puede "preguntar" al sistema cual es esta carpeta y colocar sus archivos allí, en una subcarpeta propia. Para hacerlo hay dos formas: leer variables de entorno, ó utilizar una biblioteca. En GNU/Linux, la variable HOME contiene la dirección, mientras que en Windows, lo hacen variables como USERPROFILE, HOMEPATH, APPDATA (según el uso que le demos). Para leer una variable de entorno en C++ se puede utilizar la función getenv de la biblioteca csdtlib. Esta función recibe el nombre de la variable en un cstring como argumento y devuelve un puntero a un cstring con el contenido de dicha variable (se debe copiar ese contenido a una variable local, no modificarlo directamente). Otra forma de conocer los directorios especiales del sistema es utilizando una biblioteca. Por ejemplo, en wxWidgets existen la función wxGetHomeDir en wx/utils.h, o la clase wxStandardPaths. Lo mismo se aplica para obtener un lugar donde guardar archivos temporales.
En http://en.cppreference.com/w/cpp hay también una referencia bastante completa, que incluye además los elementos del la última versión del lenguaje (marcados con C++11, como expresiones regulares, funciones lambda, manejo de hilos, etc). Lo interesante de esta segunda alternativa es que se puede descargar al disco para explorar más tarde sin conexión a internet desde este link
¿Cómo utilizo las clases que ya tengo diseñadas en un nuevo proyecto?
Supongamos que tenemos una clase ya desarrollada en los archivos miclase.h y miclase.cpp y queremos utilizarla en un nuevo proyecto. Para utilizar esta clase se requiere:
Realizar el #include correspondiente en el código cliente.
En el IDE: indicar que el archivo .cpp debe ser compilado.
Si sólo realizamos el include, al compilar el proyecto se podrá compilar correctamente el objeto del cpp cliente, pero no se podrá enlazar el ejecutable (el error será del tipo undefined reference to... en gcc). En el ejecutable se debe incluir el código objeto de todos los cpps (los de las clases previamente desarrollada y los del programa cliente). Para esto, hay que asociar al proyecto los archivos de las clases. No alcanza con copiar el .cpp y el .h a la carpeta del proyecto (aunque es conveniente hacerlo de todos modos para que todos los archivos de un proyecto estén en la carpeta del mismo), sino que hay que indicarle al IDE que debe tenerlo en cuenta.
Por ejemplo, si se utiliza un proyecto en ZinjaI, hay que abrir los archivos .cpp .h (tenien abierto en ZinjaI el proyecto). Al realizar esta acción (menu Archivo->Abrir... y seleccionar los archivos) ZinjaI preguntará si se desea agregar estos archivos al proyecto. Para ver que un archivo está efectivamente en el proyecto podemos observar el árbol de proyecto (ubicado en un panel contra el margen izquierdo). Notar que si estamos utilizando ZinjaI pero no creamos un proyecto no podremos hacerlo; para crear un proyecto hay que ir al menú Archivo y seleccionar Nuevo Proyecto. Si se utiliza por ejemplo Borland Builder 6, hay que ir al Project Manager (desde el menú View), hacer click con el botón derecho sobre el ejecutable (por ejemplo Project1.exe) y seleccionar el comando Add del menú contextual.
¿Cómo utilizar una misma instancia de cierta clase en todo el programa?
Es muy común que un programa orientado a objetos tengamos una o unas pocas clases que controlan todo el modelo de datos. Por ejemplo, en un software de gestión de bibliteca, podemos tener las clases libro, socio, y prestamo para representar un libro, un socio, o un prestamo; pero también las clases como AdminLibre, AdminSocios, AdminAlquileres, para gestionar las listas de libros, socios, y alquieleres respectivamente. También puede ser necesario una clase aún más global, llamada por ejemplo Bibliteca, que gestione los tres administradores y algunos detalles más. Para que nuestra aplicación sea consistente es muy probable que deba interactuar siempre con la misma instancia de Biblioteca (o las mismas tres instancias de Admin*). Por ejemplo, en una aplicación con interfaz de ventanas, todas las ventanas (probablemente implementadas en .cpp individuales) deben operar sobre la misma base de datos.
La forma más rápida de hacerlo es declarar un puntero global, que se pueda "ver" desde todo el programa. Para esto tenemos que declarar el puntero en algún archivo .cpp de la forma convencional (Biblioteca *la_biblioteca=NULL;), fuera de cualquier función; y además declararlo con la palabra clave "extern" en algún .h (extern Biblioteca *la_biblioteca). Una declaración sin extern es obligatoria en alguna parte del programa, pero debe ser en un archivo .cpp, ya que si lo hacemos en un .h e incluimos ese .h en más de un lugar (por ejemplo en dos .cpp), tendremos en realidad dos punteros. La declaración en el .h es necesaria porque de otra forma los demás .cpp no verían el puntero. Entonces, la declaración con extern en el .h le avisa al compilador que ese puntero existe, y que está definido en otro lado, que no tiene que volver a reservar memoria cada vez que algún .cpp lo incluye. Queda como responsabilidad del programador asegurarse que solo se asigne una vez una dirección de una instancia real a ese puntero (la_biblioteca = new Biblioteca(...)), y que esa asignación se realice antes de que el puntero sea requerido para operar sobre sus datos.
Una mejor alternativa desde el punto de vista del diseño es utilizar el patrón singleton. Este patrón está diseñado para garantizar que existe solo una instancia de una dada clase, y que esta será creada e inicializada antes de que algún método o función cliente la utilice. Para ello, el patrón consiste en definir un único constructor privado para que nadie pueda construir un objeto de este tipo desde fuera de la clase. La clase presenta como público un método estático que retorna el puntero a la única instancia, la cual se almacena en un atributo privado también estático. Ese método se encarga de construirla la primera vez que se lo invoque, y de retornar siempre la misma. Supongamos que queremos extender la clase Biblioteca para obtener una versión singleton, podemos hacerlo por herencia como sigue:
class BibliotecaSingleton:public Biblioteca {
private:
static Singleton *instancia; // puntero a la unica instancia
BibliotecaSingleton():Biblioteca(...){}; // constructor privado
public:
// Método para crear/obtener la única instancia de la clase
static BibliotecaSingleton *ObtenerInstancia() {
if (!instancia) instancia=new Singleton(); // si no existe la instancia, se crea
return instancia;
}
};
Singleton *Singleton::instancia=NULL; // esto va en el .cpp
A la hora de utilizar la clase, se obtiene el puntero mediante este método: BibliotecaSingleton *la_biblioteca=BibliotecaSingleton::ObtenerInstancia();.
Para saber cuales son los argumentos adicionales para cada compilador (por ejemplo, macros a definir) hay que leer la documentación de cada biblioteca, o buscar una plantilla de proyecto para nuestro IDE. Sin embargo, en los sistemas GNU/Linux la mayoría de las bibliotecas utilizan un sistema común que nos permite obtener los parametros para la versión que hemos instalado en nuestro sistema. Para ello utilizamos el comando pkg-config y como argumento el nombre de la biblioteca (en algunas bibliotecas se reemplaza por un comando propio como wx-config para wxWidgets). El otro argumento necesario es el tipo de parametros que necesitamos: --cflags y/o --cppflags muestran los parámetros necesarios para compilar un objeto que utiliza la biblioteca; --libs muestra los parámetros necesarios para enlazar un programa que utiliza la biblioteca. Por ejemplo, para utilizar la biblioteca GTK+ 2.0 en GNU/Linux, los comandos "pkg-config gtk+-2.0 --cflags" y "pkg-config gtk+-2.0 --libs" muestran la lista de argumentos para compilar y enlazar respectivamente. En ZinjaI, por ejemplo, las plantillas de proyectos wxWidgets utilizan este sistema en las configuraciones para GNU/Linux (en las opciones del proyecto la llamada a wx-config aparece entre acentos, esto significa no debe insertarse el texto literal, sino que debe reemplazarse por la salida de ejecutar dicho comando), mientras que en las configuraciones para Windows estan ingresadas una a una las bibliotecas y las rutas adicionales en forma fija.
Puede encontrar más ayuda sobre como configurar el uso de bibliotecas externas en ZinjaI aquí. En enlace incluye una guía paso a paso y un resúmen de los errores más comunes y sus causas.
Depende mucho del tipo de juego. Si el juego no requiere animaciones ni respuestas en tiempo real (ajedrez, juegos de cartas, de tablero, ahorcado, etc.) se puede utilizar los mismos toolkits gráficos que utilizamos para otras aplicaciones de escritorio (wxWidgets, QT, GTK+, Borland VCL, etc.). Sin embargo, si el juego requiere un mayor despliegue gráfico (animaciones, sprites, transiciones, etc.) se necesita otro tipo de bibliotecas. Es decir, bibliotecas orientadas al uso de recursos multimedia. Utilizar primeras las primeras es más fácil porque se encargan de gestionar los eventos, y presentan todo tipo de controles y componentes (cuadros de texto, de mensaje, diálogos de selección de archivo, etc). Utilizando una biblioteca del segundo grupo generalmente debemos hacernos cargo del bucle de eventos (un bucle que debe ejecutarse contínuamente preguntando en cada iteración por los posibles eventos para llamar a las funciones o métodos que correspondan), y no disponemos de controles básicos como cuadros de ingreso de texto (hay que programar esto desde un nivel mucho más bajo). Sin embargo, como ventaja resultan mucho más eficientes y permiten el manejo de gráficos de una forma más natural. Dentro de las cientos de posibles candidatas de este segundo grupo, recomiendo empezar por Simple Fast Multimedia Library, ya que presenta una interfaz orientada a objetos muy simple y facil de utilizar, que permite gestionar gráficos, sonido y entradas de teclado, mouse y joystick eficientemente.
Esta biblioteca se puede utilizar fácilmente en un proyecto en ZinjaI descargando el complemento desde esta dirección.
Quiero hacer una aplicación que utilice la red ¿Qué necesito?
Se necesitan dos cosas:
aprender a manejar sockets
inventar un protocolo
Los sockets son digamos que algo asi como el componente de software que representa una conexión por red tcp/ip o udp/ip... Se puede trabajar directamente con la api del sistema operativo (en cuyo caso se tiene que implementar manualmente un bucle de eventos, es decir preguntar a cada rato si llego un mensaje nuevo), o usar una bilbioteca y entonces es mas simple porque recibir datos en un sockets es un evento más (como hacer click en un botón o cerrar una ventana). Para trabajar de la primer forma, como ejemplo se pueden analizar/reutilizar los archivos zockets.h y zokcets.cpp de los fuentes de PSeInt. En ellos hay funciones muy muy simples para inicializar, enviar y recibir datos, que funcionan en windows y en linux sin utilizar ninguna biblioteca externa además de las bibliotecas de sockets del propio sistema operativo. Para ejemplo de lo segundo, depende de la biblioteca que se utiliza (en google se pueden encontrar miles). Para ejemplo de sockets en wxWidgets, por ejemplo, en los fuentes de la bibliotecas (los que aparecen en la categoría "Source Archives") se encuentra una carpeta samples con ejemplos simples y basicos de todos los controles. Hay uno de sockets utilizando las clase wxSocketClient (socket del programa que realiza la llamada) y wxSocketServer (socket del programa que espera y atiende las llamadas).
El tema del protocolo es un problema de la lógica del programa, es pensar qué mensaje se envía, cómo se reconoce cuando se recibe, qué estructura tiene, qué significa, etc... Como cuando se guardan datos en un archivo, que se decide qué campos van y en qué forma (orden, tamaño, cantidad); solo que en las comunicaciones los mensajes a un socket abierto pueden llegar en cualquier momento, o a veces por partes (en varios enviós si es muy grande), entonces es un poco más complicado.
Hay que poner todo lo que sea necesario para que el programa funcione:
Ejecutable (fundamental) y dependencias (ver mas abajo)
Imagenes, iconos, textos, archivos de ayuda, lo que sea que tu programa cargue
Bases de dato en blanco o de ejemplo si las necesita
Respecto al ejecutable:
En todos los IDEs se puede elegir entre una configuracion Debug y una Release (en ZinjaI, menu Ejecutar->Opciones, y elegir en el combo de arriba a la izquierda; en Builder, menu Project->Options y en la parte inferior de la pestaña Compiler hay dos botones: Debug y Full Release). El ejecutable que se genera con la configuración Debug no esta optimizado (trucos que usa el compilador para que funcione más rapido) y tiene información de depuracion (que no hace falta si no se va a utilizar un depurarador). Entonces, para el usuario final, hay que compilar con Release, ya que el ejecutable va a ser mas chico y va a andar mas rapido. Los demas temporales que se encuentran dentro de las carpetas release/debug (archivos .o) no van en el instalador, ya que son solo archivos intermedios que luego pasan a formar parte del ejecutable.
Otro tema de interés es el uso de bilibotecas externas. Si se usa enalazado dinamico, el programa necesita las bibliotecas (archivos .dll en Windows, .so en GNU/Linux), y entonces el instalador las tiene que copiar (en Windows, en la carpeta del tu programa o en System32). Si se usa por ejemplo alguno de los productos de Borland, por defecto se enlaza dinámicamente asi que hay que copiar dlls (y bpls?) o cambiar las opciones para que compile estáticamente (poner las bibliotecas dentro del todo en el exe). Si se usa ZinjaI y wxWidgets no se necesita nada porque la compilación de wxWidgets que incluye ZinjaI es estática. Si se utiliza Builder, en las opciones de proyecto (menu Project->Options) hay que desactivar la opción "Use dynamic RTL" de la pestaña Linker y la opción "Build with runtime packages" de la pestaña Packages.
¿Cómo convierto entre cadenas (char*,string,...) y números (int,float,...)?
Entre las funciones del C++ estándar encontramos atoi y atof (biblioteca cstdlib) para convertir desde cadenas (cstring) a enteros y flotantes respectivamente. Sin embargo, las funciones que hacen lo contrario (itoa, ftoa) no existen en el estándar. Algunos compiladores las implementan igual, pero si queremos escribir un código correcto y portable no debemos fiarnos de ello. Para realizar esta conversión de forma fácil, hay tres maneras:
Utilizar stringstreams: los stringstream son objetos que tienen las características de los flujos, entre ellas la posibilidad de utilizar los operadores << y >>, pero su contenido no se guarda en un archivo ni se imprime en la consola como los flujos que utilizamos más habitualmente (cin,cout,ifstream,ofstream), sino que se guarda en memoria, y se puede extraer como un objeto de tipo string. Entonces, una forma fácil para colocar un número en un string, es crear un stringstream vacio, concatenarle el int/float/double con << y luego extraer el string. Ejemplo: int x=5; stringstream ss; string res; ss<<5; res=ss.str(); // res finalmente contiene el entero x convertido a string
Utilizar sprintf: la función sprintf escribe en un cstring de la misma forma que lo hace printf en la consola (el cout de C). Los argumentos son, la cadena donde se escribe, el formato de la escritura y luego las variables. Para escribir un entero el formato es "%i", mientras que para escribir un flotante el formato es "%f", o "%.2f". En el segundo caso, el 2 indica que se debe escribir con 2 decimales. La ventaja de este método es que podemos agregar texto constante en el formato (por ejemplo el postfijo "$" para montos de dinero) o escribir más de una variable en la misma llamada. Ejemplo: char c[20]; double x=2.5; sprintf(c,"%.2f $",x); // escribe "2.50 $"
Utilizar una biblioteca: no se justifica utilizar una biblioteca sólo para la conversión, pero si de todas formas estamos utilizando alguna biblioteca (por ejemplo para la interfaz gráfica) y ésta contiene funciones o clases para simplificar esta tares podemos aprovecharlas.
En los productos de Borland, las bibliotecas propias incluyen las funciones StrToInt, IntToStr, StrToFloat y FloatToStr para realizar las conversiones. Si queremos controlar con detalle el formato (por ejemplo, especificar la cantidad de decimales) debemos utilizar FloatToStrF.
Si utilizamos wxWidgets, la clase wxString presenta las propiedades de los flujos de salida, por lo que podemos utlizar << como si fuera un stream. Por ejemplo, para colocar un entero x en un cuadro de texto: text_ctrl->SetValue( wxString()<<x ); Si queremos controlar mejor el formato debemos utilizar el método PrintF, que recibe los el mismo tipo de formato que sprintf. Ejemplo: wxString aux; aux.PrintF("%.2f",x);. Para el proceso inverso, wxString tiene los método ToLong y ToDouble que convierten a entero largo y double respectivamente. Ambas reciben un puntero a la variable donde colocan el valor convertido, y devuelven un booleano indicando si la conversión se realizó con éxito.
Si el programa compila correctamente pero en algún momento de la ejecución se detiene de forma anormal (vemos segfault o violación de segmento en Linux, o el clasico cartel de error en Windows donde apretamos "No Enviar" sin leer, o cualquier mensaje de ese estilo) lo primero que se suele hacer es correr nuevamente el programa pero utilizando un depurador (en ZinjaI, presionando la tecla F5). Luego intentamos que el programa vuelva a detenerse repitiendo la secuencia de pasos que causa el problema. Cuando el programa se interrumpa por el error el depurador debería mostrarnos el trazado inverso (qué funciones estaban en curso cuando ocurrió el error) y habilitarnos la tabla de inspecciones para ver qué sucedió (si no está familiarizado con los conceptos básicos de depuración puede comenzar con esta guia o los tutoriales de su IDE). Lo primero que hay que buscar son índices fuera de rango (por ejemplo un índice de un arreglo de 20 elementos que vale 20 o más, o negativo), punteros con direcciones erróneas (NULL o muy bajas, incluso en el puntero this), divisiones donde el divisor pueda ser cero, etc. Hay que buscar en todos los niveles del trazado inverso que hayamos implementado. Para que esta búsqueda sea más facil siempre conviene inicializar todas las variables que no se usan inmediatamente (por ejemplo, comenzar con todos los punteros en NULL). Otra pista importante puede venir desde los warnings del compilador. Es conveniente intentar que nuestro código no genere warnings de ningún tipo ya que aunque la mayoría son inofensivos, si nos acostumbramos a ver numerosos warnings en cada compilación dejamos de prestar atención a los mismos y pasamos por alto los importantes. Finalmente, si no se encuentra el problema se recurre a la ejecución paso a paso. Se puede colocar un punto de interrupción antes de que surja el problema (por ejemplo, en la primer línea del evento donde se produce) y utilizar la ejecución paso a paso para acotar el momento. Esto también es útil cuando el programa explota de tal forma que el depurador no es capaz de obtener el trazado inverso. La habilidad para depurar es una de las habilidades más importantes y útiles para un programador, y sólo se desarrolla con la práctica.
¿Cómo obtengo la hora y/o la fecha del reloj del sistema?
Entre las funciones estándar de C++ tenemos time. Esta función devuelve un valor de tipo time_t, que en la mayoría de los compiladores es un entero con la cantidad de segundos que transcurrieron desde el 1ero de enero de 1970 a las 0:00. En este valor están codificadas fecha y hora. La ventaja de esta codificación radica en que así se pueden sumar o restar (las diferencias dan en segundos). Para extraer dia, mes, horas, minutos de una variable de tipo time_t se utilizan las funciones gmtime y localtime, que convierten de un tipo time_t a un tipo tm (devuelve un puntero a una variable de tipo tm). La diferencia entre una y otra es que una utiliza la hora local mientras que otra la hora UTC. El tipo tm es una estructura que contiene los campos dia del mes, dia de la semana, mes, año, horas, minutos, segundos, etc por separado. Solo hay que tener en cuenta cómo interpretar cada campo. Por ejemplo, el día del mes va de 1 a 31, pero el mes va de 0 a 11 (entonces el 1 es febrero), y el año comienza a contar desde 1900, entonces 111 significa 2011, etc. Ejemplo de uso:
time_t t1 = time(NULL);
tm *ptm2 = localtime( &t1 );
cout << "Fecha actual (local): " << ptm2->tm_mday << "/" << ptm2->tm_mon+1 << "/" << ptm2->tm_year+1900 << endl;
cout << "Hora actual (local): " << ptm2->tm_hour << ":" << ptm2->tm_min << ":" << ptm2->tm_sec << endl;
Todas estas funciones y tipos de dato estan definidos en la cabecera ctime.h.
En los casos en los que el proyecto utilice alguna biblioteca o toolkit adicional, se pueden utilizar clases más simples para esta tarea que provengan de estas bibliotecas, aunque generalmente solo es recomendable hacerlo dentro de las clases asociadas directamente a la interfaz, dejando el nucleo del programa independiente de la bilbioteca si este es el caso. Un ejemplo de esto sería la clase wxDateTime, que tiene métodos para asignas fecha y hora actual, y para obtener cada una de las partes por separado y en diferentes formatos o escalas.
¿Qué tengo que tener en cuenta para que mi programa funcione en Windows y GNU/Linux?
Es posible escribir un programa que se pueda compilar en varios sistemas operativos (so) sin (o casi sin) tenes que hacer ningún cambio, pero para eso hay que tener ciertas precauciones:
El IDE: Si van a desarrollar y probar en diferentes sistemas, es bueno utilizar un IDE portable (como ZinjaI :) para que puedan así trabajar siempre de la misma manera, y además para que el IDE ya les solucione algunas tareas. ZinjaI maneja las conversiones que necesita para sus cosas internas automáticamente cuando abrimos un proyecto de un so en otro. Para las cosas que debe configurar el usuario (la compilación: rutas de bibliotecas y esas cosas), todos los IDEs tienen perfiles de compilación (en ZinjaI, solo en proyectos, menú Ejecución->Opciones). Si usamos bibliotecas no estándar vamos a necesitar seguramente distintos perfiles para los disitintos sistemas. Las plantillas de ZinjaI que usan bibliotecas externas como las de wxWidgets y SFML ya tienen estos perfiles predefinidos. Si crean un proyecto en blanco y configuran ustedes las plantillas, deben tener en cuenta esto y crear perfiles separados.
Las Bibliotecas: Si usan bibiotecas estándar no van a tener ningún problema en compilar en cualquier plataforma. Si usan bibliotecas adicionales deben buscar las que sean portables (que se consigan o puedan compilar en distintos os). Por ejemplo, wxWidgets es en realidad un wrapper muy complejo para las cosas específicas de cada sistema. Es decir, en GNU/Linux, las implementaciones de los métodos de wx llaman a cosas de gtk, mientras que en Windows usan la winapi. Así las diferencias quedan ahí adentro y nuestro progama no tiene porqué enterarse (los métodos de wx se ven siempre igual desde "afuera" más allá de qué usen "adentro"). Sin embargo, pueden notar pequeñas diferencias entre las distintas versiones y hay que tener cuidado con eso, sobre todo porque no tenerlo en cuenta puede ser un error y puede que si probamos siempre en una misma plataforma no lo notemos. Por ejemplo, los tamaños por defecto de los controles pueden ser diferentes y entonces lo que pensamos para un so no se ve bien en otro (por eso wx usa sizers y se recomienda nunca poner tamaños fijos, sino tamños mínimos) u otros detalles (como que el orden en que se llaman dos eventes puede ser el inverso, por ejemplo el de recibir el foco y el de responder a un click en un wxTreeCtrl).
Compilación condicional:Si por alguna razón tienen un código que necesitan que sea diferente según la plataforma pueden usar directivas de preprocesador como sigue:
#ifdef __WIN32__
// hacer algo solo en windows
string so="Windows";
#else
// hacer algo solo en GNU/Linux
string so="GNU/Linux";
#endif
El ejemplo usa un if de preprocesador (antes de compilar se evalúa el if y se compila solo una de las dos opciones) y unas constantes que ya vienen definidas en cada sistema (las usuales son __WIN32__, __APPLE__, __unix__). Lo más común es preguntar, como en el ejemplo, si es Windows o es cualquier otra cosa, ya que el 99% de las otras cosas siguen un estándar llamado POSIX y en general con eso nos alcanza.
Nombres de Archivos: Los nombres de archivos (cualquiera, imágenes que carga una ventana, cabeceras que incluimos en el código, o los que genere nuestro programa) se tratan de forma diferente en Windows que en el resto de los so. Para empezar, Windows no distingue entre mayúsculas y minúsculas, pero el resto sí. Entonces en Windows es lo mismo el archivo "Hola.txt" que el archivo "HOLa.TXT" que el archivo "hoLA.TxT", pero en GNU/Linux son todos diferentes. Si programan en Windows, tengan cuidado de usar los nombres correctos para los #includes y de escribir los nombres de sus propios archivos siempre igual. Otro detalle está en cómo separan las carpetas. En Windows se usa una barra (\) mientras que en el resto del mundo otra (/). Sin embargo este problema es menor ya que en la mayoría de los casos podemos usar / y la biblitoeca/compilador/quien le toque la entiende igual aunque esté en Windows (por ejemplo, al hacer un #include <GL/glut.h>" no hay problemas compilando en Windows). En caso de haber problemas o querer ser cuidadoso pueden usar un #ifdef, o ver si alguna biblioteca que esté usando tiene algo para esto (como por ejemplo wxFileName::GetPathSeparator en wxWidgets). Otra recomendación importante para evitar muchos problemas es no usar espacios, ni ñs, ni acentos, ni otros caracteres raros en los nombres de archivos o carpetas.
Ubicación de los Archivos: A menos que el programa tenga un guardar como y el usuario elija donde, al elegir uno la ubicación es razonable no querer guardar los archivos que el programa genera en la carpeta de instalación, ya que esta será "opt/algo" o "usr/algo" en GNU/Linux, "program files/algo" o "archivos de programa/algo" en Windows, y se supone que el usuario común no tiene permisos para escribir ahí (aunque Windows hace un truco y anda). Cada so tiene carpetas específicas pensadas para esto. En GNU/Linux lo común es hacer una carpeta nueva llamada .algo (el . es para que quede oculta) en el home del usuario (que se obtiene con la variable de entorno $HOME). En Windows depende de las versiones (sobre todo pre/post vista) y no hay una variable como $HOME que de la correcta en todas, pero pueden usar %APPDATA% que sí está definida en todas y es lo más parecido (da un lugar razonable). Entonces, lo mejor sería que el programa haga una carpeta nueva en %APPDATA%/algo y ahi guarde las cosas (las que no le pregunta al usuario donde guardar). Para consultar estas variables de entorno (HOME, APPDATA, y otras) pueden usar getenv, que una función estándar de C.
¿Cuál es la diferencia entre el manual de usuario y el manual de referencia?
El manual de usuario de una aplicación es el manual que un usuario nuevo (que nunca utilizó la aplicación) leería para aprender a utilizarla. Usualmente incluye guías paso a paso con capturas de pantalla que explican cómo realizar las operaciones básicas (instalar el software, cargar datos, buscar datos, etc). Es conveniente organizarlo por funciones (qué se quiere hacer) y no por ventanas (ya que a priori un usuario no sabe a través de qué ventana podrá realizar determinada operación.
El manual de referencia es el manual que consulta un usuario que ya conoces los principios básicos del programa. Se utiliza para consultar cosas más específicas sin tener que leer toda la guía del manual de usuario. Buscar en él debe ser fácil y rápido, por lo que suele presentarse en forma de glosario ordenado alfabéticamente. Los items que se incluyen en el mismo suelen ser campos o controles (obligadamente los relacionados a entrada de datos, y opcionalmente los demás) y la información que presenta es una breve descripción del campo o control, y si es para entrada de datos qué restricciones presenta (longitud máxima, si acepta solo numeros, valor máximo o mínimo, etc), o cualquier otro detalle que se considere importante.
Debe tenerse presente que ambos manuales están destinados al usuario final de la aplicación, que en general no tendrá conocimientos de programación, por lo que las descripciones no deben incluir vocabulario propio del desarrollo (por ejemplo, no refererirse a un dato como float o double, sino como número real).
¿Cómo hago para que mi aplicación abra el manual (u otro documento)?
Para abrir documentos o cualquier tipo de archivo no ejecutable (archivos pdf, doc, html, etc), se requiere de un programa. Este programa puede no ser el mismo en distintas computadoras o sistemas operativos, y aún siendo el mismo puede no estar instalado en el mismo lugar. Es por esto que no conviene utilizar código como "system("C:\archivos de programa\acrobat\reader\acroread.exe manual.pdf")" para abrir por ejemplo un manual. Cada sistema operativo tiene sus mecanismos para asociar un tipo de archivo con una aplicación elegida por el usuario para abrirlo. Cuando nuestro programa necesita abrir un documento debe solicitar al sistema operativo que lo haga para que utilice sus mecanismos para lanzar la aplicación adecuada según el tipo de archivo (en caso de no ser posible o conveniente, nuestro programa debería permitir configurar qué aplicación externa utilizar). A continuación se muestra cómo hacerlo en sistemas Windows y GNU/Linux.
Windows: la API de windows incluye una función específica para esta acción que se denomina ShellExecute. Esta función recibe una dirección de un archivo y ejecuta el mismo (si es ejecutable) o lo abre con la aplicación asociada (si es un documento), de la misma forma en que lo haría si le hiciéramos doble click desde el explorador de archivos. Sus parámetros son: el handle de la ventana padre (puede ser NULL), el tipo de acción (usualmente "open"), la dirección del archivo, el comando completo (agregando argumentos si fuera un ejecutable), la carpeta de trabajo (puede ser NULL para heredar la del programa que lo llama), y el modo de visualización de la ventana (usualmente SW_NORMAL). Para usarla se debe incluir la cabecera <windows.h>. Entonces, para abrir por ejemplo el archivo "manual.pdf", el código sería: ShellExecute(NULL,"open","manual.pdf","manual.pdf",NULL,SW_NORMAL);
Nota: si genera un error relacionado a convertir char* a wchar_t* se debe anteponer un L a cada constante de texo: ShellExecute(NULL,L"open",L"manual.pdf",L"manual.pdf",NULL,SW_NORMAL);
GNU/Linux: aquí las asociasiones dependen muchas veces del escritorio que se utiliza (kde, gnome, xfce, etc). Sin embargo, en desde hace unos años se comenzó a utilizar una interfaz común para unificar estas tareas, denominada xdg. La mayoría de las distribuciones ya integra en su software de base un conjunto de ejecutables xdg-*. El programa xdg-open se utiliza para abrir un documento con la aplicación asociada a su tipo. Simplemente hay que ejecutar dicho comando agregando como parámetro el nombre del archivo. Si la ejecución desde C++ se realiza con la función system (de cstdlib), puede que nuestra aplicación se "congele" hasta que la apliación que muestra el documento finalize, ya que se ejecuta en primer plano. Para evitar esto, solo hay que agreagar un signo & al final del comando. Por ejemplo, para abrir el archivo manual.pdf el código sería: system("xdg-open manual.pdf &");
Es importante notar que en ambos casos el sistema debe tener una aplicación para abrir el tipo de documento en cuestión instalada, de lo contrario estas acciones no tendrán ningún efecto. Es recomendable usar formatos para los cuales existan aplicaciones gratuitas y livianas (utilizar pdf, y nunca doc), o que se encuentren presente por defecto en todos los sistemas operativos (html por ejemplo). Si queremos depender de aplicaciones externas debemos desarrollar en nuestra aplicación un visor de ayuda con nuestro toolkit gráfico. Por ejemplo, en wxWidgets, se incluye un control (wxHtmlWindow) para visualizar páginas HTML simples (sin uso de javascript, hojas de estilo, etc.), con el cual se puede desarrollar un visor muy básico con mínimo esfuerzo.
Es comun cuando se utilizan bibliotecas de componentes visuales no implementar una función main. Esto se debe a que la función main en realidad la implementa la biblioteca. Este es el caso de wxWidgets. La función main ya está implementada, por lo que al utilizarla wxWidgets no podemos utilizar nuestra propia función main (de hacerlo el compilador mostrará un error del tipo "multiple definitions of..."). Cuando declaramos la Application y utilizamos la macro IMPLEMENT_APP (última linea de Application.h en la plantilla de ZinjaI) estamos diciendo cual es la clase de nuestra applicación. La función main que se encuentra dentro de wxWidgets creará una instancia de esta clase y llamará al método OnInit de la misma. Es por esto que el equivalente a la función main para una aplicación con wxWidgets es el método OnInit de la clase que hereda de wxApp. Allí debemos colocar el código de inicialización de nuestro programa (cargas bases de datos, mostrar la ventana inicial, etc.). Este método debe retornar un valor booleano. Generalmente el valor será true indicando a la función main de wxWidgets que la aplicación se inició correctamente y que debe comenzar a escuchar eventos. Si se retorna false, la aplicación finaliza inmediatamente luego de ejecutar el método OnInit.
Para que al iniciar una aplicación que utiliza wxWidgets el usuario vea una ventana en particular hay que:
Crear una instancia de la clase de dicha ventana. Al diseñar una ventana estamos creando una clase, pero para que se muestre en pantalla debemos crear un objeto (una instancia de dicha clase). Esto usualmente se hace en el método OnInit de la clase que hereda de wxApp (ver ¿Dónde quedó la función main?). Notar que la instancia de la clase casi siempre debe ser creada dinámicamente (es decir, con el operador new), ya que de lo contrario (si es una variable estática, local en el método) la instancia se destruye al finalizar el método OnInit (lo cual ocurre generalmente de inmediato, de forma que la ventana ni siquiera llega a visualizarse).
Utilizar el método Show de wxFrame o wxDialog: cuando creamos una instancia de una clase que representa una ventana esta se carga en memoria pero no se muestra en pantalla. Para que efectivamente se muestre debemos invocar al método Show de la misma. Esto se puede hacer desde la función que crea la ventana (guardando un puntero a la misma al crearla y utilizandolo después para invocar a Show), o invocando al método Show directamente desde el constructor de la ventana (es conveniente que sea la última acción del constructor). Alternativamente, si es un dialog se puede usar ShowModal y evitar el new (ver ¿Que diferencia hay entre un wxDialog y wxFrame?)
Nota: Es un error muy común crear una instancia a la clase base (la generada por wxFormBuilder) en lugar de la hija (la herencia hecha en ZinjaI, que debería ser donde hacemos las implementaciones que nos interesan). Verificar esto en el método OnInit si la primer ventana no aparece al ejecutar el proyecto, o si no responde a eventos que ya están programados.
Veo mi ventana pero no hace nada cuando hago click en un boton
Para que la ventana responda a un botón hacen falta dos cosas: El código que relaciona el evento (click en el botón) con un método, y el método en sí.
Cuando se utiliza un diseñador visual como wxFormBuilder, el código que asocia eventos y métodos suele generarlo wxFormBuilder, por lo que sólo debemos corroborar que el método esté asignado en el diseñador (seleccionar el control y observar la pestaña Eventos del cuadro de propiedades que se muestra a la derecha de la ventana). Al implementar el método en nuestra clase heredada debemos observar que los prototipos coincidan perfectamente con el método declarado en la clase base generada por el diseñador. Esto generalmente implica que el método reciba por referencia un objeto de tipo wxCommandEvent,wxCloseEvent,wxResizeEvent,etc. según el tipo de evento generado. Otro error común en el uso de diseñadores de este tipo es crear una instancia de la clase diseñada en lugar de la clase heredada. Recordemos que al trabajar con wxFormBuilder, este no genera una clase con métodos virtuales para los eventos, de la cual debemos heredar nuestra propia clase. Es un error frecuente invocar a la clase diseñada en lugar de la clase heradada.
¿Dónde pongo los delete para las ventanas y controles?
Cuando se trabaja con wxWidgets las ventanas y controles usualmente se alocan dinámicamente con el operador new, pero no se desalocan con el operador delete. Las clases de la biblioteca implementan un sistema propio para liberar la memoria que funciona de la siguiente manera: cuando queremos liberar la memoria que utiliza una ventana y destruir dicho objeto, en lugar de utilizar al operador delete se debe utilizar el método Destroy de dicho objeto; al llamar a este método wxWidgets marca el objeto para ser eliminado, pero no lo elimina inmediatamente, sino que espera a que se procesen todos los eventos pendientes y luego lo elimina. Esto tiene dos ventajas: por un lado, se evitan los problemas que podrían surgir si hay pendiente un evento de un objeto y el objeto se elimina antes que el evento se procese; por otro lado, una ventana puede eliminarse a sí misma (se puede llamar a Destroy desde un método/evento de la propia ventana). Además, al destruir un componente que es padre de otros se destruyen todos ellos. Es por esto que si bien al crear una ventana se crean uno por uno los componentes (aunque este código lo suele generar automáticamente el diseñador), para destruirla sólo se invoca al método Destroy de la ventana, pues los mecanismos internos de wxWidgets se encargaran de eliminar los componentes contenidos en dicha ventana.
En conclusión, lo que conviene hacer para gestionar correctamente la memoria y evitar que queden cargadas ventanas que ya no se ven ni utilizan es llamar al método Destroy de la ventana. Suele ser conveniente hacerlo en el evento de cierre de la ventana (OnClose), ya que de esta forma se garantiza que siempre que la ventana deja de visualizarse se libera la memoria correspondiente. Al hacer esto, desde los demás eventos que deban eliminar la ventana (por ejemplo los botones Aceptar y Cancelar si es una cuadro de diálogo) basta con llamar el método Close, ya que este generará el evento de cierre y el evento invocará a Destroy.
Tanto wxFrame como wxDialog heredan de wxTopLevelWindow, por lo que comparten la mayoría de las métodos. Visualmente tienen diferentes estilos por defecto (el diálogo no aparece en la barra de tareas, no tiene botón maximizar, etc), y funcionalmente la ventana es más genérica mientras que el diálogo tiene algunas pocas pero útiles funcionalidades más específicas. Usualmente la ventana principal de una aplicación será de tipo wxFrame, y las ventanas emergentes, de opciones, de mensaje, etc. serán de tipo wxDialog. Un wxDiálog es usualmente una ventana auxiliar que se muestra sobre la ventana principal, en algunos casos funcionando a la par de esta, en otros casos bloqueandola hasta su cierre. Esta es una de las principales ventajas funcionales de wxDialog. Además del método Show, para mostrarse tiene el método ShowModal. Cuando un evento muestra una ventana con el método Show, el evento continúa ejecutandose hasta finalizar y luego quedan ambas ventanas visibles (la del el evento y el nuevo cuadro de diálogo). Cuando una ventana muestra un wxDialog con ShowModal, el evento que lo hace se interrumpe hasta que el cuadro de diálogo se cierre. Es decir, el código que le sigue a la llamada no se ejecuta hasta que el diálogo no termine. Esto tiene dos ventajas: la ventana principal que deshabilitada hasta que se cierre el diálogo (lo cual a veces es deseable), y el código del evento puede incluir ahí mismo código para reaccionar a los datos que el usuario ingresa en el dialogo.
Por ejemplo, si una ventana tiene una lista y al hacer doble click se crea una diálogo para editar un ítem de la lista, utilizando show se tienen los siguientes problemas: el diálogo debe recibir al crearse un puntero a la ventana que contienen la lista (o a la lista) para poder modificarla luego de que el usuario edite los datos y presione Aceptar; y como la ventana de la lista no se deshabilita, el usuario puede abrir varios diálogos de edición que editen el mismo ítem si no se control correctamente, lo cual genera problemas de lógica en el programa. Si se utiliza ShowModal el segundo problema se evita automáticamente ya que mientras un dialogo se muestra el usuario no puede interactuar con la lista. Además, dado que el evento del click en la lista queda a la espera de la finalización del cuadro de diálogo, el código para actualizar la lista puede incluirse en este mismo evento. Adicionalmente, el método ShowModal puede devolver un entero. Para ello el diálogo debe cerrarse mediante el método EndModal en lugar de Close, ya que EndModal permite especificar el valor de retorno. Este valor puede utilizarse por ejemplo, para saber si se debe actualizar o no la lista de la ventana principal (si el usuario cerró el cuadro de diálogo con el botón Aceptar o con Cancelar).
¿Por qué mis botones no se ven redondeados como en los demás programas de Windows Vista/7?
Los programas para Windows pueden incluir un archivo xml (manifest) con cierta información sobre cómo ejecutarse (versiones bibliotecas que utiliza, niveles de seguridad, etc). Si el xml no existe, Windows muestra las ventanas y demás controles de wxWidgets sin aplicarle el "tema". Para que se vean como el resto de las aplicaciones de Windows debemos incluir este archivo e indicar en él que queremos utilizar cierta versión de la bibilioteca de Windows que dibuja estos controles. Hay dos formas de utilizar este archivo: una es colocarlo dentro del ejecutable, la otra es nombrarlo igual que el ejecutable y agrerle ".manifest" al final. Cuando Windows ejecuta el programa, primero busca dentro del ejecutable, si no lo encuentra busca en la carpeta del ejecutable. Por ejemplo, si el ejecutable es zinjai.exe, el archivo manifest sera zinjai.exe.manifest. Si se quiere colocar dentro del ejecutable, en ZinjaI se puede indicar desde la pestaña "Enlazado" del cuadro de Opciones de Ejecucion y Compilación de Proyecto (menu Ejecutar->Opciones).
El contenido del archivo es el siguiente:
Las plantillas de proyectos wxWidgets y wxFormBuilder de las versiones de ZinjaI posteriores a 20110610 ya incluyen este archivo en las compilaciones para Windows.
¿Dónde consigo ejemplos y referencias de wxWidgets?
La biblioteca incluye una muy completa referencia en formato html. Se puede acceder desde este enlace, descargar desde esta pagina (Current Stable Release -> Documentacion -> HTML Docs), o en ZinjaI acceder rápidamente desde el ítem "Referencia wxWidgets" del submenú "Diseñar Interfases" del menú "Herramientas" (la versión para Windows ya incluye estos documentos, en Linux hay que descargarlos y configurar la dirección en la pestaña de rutas del cuadro de preferencias).
Para conseguir ejemplos se puede descargar los fuentes de la biblioteca (Current Stable Release -> Source Archive -> wxAll). Al descomprimir los fuentes se encuentra una carpeta "samples" que contiene ejemplos completos del uso de la mayoría de los widgets de la biblioteca. Para compilar estos ejemplos con ZinjaI pueden realizar los siguientes pasos: cerrar todos los archivos abiertos y abrir solo el .cpp del ejemplo (cada carpeta tiene un .cpp, si tuviera más de uno abrirlos todos). Crear un nuevo proyecto (Archivo->Nuevo Proyecto) en el directorio del ejemplo (seleccionar "Directorio Actual" en el asistente) con la plantilla de "wxWidgets" (no wxFormBuilder), y con los archivos abiertos (seleccionar "Utilizar los archivos abiertos" debajo de la lista de plantillas).
¿Cómo hago para que un control de texto (wxTextCtrl) permita ingresar sólo números o sólo letras?
Para restringir el tipo de entrada de un wxTextCtrl puede asociarse a dicho control un objeto de tipo wxTextValidator. Este tipo de objetos se construyen especificando los caracteres permitidos y, una vez asociados al control, descartan las entrada correspondiente a cualquier caracter no permitido.
Se puede asociar un wxTextValidator a un wxTextCtrl de dos formas: añadiendo el código necesario en algún punto de nuestro programa entre que se crea el cuadro de texto y se le da la oportunidad al usuario de utilizarlo, por ejemplo en el constructor de la ventana que lo contiene; o definiendolo en las propiedades del cuadro de texto en el diseñador wxFormBuilder.
El siguiente código inicializa un validador especificando la constante wxFILTER_NUMERIC, la cual indica que deben permitirse únicamente caracteres numéricos, y la asocia a un control de texto:
wxTextValidator v(wxFILTER_NUMERIC);
miControlDeTexto->SetValidator(v);
A partir de la asociación el control de texto descartará automáticamente toda entrada que no sea numérica.
Para hacerlo desde el diseñador wxFormBuilder sin tener que codificar, se debe:
1. Seleccionar el control de texto al que se le quiere aplicar la validación.
2. Entre las propiedades del objeto, buscar la sección Propiedades y modificar:
* validator_type: cambiar wxDefaultValidator por wxTextValidator
* validator_style: destildar wxFILTER_NONE y seleccionar el tipo de filtro que se quiera utilizar
* validator_variable: escribir un nuevo identificador para una variable que será atributo de la clase y que utilizará solamente el validator
* validator_data_type: seleccionar wxString
Los mismos pasos se pueden aplicar a muchos otros controles de entrada, variando solamente validator_data_type según el tipo de control (la ayuda que muestra el diseñador aclara para qué tipo se debe usar para cada uno).
¿Cómo hago para obtener una ventana que muestre los archivos y carpetas del sistema, similar a las que permiten abrir/guardar archivos en otros programas?
Para este tipo de dialogos, depende si lo que se desea seleccionar es un archivo o un directorio:
Para seleccionar un directorio se debe usar un objeto de la clase wxDirDialog.
Para seleccionar uno o varios archivos, se debe utilizar un objeto de la clase wxFileDialog.
Ambos objetos son muy similares y corresponden a diálogos, por lo cual se debe llamar a la función ShowModal() para que se muestren, y esta llamada bloquea la ventana padre hasta que se haya seleccionado un archivo/directorio. Una vez que la ventana de selección sea cerrada y el flujo del programa retorne a la ventana inicial, existen varias funciones que devuelven información sobre el ítem seleccionado.
El siguiente código, utilizado dentro de un método de alguna ventana, inicializa y muestra un diálogo de selección de archivos:
wxFileDialog fd(this, "Elija un archivo de texto", "C:\\", "", "*.txt");
// el 3er y cuarto parametro son el directorio inicial y el nombre de archivo inicial respectivamente
// el 5 parametro indica que se puede seleccionar cualquier archivo (* es como un comodin)
// siempre y cuando termine en ".txt"
if (fd.ShowModal()==wxID_OK) { // se compara con wxID_OK para asegurarnos de que el usuario eligió un archivo y no presionó "Cancelar"
wxString archivo = fd.GetPath(); // devuelve el archivo seleccionado con la ruta y todo
/** hacer algo con el archivo **/
}
Se puede agregar un argumento más en el constructor que consiste en una o más banderas de las indicadas en la ayuda de la clase (wxFD_* o wxDD_*), para decir si es un cuadro de abrir o guardar, si permite seleccionar más de un archivo, si permite escribir un nombre de archivo que no exista, si debe preguntar al aceptar antes de sobreescribir un archivo existente, etc.
Primero hay que distinguir que colocarle un ícono al ejecutable (solo Windows), que es el ícono que vemos en el explorador de windows y generalmente en los accesos directos; es diferente de colocarle un ícono a una ventana (aunque podría ser el mismo).
Para colocarle un ícono al ejecutable, hay que generar y compilar un archivo de recursos. Si utiliza ZinjaI, esto se hace automáticamente definiendo el archivo de icono (de extensión .ico) en la pestaña Enlazado del cuadro de Opciones de Compilación (menu Ejecutar->Opciones).
Para colocar un ícono en una ventana se utiliza el método SetIcon, presente en las clases wxFrame y wxDialog. Este método recibe un objeto de tipo wxIcon. Estos objetos se pueden construir a partir de un archivo de ícono simplemente pasando la ruta del archivo en el constructor. Usualmente este código se inserta en el constructor de la ventana (es necesario agregar el include de wx/icon.h). Ejemplo: SetIcon(wxIcon("archivo.ico"));
Alternativamente en lugar de utilizar una archivo de ícono se puede generar un archivo xpm. Un archivo xpm es una imágen codificada como código c++ (declaración de una estructura con los datos de la imágen). Podemos hacer un #include del archivo xpm y pasarle el nombre de la estructura al constructor de wxIcon. La ventaja de este método radica en que el ícono se compilará dentro del ejecutable, mientras que con el método anterior siempre tendremos que copiar el archivo de ícono además del archivo ejecutable al copiar el programa. Para generar archivos xpm o convertir de ico/png/etc a xpm podemos usar cualquier editor de imágenes que soporte este formato (por ejemplo, GIMP).
Advertencia: en muchos sistemas Windows los íconos sólo se muestran correctamente en las ventanas si tienen un tamaño de 32x32. De lo contrario sólo se muestran en la barra de tareas.
¿Cómo hago para que mi aplicación muestre un icono a la derecha de la barra de tareas, al lado del reloj?
Muchas aplicaciones pensadas para permancer abiertas muestran un ícono en la zona de notificaciones (en la barra de tareas, a la derecha, junto al reloj), que usualmente permite mostrar la ventana principal de la aplicación, o presenta un menú contextual con opciones. Para obtener estas funcionalidades con wxWidgets lo que hay que hacer es generar una clase que herede de wxTaskBarIcon. Esta nueva clase debe utilizar el método SetIcon para definir la imágen del icono. Hay dos formas de asociar eventos y métodos en una clase wxWindow: con el método Connect, o con una tabla de eventos generada por macros. En este ejemplo vamos a utilizar la segunda. Para ello, hay que agregar dentro de la clase un linea "DECLARE_EVENT_TABLE();", y luego, en el cpp, armar la tabla con las macros "BEGIN_EVENT_TABLE(nombre_de_la_clase,wxTaskBarIcon) y END_EVENT_TABLE()". Entre medio de estas dos macros, se colocan macros que comienzan con EVT_ y que asocian eventos y métodos. Sin embargo, para el caso en que se quiera desplegar un menú contextual con el click derecho, la clase base incluye un método virtual CreatePopupMenu que puede ser reimplementado en la clase heredada y que se llama automáticamente sin necesidad de las macros. Este método debe crear un wxMenu dinámicamente y retornarlo, la biblioteca se encargará de liberar la memoria cuando luego de mostrarlo.
En el siguiente ejemplo, se crea una clase mxTaskBarIcon que toma la imagen de un icono xpm y que presenta un menu con tres opciones.
En el archivo mxTaskBarIcon.h:
#ifndef MXTASKBARICON_H
#define MXTASKBARICON_H
#include <wx/taskbar.h>
// es necesario numeros para identificar las opciones del menu, que deben ser mayores a wxID_HIGHEST
enum { ID_OPCION_1=wxID_HIGHEST+1, ID_OPCION_2, ID_OPCION_3 };
// clase heredada
class IconoEnLaBarra : public wxTaskBarIcon {
public:
mxTaskBarIcon();
// metodo que construye el menu (click derecho)
wxMenu *CreatePopupMenu();
// metodos para las opciones del menu
void OnOpcion1(wxCommandEvent &event);
void OnOpcion2(wxCommandEvent &event);
void OnOpcion3(wxCommandEvent &event);
// metodo para el click izquierdo
void OnClickDerecho(wxTaskBarIconEvent &evt);
// para la tabla de eventos
DECLARE_EVENT_TABLE();
};
#endif
En el archivo mxTaskBarIcon.cpp:
#include <wx/icon.h>
#include <wx/menu.h>
#include "mxTaskBarIcon.h"
#include "icono.xpm"
// tabla de eventos
BEGIN_EVENT_TABLE(IconoEnLaBarra,wxTaskBarIcon)
EVT_TASKBAR_LEFT_DOWN(IconoEnLaBarra::OnClickDerecho)
EVT_MENU(ID_OPCION_1,IconoEnLaBarra::OnOpcion1)
EVT_MENU(ID_OPCION_2,IconoEnLaBarra::OnOpcion2)
EVT_MENU(ID_OPCION_3,IconoEnLaBarra::OnOpcion3)
END_EVENT_TABLE()
// constructor que asigna la imagen (si no es xpm, sacar el include y pasar el nombre de archivo como string al ctor. de wxIcon)
mxTaskBarIcon::mxTaskBarIcon() { SetIcon(wxIcon(icono_xpm),"Tooltip del icono"); }
// metodo que crea el menu
wxMenu * mxTaskBarIcon::CreatePopupMenu ( ) {
wxMenu *menu = new wxMenu();
menu->Append(ID_OPCION_1,"Texto opcion 1...");
menu->Append(ID_OPCION_2,"Texto opcion 2...");
menu->Append(ID_OPCION_3,"Texto opcion 3...");
return menu;
}
// metodos que se ejecutan cuando el usuario elije una opcion del menu
void mxTaskBarIcon::OnOpcion1 (wxCommandEvent &event) { ... }
void mxTaskBarIcon::OnOpcion2 (wxCommandEvent &event) { ... }
void mxTaskBarIcon::OnOpcion3 (wxCommandEvent &event) { ... }
// metodo para que tambien muestre el menu con el click izquierdo
void mxTaskBarIcon::OnClickDerecho(wxTaskBarIconEvent &event) { PopupMenu(CreatePopupMenu()); }
¿Cómo agrego atajos de teclado para mi aplicación?
Hay dos formas de asociar una combinación de teclas a un evento:
Utilizando el menu de la ventana: Si la ventana posee una barra de menu (wxMenuBar), cada item del menu (wxMenuItem) puede tener asociado un atajo de teclado. En el diseñador wxFormBuilder, la propiedad "shorcut" del cuadro de propiedades de un wxMenuItem permite definir dicho atajo. Allí se introduce un string con la combinación de teclas del atajo (ej:"F1", "Ctrl+D", "Alt+L", etc...). Cuando se utilice esa combinación de teclas en la ventana se invocará al mismo evente que cuando se hace click sobre el elemento del menu. Si el atajo se ingresa correctamente, al ejecutar la apliación este se muestra a la derecha del elemento del menú. Nota: no todas las combinaciones de tecla son posibles, ya que algunas pueden estar ocupadas por el sistema operativo o depender de la implementación.
Crear una tabla de atajos y asociarla a una ventana: este método es algo más trabajoso pero permite asociar atajos sin necesidad de que las acciones figuren en un menu. Para ello se debe crear una tabla de atajos, asociarla a la ventana, y asociar los eventos de los atajos con métodos de la ventana. Para lo primero se crea un arreglo estático de wxAcceleratorEntry, se lo carga cada item del arreglo con el método Set, se crea un wxAcceleratorTable utilizando pasando el arreglo creado en el constructor, y se asocia a la ventana con el método SetAcceleratorTable. Luego, para conectar los eventos se utiliza el mismo código que para conectar los eventos de menú, pero variando el id. El método Set de wxAcceleratorEntry recibe tres argumentos: teclas modificadoras, tecla, y id. Las teclas modificadoras son Control (wxACCEL_CTRL), Shift (wxACCEL_SHIFT), Alterno (wxACCEL_ALT), o ninguna (wxACCEL_NORMAL). La tecla principal del atajo puede es un int que se corresponde con los códigos ascii para los número y letras, y tiene definidas constantes para las demas teclas (WXK_SPACE, WXK_F1, WXK_TAB, ...). Finalmente, el id es un número para identificar ese evento. wxWidgets utilizar internamente ids que van desde 0 hasta la constante wxID_HIGHEST, por lo que podemos utilizar cualquier id superior. A continuación se muestra un ejemplo completo del código que inicializa dos atajos en una ventana y conecta los eventos (este código usualmente va en el constructor de la misma):
wxAcceleratorEntry entries[2];
entries[0].Set(wxACCEL_CTRL|wxACCEL_ALT, WXK_SPACE, wxID_HIGHEST+1);
entries[1].Set(0, WXK_F3, wxID_HIGHEST+2);
wxAcceleratorTable accel(2, entries);
SetAcceleratorTable(accel);
Connect( wxID_HIGHEST+1, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( VentanaPrincipal::OnCtrlAltEspacio) );
Connect( wxID_HIGHEST+2, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( VentanaPrincipal::OnF3) );
El código asocia las combinaciones Ctrl+Alt+Espacio y F3 a los métodos OnCtrlAltEspacio y OnF3 de VentanaPrincipal. Estos metodos, al igual que los métodos para eventos de menu, deben recibir por referencia un obtejo de tipo wxCommandEvent. En este caso se utilizan los dos primeros ids libres. Cuando se generan muchos eventos de esta forma conviene utilizar enum para definir palabras clave para cada id, de forma que el código se más facil de leer y modificar (enum mis_ids={EVENTO_CTRL_ALT_ESPACIO=wxID_HIGHEST+1,EVENTO_F3,EVENTO...};).
Nota: la ventana debe tener el foco de teclado para que estos atajos funcionen. Para eso, el foco debe estar en un control de la misma (por ejemplo en un botón o en un control de texto, se puede forzar con el método SetFocus del control desde el constructor de la ventana).
Un temporizador es un control que se encarga de generar un evento cada cierto período de tiempo. El control (wxTimer) no tiene representación visual, por lo que inicialmente puede no estar asociado a ninguna ventana. Para poder recibir sus eventos debemos asociarlo a una ventana. Esto se puede hacer mediante el constructor, que recibe el manejador de eventos de la ventana, el cual se obtiene con el método GetEventHandler. A continuación debemos asociar un método de la ventana al evento del timer utilizando el método Connect de la ventana. Finalmente, con el método Start de wxTimer, ponemos en marcha el temporizador. Start recibe dos parámetros, el tiempo (en milisegundos) y una bandera que indica si el evento debe dispararse una vez y detener el timer, o dispararse periodicamente. El método que recibe el evento debe tener como argumento un objeto de tipo wxTimerEvent por referencia.
El siguiente código, colocado en el constructor de una ventana (VentanaPrincipal), crea un timer, asocia su evento a un método de la misma (OnTimer), y lo inicializa para generar el evento cada 1 segundo:
wxTimer *timer = new wxTimer(GetEventHandler());
Connect(wxEVT_TIMER,wxTimerEventHandler(VentanaPrincipal::OnTimer),NULL,this);
timer->Start(1000,false);
Sigue estos pasos si el gestor de paquetes de tu distribución no tiene la versión de wxWidgets que necesistas (por ejemplo, en los primeros proyectos es más simple utilizar la versión ANSI pero los repositorios suelen tener la versión UNICODE):
Descomprimir los fuentes descargados: por ejemplo, si el archivo es wxWidgets-2.8.12.tgz, desde una consola se puede hacer con "tar -xzvf wxWidgets-2.8.12.tgz". Si la extensión es .tar.bz2, cambiar "-xzvf" por "-xjvf". Si es zip, utilizar "unzip -x wxWidgets-2.8.12.zip".
Abrir una consola y entrar en la carpeta descomprimida (con el comando cd).
Ejecutar el script de configuración con el comando "./configure --enable-ansi --disable-unicode" (si se quiere la versión unicode omitir los 2 parámetros). Se configura para instalar en el directorio "/usr/local", para cambiar, por ejemplo a "/opt/wx" agregar "--prefix=/opt/wx". Si todo va bien y el sistema tiene el compilador y las dependencias instaladas, el script mostrará un resúmen de la configuración elegida. Si hay algún error, probablemente se deba a la falta de una biblioteca e indique cual. Hay que intentar instalar la versión de desarrollo de la misma (que termina en -dev o -devel) con el gestor de paquetes de la distribución. Si en este paso aparece algún error, se deberá a que el sistema no tiene instaladas algunas dependencias necesarias. Usualmente, el paquete faltante será "gtk+-2.0-dev", así que lo que se debe hacer es instalarlo con el gestor de paquetes de la distribución y probar nuevamente ejecutar el "configure....".
Compilar la biblioteca con el comando "make", o alternativamente con "make -j 4" para compilar utilizando 4 núcleos en pcs con procesadores modernos.
Instalar la biblioteca con el comando "sudo make install"
Listo. Se puede intentar ejecutar el comando "wx-config --unicode=no --libs" para ver si está bien instalada (muestra una lista de archivos precedidos cada uno por "-l") o no (muestra algún mensaje de error).