Tutorial de wxWidgets.

Esto es un micro-tutorial de wxWidgets (http://www.wxwidgets.org/), "an open source, cross-platform, C++ GUI framework". Un compendio de los pasos básicos y elementales para que el arranque les resulte más fácil.

Recomendación general: miren todos los "tutoriales" que encuentren por ahi con escepticismo. La fuente más confiable es el mismo código fuente :) , luego el manual de la versión correspondiente, luego el resto de la documentación mencionada en el sitio, etc. Hay información desactualizada, reportes de "lo hice así y funcionó" como si fueran el único camino, etc.

Preparar el entorno de programación

Una serie de pasos a tener en cuenta:

  1. Entorno C++. Instalar, configurar o verificar el entorno de programación C++. Las instrucciones que siguen son generales en lo general, pero en los detalles se refieren al C++ del Microsoft Visual Studio .Net 2003 (en adelante msvc). Traten de no usar versiones anteriores, en lo posible, porque su soporte de C++ no es completo. Pueden usar otros compiladores modernos. Por ejemplo gcc en linux.
  2. wxWidgets. Obtener e instalar wxWidgets. Usen la versión más nueva que sea estable (al 20mar2006 la 2.6.2, pero la 2.6.3 está por salir). Por ejemplo bajándolo de la página oficial. Ver los txt que vienen con instrucciones de instalación (install-msw-2.6.2.txt, install-gtk-2.6.2.txt, etc.). En el caso de windows (wxMSW), las instrucciones remarcan que se debe instalar en un path sin espacios , ojo! Luego de poner todos los archivos en su lugar, hay que compilar la(s) librería(s) (bibliotecas)! Antes de compilar, puede ser conveniente editar el archivo "include/wx/msw/setup.h" según corresponda. Para empezar se puede dejar como está. Una forma fácil (en msvc) es usando wxWidgets-2.6.2\build\msw\wx.dsw.
  3. Directorios. Para poder usar los headers de las distintas clases y funciones, el compilador tiene que conocer el directorio donde están. Una manera es configurar el proyecto o makefile. Otras es configurar el entorno de trabajo (Tools|Options|VC++Directories en el caso msvc):
  4. Proyecto. Crear un proyecto del tipo apropiado. En windows es "win32 application" (no consola, no MFC o ATL). Configurar todas las opciones del proyecto a sus valores apropiados:

Primeros Pasos

Veamos la aplicación wxWidgets más simple posible (bueno, en realidad no). simple.cpp

	#include <wx/wx.h>

	class SampleApp : public wxApp
	{
	};

	IMPLEMENT_APP(SampleApp)
		

Si compilamos y corremos, el resultado es una aplicación que no hace nada, no muestra nada, y se cuelga. Al hacer break en el compilador, obtenemos el siguiente call-stack:

	simple.exe!GetMessage(tagMSG * lpMsg=0x0012fc30, HWND__ * hWnd=0x00000000, unsigned int wMsgFilterMin=0, unsigned int wMsgFilterMax=0)  Line 273 + 0x18	C++
 	simple.exe!wxEventLoop::Dispatch()  Line 318 + 0xf	C++
 	simple.exe!wxEventLoop::Run()  Line 247 + 0xd	C++
 	simple.exe!wxAppBase::MainLoop()  Line 272 + 0x1b	C++
 	simple.exe!wxAppBase::OnRun()  Line 340 + 0x10	C++
 	simple.exe!wxEntryReal(int & argc=1, char * * argv=0x00c414f0)  Line 417 + 0x1b	C++
 	simple.exe!wxEntry(int & argc=1, char * * argv=0x00c414f0)  Line 216 + 0xd	C++
 	simple.exe!wxEntry(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * __formal=0x00000000, HINSTANCE__ * __formal=0x00000000, int nCmdShow=1)  Line 299 + 0xd	C++
 	simple.exe!WinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, char * lpCmdLine=0x00132ce6, int nCmdShow=1)  Line 7 + 0x33	C++
 	simple.exe!WinMainCRTStartup()  Line 390 + 0x39	C
		

¿Qué significa todo esto?

La aplicaciones de windows consisten en un ciclo "getmessage(); dispatchmessage()". A diferencia de una aplicación "consola", luego de la inicialización una aplicación de windows funciona principalmente en respuesta a eventos. Se lo llama "orientado a eventos" o "event-driven", y para los que nunca programaron así es un cambio bastante grande.

El primer "evento" que vamos a atender es arrancar nuestra aplicación. simple2.cpp. En este ejemplo, en lugar de inicializar las cosas, mostramos un mensaje y salimos.

	#include <wx/wx.h>

	class SampleApp : public wxApp
	{
	public:
		virtual bool OnInit();
	};

	IMPLEMENT_APP(SampleApp)

	bool SampleApp::OnInit()
	{
		wxMessageBox( "Hola!", "Titulo de la ventana", wxOK | wxICON_INFORMATION );
		return false; //indica que NO se pudo inicializar; normalmente se devuelve true
	}
		

Agreguemos ahora una ventana, para que no sea tan aburrido. simple3.cpp

	#include <wx/wx.h>

	class SampleApp : public wxApp
	{
	public:
		virtual bool OnInit();
	};

	class TopFrame : public wxFrame
	{
	public:
		TopFrame();
	};

	IMPLEMENT_APP(SampleApp)

	bool SampleApp::OnInit()
	{
		TopFrame *frame = new TopFrame;
		frame->Show(true);
		return true; //true == arrancar la app
		//notar que no hay delete del frame
	}

	TopFrame::TopFrame()
	: wxFrame(NULL, wxID_ANY, "titulo de la vantana")
	{
	}
		

