8 Controles de lista

wxWidgets dispone de varios controles de tipo lista.
Un control de lista permite al usuario elegir una o varias entre un grupo opciones posibles y limitadas. Esas opciones se muestran en forma de lista, con una o más columnas. En principio, estos controles no permiten editar ni suprimir opciones de la lista de forma directa, es decir, cada opción es de sólo lectura.
Control ListBox
La forma más simple se encapsula en la clase wxListBox y consiste en una lista de cadenas entre las que podremos marcar una o varias. Por defecto un listbox sólo permite seleccionar una opción, pero podemos hacer que permita varias añadiendo el estilo wxLB_MULTIPLE, de modo que el usuario podrá seleccionar y deseleccionar varias opciones. Sólo en los listbox de selección múltiple el usuario podrá dejar sin marcar todas las opciones.
En los de selección sencilla el usuario deberá marcar siempre una opción, y no podrá dejar todas desmarcadas. Por eso es buena idea marcar una opción por defecto, o añadir una opción "ninguna" si nuestra aplicación puede permitir no asignar un valor para ese dato.
Para añadir las opciones en el control podemos usar uno de los constructores con parámetros, o el equivalente que consiste en usar el constructor por defecto, y después llamar a uno de los métodos Create.
El constructor para estos controles requiere un puntero a la ventana padre, un identificador, la posición y tamaño, el número de elementos, un array con las cadenas con las etiquetas de cada uno de los elementos, estilos, un validador y una cadena con el nombre del control. Existe un segundo constructor que en lugar de los parámetros quinto y sexto sólo usa un parámetro de tipo wxArrayString.
wxString cads[] = { _T("Rojo"), _T("Naranja"), _T("Amarillo"), _T("Verde"), _T("Azul"), _T("Añil"), _T("Violeta") }; m_listBox = new wxListBox(this, idListBox, wxDefaultPosition, wxSize(200, wxDefaultCoord), sizeof(cads)/sizeof(cads[0]), cads, 0, lbValidator(&data.opcion)); m_listBox->Append(_T("Negro"));
También podemos crear el control sin añadir opciones, e insertarlas posteriormente.
Para ello podemos usar cualquiera de los métodos Append de la clase base wxItemContainer para añadir opciones al final de la lista, o cualquiera de los métodos Insert para insertarlos en la posición que se requiera. La clase wxListBox también dispone de métodos InsertItems para insertar opciones.
Insertar opciones sólo tiene sentido si queremos establecer un orden concreto. Pero si queremos que ese orden sea alfabético es más sencillo usar el estilo wxLB_SORT, de modo que las opciones siempre se mostrarán ordenadas, independientemente de la posición en que se inserten o del orden en que se añadan.
Validadores
En estos controles el validador por defecto tampoco nos sirve de ayuda, de modo que podemos crear nuestros propios validadores personalizados. El más simple marcará la selección o selecciones iniciales y recuperará las opciones seleccionadas al salir.
Para establecer valores y recuperarlos también disponemos de varias posibilidades:
- Podemos trabajar con las etiquetas de texto de cada opción.
- Usar los índices de las opciones, que corresponden a la posición dentro del control.
- Usar los datos asociados a cada una de las opciones.
Por ejemplo, un validador para trabajar con las etiquetas podría tener esta forma:
class lbValidator : public wxValidator { public: lbValidator(wxString *v = nullptr); virtual wxObject* Clone() const { return new lbValidator(*this); } virtual bool TransferFromWindow(); virtual bool TransferToWindow(); virtual bool Validate(wxWindow * parent); private: wxString* m_valor; DECLARE_DYNAMIC_CLASS( lbValidator ) DECLARE_EVENT_TABLE() };
El valor es un puntero a un objeto wxString.
IMPLEMENT_DYNAMIC_CLASS( lbValidator, wxValidator ) BEGIN_EVENT_TABLE( lbValidator, wxValidator ) END_EVENT_TABLE() lbValidator::lbValidator(wxString *v) : wxValidator(), m_valor(v) {} bool lbValidator::TransferFromWindow() { try { wxListBox *lb = dynamic_cast<wxListBox*>(m_validatorWindow); int s = lb->GetSelection(); if(s != wxNOT_FOUND) *m_valor = lb->GetString(s); else *m_valor = ""; } catch (...) { wxFAIL_MSG( _T("lbValidator sólo funciona con wxListBox")); return false; } return true; } bool lbValidator::TransferToWindow() { try { wxListBox *lb = dynamic_cast<wxListBox*>(m_validatorWindow); lb->SetStringSelection(*m_valor); } catch(...) { wxFAIL_MSG( _T("lbValidator sólo funciona con wxListBox")); return false; } return true; } bool lbValidator::Validate(wxWindow* parent) { return true; }
Datos de cliente
Usar etiquetas tiene algunas desventajas, por ejemplo, que la aplicación no funcionará bien en diferentes idiomas, es decir, los datos almacenados en una versión de la aplicación en un idioma no podrán usarse, en principio, en una versión para un idioma diferente. Por otra parte, las cadenas de un listbox a menudo son sólo etiquetas que hacen referencia a objetos con más información, y esas etiquetas posiblemente no son la mejor manera de acceder a esa información.
Por ejemplo, un listbox nos puede servir para seleccionar un registro de una base de datos, y cad registro puede tener una clave, un nombre, y varios datos más. En el listbox podemos usar los nombres, junto con otros datos, como etiquetas, pero probablemente sea más útil acceder al dato por su clave, que después podremos usar para obtener el dato completo desde la base de datos. La clave identifica correctamente un registro, pero en nombre no tiene por qué es único ni constante.
Trabajar con los índices tampoco suele ser una buena opción, ya que se corresponden con la posición dentro del control, y esas posiciones pueden cambiar si se añaden o se eliminan opciones, o incluso si se modifica el orden, de modo que no están necesariamente relacionados con el dato, y están ligados al control.
La mejor opción en casi todos los casos, en mi opinión, es la tercera. La clase wxItemContainer de la que se deriva wxListBox, permite asociar un dato, denominado dato de cliente, a cada opción. Ese dato puede ser un puntero genérico (void*) o un puntero a un objeto wxClientData.
Nota: como norma general, para los casos en que los datos a seleccionar se referencien mediante los datos de cliente, es importante que esos datos sean únicos, es decir, que no haya dos opciones del listbox con el mismo dato de cliente. Si hubiese dos o más opciones con los mismos datos de cliente sería imposible diferenciarlas a la hora de marcar selecciones iniciales o recuperar los valores de las opciones seleccionadas.
De nuevo, suele ser mejor opción usar un puntero a un objeto wxClientData, ya que al asignar a una opción uno de esos punteros pasa a ser de su propiedad, y se destruirá automáticamente al destruir el ítem de la opción o al destruir el control, cosa que no ocurre con el objeto apuntado por (void*), que debe ser destruido por la aplicación.
Además, estos datos de cliente nos serán muy útiles en controles más complejos que veremos en más adelante, así que conviene aprender a manejarlos.
La clase wxClientData es en realidad un clase base que no contiene datos. Nuestros datos de cliente pertenecerán a una clase propia derivada de ella.
Todo esto quedará mucho más claro, espero, con un ejemplo.
Partamos del ejemplo anterior, donde el usuario debe elegir un color. A cada color le corresponde un valor RGB, que podemos codificar en un entero sin signo de 32 bits. Así que para cada color tenemos una cadena con su nombre, y un valor entero con su valor RGB:
struct tipoOpcion { wxString nombre; UINT32 rgb; };
Almacenaremos los colores en un vector:
std::vector<tipoOpcion> opciones = { {_T("Rojo"), 0xff0000}, {_T("Naranja"), 0xff8000}, {_T("Amarillo"), 0xffff00}, {_T("Verde"), 0x00ff00}, {_T("Azul"), 0x0000ff}, {_T("Añil"), 0x00ffff}, {_T("Violeta"), 0xff00ff} };
Nuestra clase para datos de cliente sólo tiene que almacenar un valor UINT32, y sobrecargar el constructor y los métodos para asignar y recuperar el valor.
class colorClientData : public wxClientData { public: colorClientData(UINT32 v=0) : m_color(v) {} UINT32 GetData() const { return m_color; } void SetData(UINT32 v) { m_color=v; } private: UINT32 m_color; };
Luego nos ocuparemos del validador, que ahora tiene que manejar un valor UINT32.
Para crear el listbox usaremos esta expresión, por ejemplo, que en principio estará vacío:
m_listBox = new wxListBox(this, idListBox, wxDefaultPosition, wxSize(200, wxDefaultCoord), 0, nullptr, wxLB_SORT, cdValidator(&data.opcion));
Ahora es más difícil usar uno de los constructores que inician la lista de opciones, ya que nuestras cadenas no se almacenan solas en un array. De modo que las añadiremos una a una:
for(size_t i=0; i<opciones.size(); i++) { m_listBox->Append(opciones[i].nombre, new colorClientData(opciones[i].rgb)); }
El método Append tiene diez sobrecargas, en este caso hemos usado la que añade una opción y le asigna unos datos de cliente. De este modo a cada elemento del listbox le corresponde una etiqueta con el nombre del color y unos datos de cliente con su valor RGB.
Veamos ahora nuestra clase validadora, que se encargará de transferir datos a y desde el listbox:
class cdValidator : public wxValidator { public: cdValidator(UINT32 *v = nullptr) : wxValidator(), m_valor(v) {} virtual wxObject* Clone() const { return new cdValidator(*this); } virtual bool TransferFromWindow(); virtual bool TransferToWindow(); virtual bool Validate(wxWindow * parent); private: UINT32* m_valor; DECLARE_DYNAMIC_CLASS( cdValidator ) DECLARE_EVENT_TABLE() };
La implementación tiene esta forma:
IMPLEMENT_DYNAMIC_CLASS( cdValidator, wxValidator ) BEGIN_EVENT_TABLE( cdValidator, wxValidator ) END_EVENT_TABLE() bool cdValidator::TransferFromWindow() { try { wxListBox *lb = dynamic_cast<wxListBox*>(m_validatorWindow); int s = lb->GetSelection(); if(s != wxNOT_FOUND) { colorClientData* ccd = static_cast<colorClientData*>(lb->GetClientObject(s)); *m_valor = ccd->GetData(); } else *m_valor = 0; } catch (...) { wxFAIL_MSG( _T("cdValidator sólo funciona con wxListBox con datos de cliente")); return false; } return true; }
Para recuperar el valor seleccionado del listbox obtenemos un puntero al listbox, y a continuación el índice de la opción seleccionada en s.
Si el valor de s es válido, obtendremos un puntero a los datos de cliente asociados a ese índice en ccd, usando el método GetClientObject().
Finalmente, asignamos al valor de retorno el dato asociado a nuestros datos de cliente, usando el método GetData() de nuestra clase colorClientData.
bool cdValidator::TransferToWindow() { try { size_t i; wxListBox *lb = dynamic_cast<wxListBox*/gt;(m_validatorWindow); colorClientData* ccd; for(i=0; i < lb->GetCount(); i++) { ccd = static_cast<colorClientData*>(lb->GetClientObject(i)); UINT32 v = ccd->GetData(); if(v == *m_valor) break; } if(i < lb->GetCount()) lb->SetSelection(i); } catch(...) { wxFAIL_MSG( _T("cdValidator sólo funciona con wxListBox con datos de cliente")); return false; } return true; }
Para seleccionar la opción inicial, como nuestra clase validadora no tiene acceso al vector con las opciones, optaremos por recorrer las opciones del control hasta que el dato de cliente asociado sea igual al que tenemos que activar. Este sistema puede no ser muy aconsejable para listboxes con muchas opciones, pero para este ejemplo es perfectamente válido.
Para cada elemento del listbox obtenemos un puntero a sus datos de cliente, usado GetClientObject, obtenemos el valor del dato y si es igual al valor que queremos seleccionar, abandonamos el bucle y marcamos ese índice como seleccionado, mediante SetSelection().

