Capítulo 4: Soy un artista... ¡No me coartes!

De doc.ubuntu-es
Saltar a: navegación, buscar

Contenido

Introducción

En este capítulo no vamos a introducir nada nuevo, tan sólo vamos a trabajar con dos elementos un tanto singulares, ambos dedicados a desarrollarnos como artistas, pues el primero son los sizers, que nos ayudarán enormemente a organizar nuestras ventanas, y el segundo es wxWindow, que serán las ventanas donde pintaremos.

Parecerá chocante la forma de moverse de wxWidgets en cuanto a nomenclatura, pues las wxWindow serán para nosotros lo que para el resto de la humanidad son las Canvas, wxFrame, como ya vimos en capítulos anteriores, no serán pantallas (para pintar por ejemplo), sino ventanas de cabecera. El único que mantiene una cierta línea clásica es wxDialog, que aunque no llegaremos a usarlo, os podeis imaginar que su uso será del todo análogo.

Por tanto presentemos el plan de trabajo para este capítulo:

  • Plan de trabajo del capítulo 4:
    • Creación de una wxWindow:
      Crearemos nuestra primera ventana para pintar, dentro del propio TopFrame.
    • Organización de los elementos en la ventana (sizers):
      Usando los sizers colocaremos a nuestro gusto todos los elementos que tenemos.
      • Creación de algunos elementos basura:
        Crearemos unos cuantos cuadros de texto y labels para tener mas objetos que poder colocar.
      • Colocación de los elementos.
    • Pintado de la wxWindow:
      • Pintado directo (wxPaintDC).
      • Pintado sobre un bitmap (wxMemoryDC).
      • Pintado con contexto gráfico. (de momento queda pendiente)

El último punto es realmente interesante, y una de las más modernas implementaciones de estas magníficas librerías. No obstante esta herramienta nos costará algunos disgustos cuando compilemos en Windows, lo primero porque en Windows tienes que añadir estas librerías manualmente, y lo segundo porque sufren de una alta inestabilidad.

Hecha esta breve introducción, procedamos a trabajar un poco...

Creación de una wxWindow:

Como siempre, con nuestra API bien cerquita, acudimos a ella y buscamos wxWindow, y nos informamos de todas las posibilidades que tiene (que son muchas). Para proceder con una wxWindow, al igual que con una wxFrame, y con un wxDialog, nos crearemos una clase heredada de wxWindow.

Ya introducimos esta idea cuando creamos nuestra wxFrame, y ya adelantabamos que cuando nos encontráramos con elementos complicados (una ventana, un dialogo, un canvas, una malla...) debíamos actuar de esta forma.

Bueno, ya que hemos alineado un poco nuestras ideas, recuperamos nuestro proyecto donde lo dejamos, y creamos dos archivos, llamados "mycanvas.cpp" y "mycanvas.h", y como siempre los limpiamos.

Y declaramos nuestra nueva clase, en "mycanvas.h":

 class MyCanvas : public wxWindow
 {
 public:
 	MyCanvas(SampleApp * AApp, wxWindow *parent, wxWindowID id, const wxPoint& 
       pos=wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0);
 
 	void drawSomething();
     wxMemoryDC dc;
     wxGraphicsContext *gc;
 
 protected: // event handlers (NO virtual)
 	void OnPaint(wxPaintEvent& event);
 	void OnMaximize(wxSizeEvent& event);
 private:
 	wxBitmap m_pixels;
 	SampleApp * App;
 
 	// any class wishing to process wxWidgets events must use this macro
 	DECLARE_EVENT_TABLE()
 };

En un acto de previsión, hemos ido añadiendo todo aquello que creemos que vamos a necesitar, tal vez sería buena idea que buscarais en el API todos los elementos que hemos añadido.