(Aclaración: esto es un ejemplo con fines pedagógicos, compactado para simplificar la presentación. En realidad la forma usualmente aceptada de poner todo este código es en 4 archivos: un .h con la declaración de la clase y un .cpp con la implementación de la clase, para cada una de estas dos clases.)

Ahora les muestro cómo agregar una barra de menú, y un item al menú, junto con el código para atender el evento que se genera. Notar que aparecen unas macros DECLARE_EVENT_TABLE y BEGIN_EVENT_TABLE, esta última con la tabla de eventos atendidos por la ventana. simple4.cpp

	#include <wx/wx.h>

	class SampleApp : public wxApp
	{
	public:
		virtual bool OnInit();
	};

	class TopFrame : public wxFrame
	{
	public:
		TopFrame();

	protected: // event handlers (NO virtual)
		void OnQuit(wxCommandEvent& event);

	private:
		// any class wishing to process wxWidgets events must use this macro
		DECLARE_EVENT_TABLE()
	};

	IMPLEMENT_APP(SampleApp)

	bool SampleApp::OnInit()
	{
		TopFrame *frame = new TopFrame;
		frame->Show(true);
		return true; //true == arrancar la app
		//notar que no hay delete del frame
	}

	// the event tables connect the wxWidgets events with the functions (event
	// handlers) which process them. It can be also done at run-time, but for the
	// simple menu events like this the static method is much simpler.
	BEGIN_EVENT_TABLE(TopFrame, wxFrame)
		EVT_MENU(wxID_EXIT,  TopFrame::OnQuit)
	END_EVENT_TABLE()

	TopFrame::TopFrame()
	: wxFrame(NULL, wxID_ANY, "titulo de la vantana")
	{
		//creamos un menu muy simple
		wxMenu *fileMenu = new wxMenu;
		fileMenu->Append(wxID_EXIT, "&Salir\tAlt-F4", "Termina el programa");
		wxMenuBar *menuBar = new wxMenuBar();
		menuBar->Append(fileMenu, _T("&Archivo"));
		SetMenuBar(menuBar);
	}

	void TopFrame::OnQuit(wxCommandEvent& WXUNUSED(event))
	{
		// true is to force the frame to close
		Close(true);
	}
		

Segundos Pasos

Ya vimos varios conceptos: un objeto que modela a la aplicación (su clase deriva de wxApp), un objeto para la ventana principal (su clase deriva de wxFrame; otra opción es wxDialog, y hay otras para MDI), el concepto de evento, las tablas de eventos, menúes.

Lo primero que queremos hacer es poder dibujar, hacer dibujos. Esto nos permitirá comenzar a hacer cosas "gráficas".

Para ello, agreguemos a la ventana del ejemplo anterior dos "sub-ventanas", una para poner controles (botones, editboxes, checkboxes, radiobuttons, sliders, ...), y otra para dibujar. Además, a modo de ejemplo, dibujamos una línea. Para dibujar hay que usar un device context (en este caso un wxClientDC, pero en usualmente va a ser un wxPaintDC). El problema es... ¿dónde (cuándo) dibujamos? Acá no tenemos un main() que nos provee el control del programa. Necesitamos un evento! Pero todavía no lo tenemos. Con fines ilustrativos, vamos a dibujar ni bien creamos la ventana. simple5.cpp

		(...)
		

Pero...

Epa! Si tapo la ventana, cuando la destapo la línea ya no está! Además, si le cambio el tamaño a la ventana, queda mal!  :-(

Para arreglar lo segundo, vamos a usar sizers. Esto es una solución, presente en varios frameworks, que permite al mismo tiempo "formatear" cuadros de diálogo u otros conjuntos de controles (ventanas), y que dicha "diagramación" sea elástica, adaptable a tamaño variable de ventana.

Lo primero es un problema fundamental. Dibujar en un DC de ventana es transitorio. Ningún sistema de ventana guarda las ventanas [por lo menos no en general], sino que deja como responsabilidad de cada ventana el redibujarla cuando es destapada luego de haber sido tapada (o minimizada, resizeada, etc.). Cuando es necesario que redibujemos una ventana, se produce un evento, que típicamente atendemos en una función OnPaint(). El framework o el sistema de ventanas sabe redibujar controles, pero no sabe redibujar cosas especiales, como nuestros propios dibujos.

En simple6.cpp agregamos sizers y nos hacemos un "canvas" que atienda el evento OnPaint. El problema es que ahora nos queda algo fijo... es muy aburrido.

Queremos dibujar por voluntad propia (no siempre hace falta). Por ejemplo, nos conseguimos un evento "dibujar" con un botón. Le agregamos un botón al panel para controles, y en respuesta a ese evento dibujamos algo (nuevo cada vez). En este caso simplemente puntos al azar. Otro problema que tenemos que resolver es... el OnPaint tiene que redibujar cada vez que se invalida la ventana, por eso necesitamos alguna manera de "recordar" qué es lo que hay que dibujar. Una posibilidad, entre otras, se ve en simple7.cpp, junto con el botón agregado y su sizer correspondiente.

(Suerte!)