Selección múltiple
Trabajar con listboxes de selección múltiple nos plantea nuevos problemas. Por una parte tenemos que estar preparados para trabajar con listas de datos, o incluso con listas vacías.
Nota: recordemos que para los casos en que los datos a seleccionar se referencien mediante los datos de cliente, es importante que esos datos sean únicos, es decir, que no haya dos opciones del listbox con el mismo dato de cliente.
La parte fácil es añadir el estilo wxLB_MULTIPLE, eso bastará para que el usuario pueda seleccionar y deseleccionar opciones.
Por lo tanto, necesitaremos un array para almacenar las selecciones, y una nueva clase validadora para iniciar y obtener las selecciones del listbox.
Dedicaremos algún capítulo a tipos definidos por wxWidgets, pero de momento usaremos algunos sin desarrollarlo en profundidad.
wxWidgets dispone de una plantilla de clase wxArray< T >, y de varios tipos predefinidos de array para los tipos fundamentales. Usaremos en este caso wxArrayInt para almacenar los datos de cliente asociados a las opciones seleccionadas:
class datosMultiple { public: datosClientData() {} datosClientData(wxArrayInt o) { opciones = o; } //private: wxArrayInt opciones; };
Podríamos haber usado un array o un vector std, pero las clases de wxWidgets trabajan con sus propias clases para estructuras de datos, y probablemente sea mejor usarlas nosotros también.
De nuevo usaremos una clase validadora propia, pero los algoritmos que usaremos para los métodos TransferToWindow y TransferFromWindow se pueden aplicar directamente al constructor de la ventana que use el control y al método que procese el botón de aceptar. Sin embargo, creo que usar clases validadoras es más general y ayuda a separar el código en fragmentos más manejables.
Nuestro listbox de ejemplo contendrá datos de cliente de tipo int.
class mulValidator : public wxValidator { public: mulValidator(wxArrayInt *v = nullptr) : wxValidator(), m_valores(v) {} virtual wxObject* Clone() const { return new mulValidator(*this); } virtual bool TransferFromWindow(); virtual bool TransferToWindow(); virtual bool Validate(wxWindow * parent); private: wxArrayInt* m_valores; DECLARE_DYNAMIC_CLASS( mulValidator ) DECLARE_EVENT_TABLE() };
Recuperar los datos de cliente asociados a las opciones seleccionadas es relativamente sencillo:
IMPLEMENT_DYNAMIC_CLASS( mulValidator, wxValidator ) BEGIN_EVENT_TABLE( mulValidator, wxValidator ) END_EVENT_TABLE() bool mulValidator::TransferFromWindow() { try { wxArrayInt indices; multipleClientData* ccd; m_valores->Clear(); wxListBox *lb = dynamic_cast<wxListBox*>(m_validatorWindow); lb->GetSelections(indices); for(size_t i=0; i<indices.GetCount(); i++) { ccd = static_cast<multipleClientData*>(lb->GetClientObject(indices[i])); m_valores->Add(ccd->GetData()); } } catch (...) { wxFAIL_MSG( _T("mulValidator sólo funciona con wxListBox de selección múltiple con datos de cliente")); return false; } return true; }
Partimos del array de valores vacío, para lo que usaremos el método wxArray::Clear, que elimina todos los elementos del array.
El método GetSelections añade al array de enteros pasado como parámetro los índices de las opciones seleccionadas. Con esos índices podemos recuperar los datos asociados a cada opción y añadirlos al array con las datos que ha introducido el cliente mediante el método wxArray::Add.
Para establecer las opciones seleccionadas inicialmente usaremos bucles anidados. No es un método muy aconsejable para listboxes con muchas opciones, pero para este caso es perfectamente válido:
bool mulValidator::TransferToWindow() { try { size_t i; wxListBox *lb = dynamic_cast<wxListBox*>(m_validatorWindow); multipleClientData* ccd; for(i=0; i < lb->GetCount(); i++) { ccd = static_cast<multipleClientData*>(lb->GetClientObject(i)); int v = ccd->GetData(); for(size_t j=0; j < m_valores->GetCount(); j++) { if(m_valores->at(j) == v) lb->SetSelection(i); } } } catch(...) { wxFAIL_MSG( _T("mulValidator sólo funciona con wxListBox de selección múltiple con datos de cliente")); return false; } return true; }
La idea es simple, para cada opción del listbox recuperamos su dato de cliente, y después lo buscamos en la lista de valores seleccionados, si aparece, marcamos la opción como seleccionada.
Evidentemente, eso se puede optimizar mucho. Para empezar, una vez localizada una opción como seleccionada, no es necesario terminar el bucle interno. Otra posible optimización es ordenar el array de valores, usando el método Sort, de modo que podamos aplicar una búsqueda binaria, usando una de las sobrecargas del método Index.
En el constructor crearemos el control y añadiremos las opciones y sus datos de cliente como en el caso de listboxes de selección sencilla:
std::vector<tipoOpcionMultiple> opciones = { {_T("Uno"), 1}, {_T("Dos"), 2}, {_T("Tres"), 3}, {_T("Cuatro"), 4}, {_T("Cinco"), 5}, {_T("Seis"), 6}, {_T("Siete"), 7} }; m_listBox = new wxListBox(this, idListBox, wxDefaultPosition, wxSize(200, wxDefaultCoord), 0, nullptr, wxLB_MULTIPLE | wxLB_SORT, mulValidator(&data.opciones)); for(size_t i=0; i<opciones.size(); i++) { m_listBox->Append(opciones[i].nombre, new multipleClientData(opciones[i].clave));

Control de elección
wxWidgets nos proporciona otra clase para hacer una selección simple de una lista. Se trata de wxChoice. La diferencia es que estos controles normalmente sólo muestran la opción seleccionada, pero el usuario puede desplegar la lista para mostrar las opciones haciendo click sobre el control o sobre el botón adosado al lado derecho.
Estos controles son útiles si queremos usar menos espacio en la pantalla, pero por lo demás se comportan igual que un wxListBox de selección sencilla.
Como en el caso del wxListBox, esta clase deriva de la clase wxItemContainer, por lo que puede hacer uso de sus métodos para acceder a cada una de las opciones.
En cuanto a estilos, sólo dispone del estilo wxCB_SORT, que ordena las opciones alfabéticamente.
Esta clase tiene varios métodos similares a los de la clase wxListBox, con los mismos nombres: como wxChoice#FindString">FindString, para localizar el elemento con la etiqueta indicada, wxChoice#GetCount">GetCount que devuelve el número de elementos en el control, wxChoice#GetSelection">GetSelection que devuelve el índice del elemento seleccionado, wxChoice#GetString">GetString que devuelve la etiqueta asociada al índice dado, wxChoice#IsSorted">IsSorted que devuelve true si los elementos están ordenados, wxChoice#SetSelection">SetSelection que selecciona el elemento con el índice dado y wxChoice#SetString">SetString que asigna una nueva etiqueta al elemento dado.
Además, dispone del método wxChoice#SetColumns">SetColumns, que permite indicar el número de columnas en las que se dividirán los elementos, aunque esta características no está disponible para todas las plataformas, por ejemplo, para Windows no lo está.
En cuanto a los validadores, se pueden utilizar los mismos que para los listboxes de selección sencilla, y las recomendaciones a la hora de usar datos d cliente son las mismas.
Ejemplo 8
Nombre | Fichero | Fecha | Tamaño | Contador | Descarga |
---|---|---|---|---|---|
Ejemplo 8 | wx008.zip | 2024-12-02 | 11451 bytes | 6 |