El siguiente punto es crear nuestra clase, en "mycanvas.cpp":

 #include <wx/wx.h>
 #include <wx/icon.h>
 #include <wx/font.h>
 #include <wx/numdlg.h>
 #include <wx/string.h>
 #include <wx/event.h>
 #include <wx/textctrl.h>
 #include <wx/wfstream.h>
 #include <wx/datstrm.h>
 #include <wx/txtstrm.h>
 #include <wx/access.h>
 #include <wx/window.h>
 #include <wx/treectrl.h>
 #include <wx/spinctrl.h>
 #include <wx/collpane.h>
 #include <wx/stdpaths.h>
 #include <wx/datetime.h>
 #include <wx/graphics.h>
 #include "main.h"
 #include "mycanvas.h"
 
 BEGIN_EVENT_TABLE(MyCanvas, wxWindow)
 	EVT_PAINT(MyCanvas::OnPaint)			//to can paint in window
 	EVT_SIZE(MyCanvas::OnMaximize)			//answer to size changes (p.ej. maximize events)	
 END_EVENT_TABLE()
 
 const int BMPW = 3000;	//auxvars
 const int BMPH = 2000;	//auxvars
 
 MyCanvas::MyCanvas(SampleApp * AApp, wxWindow *parent, wxWindowID id, const wxPoint& 
  pos /*= wxDefaultPosition*/, const wxSize& size /*= wxDefaultSize*/, long style /*= 0*/)
 :wxWindow(parent,id,pos,size,style),
 m_pixels(BMPW,BMPH,-1)
 {
     App = AApp;
 }
 
 /***************************************************************/
 /**************	OnMaximize	************************************/
 /************** answer to Size changes	************************/
 /************** Used to resize all elements	********************/
 /***************************************************************/
 void MyCanvas::OnMaximize(wxSizeEvent& WXUNUSED(event))
 {
 }
 
 /***************************************************************/
 /**************	OnPaint	****************************************/
 /************** answer to paint events *************************/
 /************** Prepares the window to paint it ****************/
 /***************************************************************/
 void MyCanvas::OnPaint(wxPaintEvent& WXUNUSED(event))
 {
 }
 
 /***************************************************************/
 /**************	drawSomething	********************************/
 /************** Paint the canvas	****************************/
 /***************************************************************/
 void MyCanvas::drawSomething()
 {
 }

Paremos un segundo a explicar a que viene cada cosa.

  1. De repente añado un montón de librerías, que ni siquiera nos son necesarias... Esas librerías son las típicas que casi seguro tarde o temprano terminareis por usar, es por eso que las añado.
  2. ¿Un sizeevent?... Como maximizamos nuestra TopFrame, es previsible que el tamaño de esta ventana cambie junto a el, On Maximize será el encargado de arreglar los posibles desperfectos.
  3. ¿Dos constantes?... Efectivamente, si miramos esas líneas de código:
 (...)
 const int BMPW = 3000;	//auxvars
 const int BMPH = 2000;	//auxvars
 
 MyCanvas::MyCanvas(SampleApp * AApp, wxWindow *parent, wxWindowID id, const wxPoint& 
  pos /*= wxDefaultPosition*/, const wxSize& size /*= wxDefaultSize*/, long style /*= 0*/)
 :wxWindow(parent,id,pos,size,style),
 m_pixels(BMPW,BMPH,-1)
 (...)

Lo que hacemos es crear un ancho y un alto, y montar con ellos nuestro bitmap (m_pixels). Ya veremos su utilidad, de momento nos bastará con saber que estos valores deben ser superiores a la resolución de pantalla, o por lo menos al tamaño de la ventana.

Respecto a las funciones, pues lo mas interesante es que tenemos un drawSomething(), que será al que llamaremos para pintar, y que será el que cree el evento para que OnPaint haga el trabajo. Esto es un primer esbozo, porque pronto veremos como no trabajaremos exactamente así.

Bueno, y ahora solo resta añadirla a topframe, así que empezamos por "topframe.h":

 (...)
 private:
 	SampleApp * App;
 	MyCanvas * m_drawPanel;
     DECLARE_EVENT_TABLE()
 };

Haber incluido esta clase en la cabecera de topframe, nos obliga a que siempre que se incluya "topframe.h", haya que incluir antes "mycnavas.h", y por otro lado, "mycanvas.h" requiere de "main.h" antes de ella. Así, en "main.cpp" debemos añadir lo siguiente:

 (...)
 #include "main.h"
 #include "mycanvas.h"
 #include "topframe.h"
 (...)

Y añadimos y construimos MyCanvas en "topframe.cpp":

 (...)
 #include "main.h"
 #include "mycanvas.h"
 #include "topframe.h"
 #include "header.h"
 (...)
(...)
 	ProgressBar = new wxGauge(ControlPanel, wxID_ANY, 100, wxPoint(3,3), wxSize(250,32));
 	MyCanvas * drawPanel = new MyCanvas(App,this, wxID_ANY, wxPoint(0,35), wxSize(640 , 480), wxTAB_TRAVERSAL | wxSUNKEN_BORDER);
     m_drawPanel = drawPanel;
 
     //toolbar
(...)

Con lo que creamos nuestra ventana en el punto wxPoint(0,35) con un tamaño de wxSize(640 , 480).

Compilamos y ejecutamos... ¡Y allí está! ¡nuestra ventana!

Pero el aspecto no es precisamente el deseado ¿verdad?, para arreglar esto vamos a recurrir a los sizers.

Organización de los elementos en la ventana (sizers):

Los sizers son una herramienta indispensable en la programación con wxWidgets, y se deben tener siempre presentes a la hora de programar. Nosotros concretamente vamos a usar wxBoxSizer, aunque existen algunas otras variantes que pueden ser muy interesantes. Pero para poder dar un poco más de juego, vamos primero a crear algunos elementos basura.

Creación de algunos elementos basura:

Propongamos poner, en la parte superior la scrollbar con un título (una etiqueta), y debajo, en la izquierda una columna de dos cuadros de texto, precedidos de una label, y en la derecha nuestro recien creado Canvas...

Asi que nos faltan dos etiquetas, y dos cuadros de texto, que por supuesto, para encontrar información sobre ellos, recurrimos a nuestra resabiada API, en la que si buscamos label, podemos encontrar, entre otras cosas, la clase wxStaticText, que es exactamente lo que buscabamos.

Así que vayamos a topframe.cpp, y creemos nuestras dos nuevas etiquetas al final del constructor:

(...)
   toolBar->AddTool(ID_START, wxBITMAP(start), wxNullBitmap, false, wxDefaultCoord, wxDefaultCoord, (wxObject *)NULL, _T("Start"), _T("Run the progress bar"));
   toolBar->AddTool(ID_STOP, wxBITMAP(stop), wxNullBitmap, false, wxDefaultCoord, wxDefaultCoord, (wxObject *)NULL, _T("Start"), _T("Run the progress bar"));
	wxStaticText * aLabel = new wxStaticText(<<DatePanel>>, wxID_ANY, _T("This is a wxGauge"),
                              wxPoint(250, 60), wxDefaultSize,
                              wxALIGN_CENTRE /*| wxST_NO_AUTORESIZE*/);
   aLabel->SetForegroundColour( *wxBLACK );
   
	wxStaticText * bLabel = new wxStaticText(<<DatePanel>>, wxID_ANY, _T("This is 2 wxTextCtrl"),
                              wxPoint(250, 60), wxDefaultSize,
                              wxALIGN_CENTRE /*| wxST_NO_AUTORESIZE*/);
   bLabel->SetForegroundColour( *wxBLACK );
   m_running = false;
(...)

Conviene destacar que les he dado un punto de creación a ambas (250, 60), pero ya vereis como eso no importa... Y es importante reseñar que en el window parent, pongo <<DatePanel>>, esto es porque como aun no hemos creado paneles para introducir estos elementos, para que si se nos olvida, nos devuelva un error.

Ahora volvemos a nuestro API, y buscamos text, encontrando la clase wxTextCtrl, si nos fijamos en su constructor, aparece algo muy interesante, el validador wxValidator (pinchamos sobre el link para conseguir información), que nos va a permitir restringir nuestro segundo cuadro de texto a solo números...

Asi que añadimos sendos cuadros de texto a nuestra interfaz, nuevamente al final de topframe.cpp:

(...)
   bLabel->SetForegroundColour( *wxBLACK );    
	// validator that only accept number keys
	wxTextValidator OnlyNum = wxTextValidator(wxFILTER_NUMERIC);
   // text box that accepts all
	wxTextCtrl * aText = new wxTextCtrl(<<DatePanel>>, wxID_ANY, _T("Any text"), wxDefaultPosition, wxSize(100, 20), wxTE_RIGHT);
   // text box that only take numbers
	wxTextCtrl * bText = new wxTextCtrl(<<DatePanel>>, wxID_ANY, _T("1"), wxDefaultPosition, wxSize(100, 20), wxTE_RIGHT, OnlyNum);
   m_running = false;
(...)

Y ya tenemos nuestros elementos, solo debemos colocarlos...

Colocación de los elementos.

Como ya hemos dicho, nuestros cuadros de texto irán en una columna diferente de nuestra barra de progreso, luego necesitan un panel distinto, así que creemos un panel para ellos:

(...)
   // Panel
	wxPanel * ControlPanel = new wxPanel(this, wxID_ANY, wxPoint(0,0), wxSize(256, 32));
	wxPanel * ControlPanel2 = new wxPanel(this, wxID_ANY, wxPoint(0,0), wxSize(256, 32));
	ProgressBar = new wxGauge(ControlPanel, wxID_ANY, 100, wxPoint(3,3), wxSize(250,32));
(...)

Y ahora ya si, ¡comenzemos con los wxSizers!, como siempre, lo primero la documentación, en la que buscamos sizer, y que rapidamente nos muestra, entre otras cosas, wxBoxSizer. Bien, si leeemos atentamente la documentación, veremos que estos sizers nos permiten colocar los elementos como si fueran cajas Cuyo alto y ancho debe coincidir entre ellas (con sus proporciones), y que nos permite apilarlas horizontal o verticalmente.

Bien, empezemos con nuestro panel para la scrollbar, en el que recordemos, primero va aLabel, y debajo ProgressBar, así que editamos aLabel para que este en el mismo panel que ProgressBar:

(...)
	wxStaticText * aLabel = new wxStaticText(ControlPanel, wxID_ANY, _T("This is a wxGauge"),
                              wxPoint(250, 60), wxDefaultSize,
                              wxALIGN_CENTRE /*| wxST_NO_AUTORESIZE*/);
   aLabel->SetForegroundColour( *wxBLACK );
(...)

Y al final del constructor de topframe creamos un sizer para ellos:

(...)
	wxBoxSizer *cpSizer = new wxBoxSizer(wxVERTICAL);
	cpSizer->Add(aLabel, wxSizerFlags().Proportion(1).Expand().Border(wxALL,4));
	cpSizer->Add(ProgressBar, wxSizerFlags().Proportion(1).Expand().Border(wxALL,4));
	ControlPanel->SetSizer/*AndFit*/(cpSizer);
(...)

Y repetimos para nuestra columna de cuadros de texto, los cuales editamos para que se incluyan en el ControlPanel2:

(...)
	wxStaticText * bLabel = new wxStaticText(ControlPanel2, wxID_ANY, _T("This is 2 wxTextCtrl"),
                              wxPoint(250, 60), wxDefaultSize,
                              wxALIGN_CENTRE /*| wxST_NO_AUTORESIZE*/);
    bLabel->SetForegroundColour( *wxBLACK );    
	// validator that only accept number keys
	wxTextValidator OnlyNum = wxTextValidator(wxFILTER_NUMERIC); 
    // text box that accepts all
	wxTextCtrl * aText = new wxTextCtrl(ControlPanel2, wxID_ANY, _T("Any text"), wxDefaultPosition, wxSize(100, 20), wxTE_RIGHT);
    // text box that only take numbers
	wxTextCtrl * bText = new wxTextCtrl(ControlPanel2, wxID_ANY, _T("1"), wxDefaultPosition, wxSize(100, 20), wxTE_RIGHT, OnlyNum);
(...)

Y creamos el sizer para este panel:

(...)
	wxBoxSizer *cpSizer2 = new wxBoxSizer(wxVERTICAL);
	cpSizer2->Add(bLabel, wxSizerFlags().Proportion(0).Expand().Border(wxALL,4));
	cpSizer2->Add(aText, wxSizerFlags().Proportion(0).Expand().Border(wxALL,4));
	cpSizer2->Add(bText, wxSizerFlags().Proportion(0).Expand().Border(wxALL,4));
	ControlPanel2->SetSizer/*AndFit*/(cpSizer2);
(...)

Bien, habíamos propuesto que esta columna de cuadros de texto estuvíera emparejada a nuestro canvas, de tal foma que creamos un sizer horizontal (justo a continuación):

(...)
	wxBoxSizer *auxSizer = new wxBoxSizer(wxHORIZONTAL);
	auxSizer->Add(ControlPanel2, wxSizerFlags().Proportion(0).Expand().Border(wxALL,4));
	auxSizer->Add(drawPanel, wxSizerFlags().Proportion(1).Expand().Border(wxALL,4));
(...)

Y por último, esta pareja debe situarse debajo de la progress bar, de tal forma que creamos ya nuestro sizer definitivo, con el que setearemos ya nuestra ventana:

(...)
	wxBoxSizer * topSizer = new wxBoxSizer(wxVERTICAL);
	topSizer->Add(ControlPanel, wxSizerFlags().Proportion(0).Expand());
	topSizer->Add(auxSizer, wxSizerFlags().Proportion(1).Expand());
	SetSizerAndFit(topSizer);
(...)

Es muy importante el tema de las proporciones... Cuando se pone proporción 0, se ajusta al tamaño del elemento, haciéndose tan pequeño como pueda, pero cuando se pone proporción 1, el elemento es el que se ajusta, haciéndose tan grande como pueda, así, si atendemos a nuestros sizers, yo le he puesto proporción 1 al canvas, primero en el modo horizontal en auxSizer, y luego en el modo vertical en topSizer.

Compilar y ejecutar para ver el aspecto... ¡Exito rotundo!

Pintado de la wxWindow:

Bueno, pues colocados todos los elementos, ya sólo nos queda pintar en nuestra wxWindow. pongamos que pintamos una linea vertical cada vez que se actualiza (en el bucle de la progress bar por ejemplo)...

Pintado directo (wxPaintDC).

Por supuesto, nada más ver el título, hay que tener ya nuestra documentación en las narices con wxPaintDC abierto. Si investigamos un poco, veremos que hay una gran variedad de formas de pintar en el canvas, pero que destacan dos:

  • wxPaintDC
    Es la forma nativa, y tarde o temprano hay que usarla, ya que es la que nos pinta realmente sobre la pantalla. No obstante es la forma más primitiva de hacerlo, y tiene el problema de que se ve como va evolucionando el pintado, de tal forma que si el pintado requiere un alto número de operaciones, se verá como se va realizando, con un molesto parpadeo (efecto flicker).
  • wxMemoryDC
    Precisamente para evitar este problema, se usa esta clase, que basicamente nos permitirá ir pintando sobre un mapa de bits, y luego pintar este mapa todo de golpe sobre el canvas usando el wxPaintDC.

Así que efectivamente, la clase que nos ocupa unicamente se encargará de pintar el bitmap, así pues, en el metodo OnPaint de mycanvas.cpp añadimos:

(...)
void MyCanvas::OnPaint(wxPaintEvent& WXUNUSED(event))
{
   	wxPaintDC Pdc(this);
	Pdc.DrawBitmap(m_pixels, 0, 0, false);
}
(...)

Que efecitamente, cada vez que llamemos a Update() o a Refresh() (o a ambas si queremos forzar el pintado inmediato), se entrará en este método en el que Pdc pintará el bitmap m_pixels.

Pintado sobre un bitmap (wxMemoryDC).

Este va a ser el que realmente tenga interés, ya que realmente será el que haga todo el trabajo... En este caso acudiremos al método drawSomething(). Pero antes necesitamos dos variables contadoras que nos permitan colocar nuestros palitos horizontal y verticalmente, así que en mycanvas.h añadimos dos contadores:

(...)
public:
	MyCanvas(SampleApp * AApp, wxWindow *parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long  style = 0);
	void drawSomething();
    wxMemoryDC dc;
    wxGraphicsContext *gc;
    int countx, county;
(...)

Y en el constructor de mycanvas, en mycanvas.cpp, iniciamos los contadores:

MyCanvas::MyCanvas(SampleApp * AApp, wxWindow *parent, wxWindowID id, const wxPoint& pos /*= wxDefaultPosition*/, const wxSize& size /*= wxDefaultSize*/, long style /*= 0*/)
:wxWindow(parent,id,pos,size,style),
m_pixels(BMPW,BMPH,-1)
{
    App = AApp;
    countx = 0;
    county = 0;
}

Y ya podemos pasar a pintar, para ello, en este mismo archivo, en el método drawSomething, creamos nuestro pintor:

(...)
void MyCanvas::drawSomething()
{
    wxMemoryDC dc;        
}
(...)

Y como tenemos la documentación, sabemos que lo primero que debemos hacer es seleccionar el bitmap y limpiarlo (el fondo por defecto es blanco, pero se puede cambiar):

(...)
void MyCanvas::drawSomething()
{
    wxMemoryDC dc;        
    dc.SelectObject( m_pixels );
    dc.Clear();
}
(...)

Bien, y ahora pintamos un palote de 5 unidades de alto. Este palote se deberá situar en x=countx, y en y=county*5, luego veremos como nos movemos con countx y county para que hagan los "retornos de carro" al llegar al final de la ventana.

(...)
void MyCanvas::drawSomething()
{
    wxMemoryDC dc;        
    dc.SelectObject( m_pixels );
    dc.Clear();
    dc.SetPen(wxPen(wxColour(0,0,0)));  //black pen
    dc.DrawLine(countx, county*5, countx, county*5 + 5);
}
(...)

Ahora gestionemos countx y county. Countx debe incementarse una unidad cada vez que se llame a drawSomething, y si en algun momento se supera el borde de la wxWindow, deberá volver a ser cero, incrementándose en una unidad county:

(...)
void MyCanvas::drawSomething()
{
    wxMemoryDC dc;        
    dc.SelectObject( m_pixels );
    dc.Clear();
    dc.SetPen(wxPen(wxColour(0,0,0)));  //black pen
    dc.DrawLine(countx, county*5, countx, county*5 + 5);
    int sizex,sizey;
    GetSize(&sizex, &sizey);	//take sizes to scale some elements
    countx++;
    if(countx>sizex)
    {
        countx = 0;
        county++;
    }
}
(...)

Y por último debemos pedirle a nuestro drawSomething que solicite el pintado:

(...)
void MyCanvas::drawSomething()
{
    wxMemoryDC dc;        
    dc.SelectObject( m_pixels );
    dc.Clear();
    dc.SetPen(wxPen(wxColour(0,0,0)));  //black pen
    dc.DrawLine(countx, county*5, countx, county*5 + 5);
    int sizex,sizey;
    GetSize(&sizex, &sizey);	//take sizes to scale some elements
    countx++;
    if(countx>sizex)
    {
        countx = 0;
        county++;
    }
    dc.SelectObject( wxNullBitmap );					//unselect the bitmap
    Refresh(false);							//draw everything
    Update();								//force to paint
}
(...)

Y ya está, ahora sólo hay que pedirle al programa que use este método, y para ello vamos a topframe.cpp, y editamos:

void TopFrame::OnStart(wxCommandEvent& WXUNUSED(event))
{
    int i;
    bool token_true;
    m_running = true;
    while(m_running)
    {
        for(i=1;i<=100;i++)
        {
            ProgressBar->SetValue(i);
            while((TopFrame *) App->Pending())
            {
                token_true=(TopFrame *) App->Dispatch();
            }
            m_drawPanel->drawSomething();
        }
        for(i=99;i>=2;i--)
        {
            ProgressBar->SetValue(i);
            while((TopFrame *) App->Pending())
            {
                token_true=(TopFrame *) App->Dispatch();
            }
            m_drawPanel->drawSomething();
        }
    }
}

Compilamos, ejecutamos y damos a start. ¡Esto ya si que empieza a ser un programa!

Capítulo 3: ¡Qué aburrido! yo quiero interactuar Nuestra primera interfaz gráfica con CodeBlocks y wxWidgets Capítulo 5: El lado oscuro (compilando para Windows)
Herramientas personales