1 Menús

Cualquier aplicación GUI hará uso de diferentes ventanas con características y apariencias diferentes y con finalidades específicas.

Ciertas ventanas están destinadas a enviar comandos a la aplicación. Entendemos como comandos las diferentes órdenes que desencadenan procesos en la aplicación. Por ejemplo, el comando "cerrar", indicará a la aplicación que el usuario quiere terminar la tarea, y la aplicación llevará acabo las acciones necesarias para guardar los datos que sea necesario, destruir los objetos que esté usando o incluso evitar el cierre si se considera necesario llevar a cabo otras acciones antes de cerrar.

Un comando puede provenir de un botón o de un menú. Cada vez que el usuario interaccione con un botón o menú se generará un evento que será enviado a la aplicación. La aplicación puede procesar o ignorar estos eventos. Muchos de esos eventos no necesitarán ningún procesamiento, y el sistema se encargará de ellos.

En este capítulo veremos como crear menús y como responder a la activación de sus opciones.

Los menús solo se pueden usar en aplicaciones basadas en marco (frame), de modo que empezaremos por crear un proyecto basado en marco. De momento no usaremos ningún editor GUI. Más adelante veremos que estos editores nos ayudarán a crear nuestros menús, pero de momento creo que será más educativo ver como crearlos manualmente.

Los marcos se utilizan principalmente en aplicaciones que requieran crear subventanas, como puede ser un editor de textos, o un IDE, aunque también suelen usarse si necesitamos usar el área de cliente, (que es la zona de la ventana no ocupada por bordes, menú, barra de título, etc) para mostrar información o cualquier salida gráfica.

Estructura de aplicación basada en marco

Pero antes, veamos la estructura de un proyecto basado en marco que crea la plantilla.

Vemos que se crean dos clases, cada una de ellas con sus ficheros de declaración (.h) y de definición (.cpp).

Clase App

Toda aplicación wxWidgets se basa en una clase derivada de wxApp. La declaración básica es muy simple:

#ifndef WX001APP_H
#define WX001APP_H

#include <wx/app.h>

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

#endif // WX001APP_H

Esto declara una clase wx001App derivada de wxApp y declara el método virtual OnInit, que es el que contiene, oculta, la función main, o en el caso de Windows, WinMain.

La definición básica también es sencilla:

#include "wx001App.h"
#include "wx001Main.h"

IMPLEMENT_APP(wx001App);

bool wx001App::OnInit()
{
    wx001Frame* frame = new wx001Frame(0L, _("wxWidgets Application Template"));
    frame->SetIcon(wxICON(aaaa)); // To Set App Icon
    frame->Show();
    
    return true;
}

En este caso se declara una clase wx001Frame derivada de wxFrame.

Lo primero que vemos es una llamada a la macro IMPLEMENT_APP que indica al compilador que seleccione el punto de entrada de la aplicación adecuado según el sistema operativo y, mediante su parámetro, qué clase de aplicación usar que en este caso es la clase derivada de wxApp.

A continuación está la implementación del método virtual OnInit. Este método crea un marco (cuya declaración veremos más abajo), le asigna un icono y lo muestra en pantalla.

Nota: El método SetIcon es heredado por wxFrame de la clase wxTopLevelWindow, y el método Show de la clase wxWindow.

Normalmente no tendremos que hacer nada más con la clase de la aplicación, y la mayor parte de la funcionalidad estará en la clase del marco, que será la ventana principal de nuestra aplicación..

Tablas de eventos

Cada ventana tiene asociada una tabla de eventos que define a qué eventos debe responder y cómo hacerlo.

Hay dos maneras de crear una tabla de eventos.

La primera forma, que es la más tradicional, consiste en crear esa tabla en fase de compilación, esto crea lo que llamamos una tabla estática.

Para declararla se usa la macro DECLARE_EVENT_TABLE, que como su nombre indica, declara una tabla de eventos.

Una tabla de eventos contiene una lista de eventos a los que la ventana debe responder, cada uno de ellos emparejado con un método que será invocado para procesarlo, y forma parte de la definición de la clase. Se usa la pareja de macros BEGIN_EVENT_TABLE y END_EVENT_TABLE. Y en nuestro ejemplo tiene esta forma:

BEGIN_EVENT_TABLE(wx001Frame, wxFrame)
    EVT_CLOSE(wx001Frame::OnClose)
    EVT_MENU(idMenuQuit, wx001Frame::OnQuit)
    EVT_MENU(idMenuAbout, wx001Frame::OnAbout)
END_EVENT_TABLE()

La segunda forma consiste en asignar la correspondencia entre evento y procedimiento en fase de ejecución, lo que crea tablas de eventos dinámicas. Para ello se usa el método Connect de la clase wxEvtHandler, que es una de las clases base de wxFrame, y de todas las ventanas y controles.

El método Connect tiene varias sobrecargas, pero ahora nos interesa la segunda.

Mediante esta función miembro podemos asignar un método a un evento determinado de la ventana o control con un identificador concreto.

Por ejemplo, para asignar el método OnQuit al evento de menú del ítem con el identificador idMenuQuit usaremos:

Connect(idMenuQuit, wxEVT_MENU, (wxObjectEventFunction)&wx001Frame::OnQuit);

De forma simétrica podemos retirar eventos de la tabla dinámicamente, usando el método Disconnect.

Disconnect(idMenuQuit, wxEVT_MENU);

Clase Frame

Las aplicaciones basadas en marco crean otra clase derivada de wxFrame:

#ifndef WX001MAIN_H
#define WX001MAIN_H

#include "wx001App.h"

class wx001Frame: public wxFrame
{
    public:
        wx001Frame(wxFrame *frame, const wxString& title);
        ~wx001Frame();
    private:
        enum
        {
            idMenuQuit = 1000,
            idMenuAbout
        };
        void OnClose(wxCloseEvent& event);
        void OnQuit(wxCommandEvent& event);
        void OnAbout(wxCommandEvent& event);
        DECLARE_EVENT_TABLE()
};

#endif // WX001MAIN_H

En la forma más simple de esta clase se declaran un constructor y un destructor, que se encargarán de las tareas de inicialización y limpieza del marco.

Vemos que se declara un enumerado anónimo que contiene identificadores. Estos identificadores serán los que posteriormente se usarán, en este caso, para las opciones de menú. La ventaja de usar un enumerado es que solo necesitamos asignar un valor al primer identificador y el resto tomarán valores consecutivos. Podríamos usar también macros para definir los valores de cada identificador, pero en C++ es más seguro usar un tipo enumerado.

Nota: En versiones anteriores de wxWidgets se podía usar la función wxNewId(), pero en las versiones actuales se considera obsoleta y no debería usarse. Es preferible usar constantes o enumerados.

A continuación se declaran tres métodos con identificadores que comienzan con 'On'. La convención es que los métodos que responden a eventos tengan identificadores que empiezan con esta palabra. Esto ayuda a leer e interpretar qué hace el código (en inglés). Por ejemplo, 'OnClose' nos dice qué hacer cuando recibamos el evento de petición del usuario de cerrar la ventana o marco, correspondiente al evento que se genera cuando se pulsa sobre el aspa o cruz en la parte superior derecha de la mayoría de las ventanas. 'OnQuit' responde al evento de selección de menú de cerrar la aplicación, etc.

El parámetro de este tipo de métodos es siempre un objeto derivado de wxEvent.

Vemos que en este caso, los métodos 'OnQuit' y 'OnAbout' tienen un parámetro de la clase wxComandEvent, lo que nos da una pista de que responderán a comandos que provendrán de un menú o de un botón.

Definición de la clase marco

La definición de nuestra clase marco es un poco más compleja.

#include "wx001Main.h"

BEGIN_EVENT_TABLE(wx001Frame, wxFrame)
    EVT_CLOSE(wx001Frame::OnClose)
    EVT_MENU(idMenuQuit, wx001Frame::OnQuit)
    EVT_MENU(idMenuAbout, wx001Frame::OnAbout)
END_EVENT_TABLE()

wx001Frame::wx001Frame(wxFrame *frame, const wxString& title)
    : wxFrame(frame, -1, title)
{
    // create a menu bar
    wxMenuBar* mbar = new wxMenuBar();
    wxMenu* fileMenu = new wxMenu(_T(""));
    fileMenu->Append(idMenuQuit, _("&Quit\tAlt-F4"), _("Quit the application"));
    mbar->Append(fileMenu, _("&File"));

    wxMenu* helpMenu = new wxMenu(_T(""));
    helpMenu->Append(idMenuAbout, _("&About\tF1"), _("Show info about this application"));
    mbar->Append(helpMenu, _("&Help"));

    SetMenuBar(mbar);

    // create a status bar with some information about the used wxWidgets version
    CreateStatusBar(2);
    SetStatusText(_("Hello Code::Blocks user!"),0);
    SetStatusText(wxbuildinfo(short_f), 1);
}

wx001Frame::~wx001Frame()
{
}

void wx001Frame::OnClose(wxCloseEvent &event)
{
    Destroy();
}

void wx001Frame::OnQuit(wxCommandEvent &event)
{
    Destroy();
}

void wx001Frame::OnAbout(wxCommandEvent &event)
{
    wxString msg = wxbuildinfo(long_f);
    wxMessageBox(msg, _("Welcome to..."));
}

Nota: He suprimido las partes de compilación condicional.

Lo primero que encontramos es la definición de la tabla de eventos. Mediante las macros BEGIN_EVENT_TABLE y END_EVENT_TABLE. En cada línea se define la respuesta a un evento.

Por ejemplo, para responder al evento 'Close', se usa la macro EVT_CLOSE, y entre paréntesis el método de nuestra clase marco que debe procesar ese evento, en este caso, wx001Frame::OnClose.

El resto son entradas para procesar comandos de menú, que usan la macro EVT_MENU. En este caso se necesitan dos parámetros. El primero es el identificador del ítem de menú que envía el comando, y el segundo el método que los procesa.

Así por ejemplo, en nuestra clase wx001Frame, en el enumerado anónimo habíamos declarado el identificador idMenuQuit, y después el método OnQuit. La macro EVT_MENU(idMenuQuit, wx001Frame::OnQuit) asocia el evento de menú con el identificador idMenuQuit con el método OnQuit.

Seguidamente encontramos el constructor de la clase wx001Frame, que recibe dos parámetros: un puntero a la clase base wxFrame y una cadena con el título del marco.

El constructor invoca primero al constructor de la clase base.

A continuación crea un menú y una barra de estado. Veremos esto más abajo en detalle.

El destructor no hace nada. El menú y la barra de estado se destruyen automáticamente al destruir el marco, no tenemos que preocuparnos por eso.

Finalmente se definen los métodos que responden a los diferentes eventos.

Creación de un menú

Antes veamos qué partes componen un menú en una aplicación GUI.

Diferentes partes de un menú.
Partes de un menú

En estado de reposo, la única parte siempre visible de un menú es la 'barra de menú'. Se trata de un menú en el que las opciones se alinean horizontalmente.

Cada una de las opciones de la barra de menú puede activarse mediante un clic del botón izquierdo del ratón. Cada opción tiene asociado un menú. Mientras esté activa al pasar el cursor del ratón sobre cada opción se mostrará un menú, en este caso con sus opciones alineadas verticalmente.

Cada una de las opciones de un menú se denomina ítem de menú.

Los ítems en menús verticales también pueden desplegar nuevos menús, con sus correspondientes ítems.

A cada una de estas tres partes se corresponde una clase que la encapsula:

  • Barra de menú: se encapsula en la clase wxMenuBar.
  • Menú: se encapsula en la clase wxMenu.
  • Ítem de menú: se encapsula en la clase wxMenuItem.

El procedimiento para crear un menú consiste en:

  1. Crear la barra de menú:

    wxMenuBar* mbar = new wxMenuBar();
    
  2. Para cada opción en la barra de menús:
    1. Crear un menú:

      wxMenu* fileMenu = new wxMenu(_T(""));
      
    2. Y se añaden los diferentes ítems mediante el método wxMenu::Append:

      fileMenu->Append(idMenuQuit, _("&Quit\tAlt-F4"), _("Quit the application"));
      
  3. Finalmente se asigna la barra de menú al marco usando el método wxFrame::SetMenuBar:

    SetMenuBar(mbar);
    

En este procedimiento no hemos creado objetos de la clase wxMenuItem, pero el método wxMenu::Append está sobrecargado para usar un objeto de esa clase. Usaremos esos objetos si queremos utilizar alguno de sus métodos, como por ejemplo Enable para activar o desactivar ítems de menú, o SetBitmap para asignar un mapa de bits. Aunque incluso esto es opcional, ya que el método Append retorna un puntero al ítem de menú añadido, y además, siempre podemos obtener un puntero a un ítem de menú mediante su identificador.

Nota: La macro _T() sirve para convertir una cadena a Unicode.

Nota: La macro _() indica que la cadena debe ser traducida (veremos esto en un capítulo posterior).

Método Append

Hablemos un poco sobre los parámetros del método wxMenu::Append.

El método Append está sobrecargado. Una de las sobrecargas se considera obsoleta, y no la usaremos.

La segunda sobrecarga es la que hemos usado. Admite los siguientes parámetros:

  1. El identificador del ítem de menú.
  2. El texto de ítem. Este texto permite definir una tecla activa, añadiendo un carácter & delante del carácter que queremos que active el ítem. Para activar los aceleradores el usuario debe pulsar la tecla Alt. Esos caracteres aparecerán subrayados en cada ítem de menú y se podrán activar pulsando la tecla correspondientes. Además, se puede añadir un atajo de teclado. Para ello se usa un carácter tabulador como separador '\t' y a continuación el código de una tecla, a la que se puede añadir un modificador, Shift, Ctrl o Alt. En nuestro programa de ejemplo tenemos un ítem con la cadena "&Quit\tAlt-F4", que se mostrará como "Quit Alt-F4", y se activará con la tecla 'Q' si el menú está activo, o con la combinación Alt+F4, aunque el menú no esté activo.
  3. El texto de ayuda. Ese texto explica de forma somera la función del ítem. Se mostrará en la barra de estado, si existe.
  4. El tipo de ítem de menú. Puede ser wxITEM_SEPARATOR, wxITEM_NORMAL, wxITEM_CHECK o wxITEM_RADIO. Por defecto, si no especificamos este parámetro, será wxITEM_NORMAL.

La tercera sobrecarga tiene un único parámetro, que es un puntero a un objeto wxMenuItem.

Separadores

Los separadores son uno de los tipos de ítem de menú. Su utilidad es puramente estética. Se usan para separar físicamente grupos de ítems, por ejemplo, en el menú de fichero, separaremos las opciones de guardar y abrir documentos.

fileMenu->Append(idMenuAbrir, _("&Abrir...\t-Ctrl-F"), _("Abrir un documento"));
fileMenu->Append(-1, _T(""), _T(""), wxITEM_SEPARATOR);
fileMenu->Append(idMenuGuardar, _("&Guardar...\t-Ctrl-S"), _("Guardar un documento"));

Items con checkbox

Otro de los ítems de menú es el que permite marcar una casilla de verificación, un checkbox. Cada vez que se seleccione el ítem cambiará el estado de la marca.

Crear uno de estos ítems es simple, bastará con usar el valor wxITEM_CHECK en el cuarto parámetro del método wxMenu::Append.

fileMenu->Append(idMenuCheck2, _("&Marcar\tF3"), _("Opción marcable"), wxITEM_CHECK);

O usar un puntero a un objeto wxMenuItem con la otra sobrecarga de wxMenu::Append:

wxMenuItem *item = new wxMenuItem(__null, idMenuCheck, _("&Marcar\tF3"), _("Opción marcable"), wxITEM_CHECK);
fileMenu->Append(item);

Obtener el estado de la marca

En algún punto de nuestro código querremos conocer el estado de la marca para ese ítem de menú.

Para ello, el objeto wxMenuItem dispone de un método IsChecked(), que nos devuelve el estado de la marca, un valor true indica que la marca está activa.

Si hemos conservado el puntero al objeto wxMenuItem, podremos invocarlo directamente mediante su método IsChecked():

bool marca = item->IsChecked();

Pero no será necesario, ya que podemos localizar un ítem de menú mediante su identificador.

Para ello disponemos de algunos métodos:

  • El método GetMenuBar() de wxFrame nos devuelve la barra de menú asociada al marco.
  • El métodoFindItem() de wxMenuBar nos devuelve el ítem de menú con el identificador que pasamos como parámetro.
  • Aplicamos el método IsChecked() al valor obtenido, y recuperamos el estado del ítem.
bool marca = GetMenuBar()->FindItem(idMenuCheck)->IsChecked();

Otra alternativa es procesar el evento wxCommandEvent cada vez que se active la opción de menú. Los ítems de menú con marca también generan eventos de comando.

Los objetos de la clase wxCommandEvent también disponen de un método IsChecked(), de modo que podemos mantener un dato con el estado del ítem tan pronto como ese estado cambie.

Tendremos que añadir la entrada correspondiente en la tabla de eventos, y también un método OnCheck para procesar el evento:

BEGIN_EVENT_TABLE(wx001Frame, wxFrame)
...
    EVT_MENU(idMenuCheck, wx001Frame::OnCheck)
END_EVENT_TABLE()
...

class wx001Frame: public wxFrame
private:
...
        bool marca;
...
        void OnCheck(wxCommandEvent& event);
...

Y definir el método OnCheck:

void wx001Frame::OnCheck(wxCommandEvent& event) {
    marca = event.IsChecked();
}

Establecer marca

También es probable que queramos establecer un valor inicial a la marca. Por defecto, el valor inicial es sin marca. Si queremos activar la marca podemos usar el método wxMenuItem::Check(), indicando como parámetro true para activar la marca o false para desactivarla.

Items con radio

Los ítems de menú con opciones de radio funcionan de forma parecida a los de marca. La diferencia es que si se insertan de forma consecutiva funcionan como un grupo, de modo que solo uno de ellos puede estar marcado.

Se insertan igual que el resto de ítems de menú, indicando en el cuarto parámetro de Append el valor wxITEM_RADIO.

No tiene sentido usar un único ítem de radio, de modo que siempre los usaremos por grupos, de al menos dos ítems. El sistema se encarga automáticamente de que solo uno de los ítems de cada grupo tenga la marca activa.

    fileMenu->Append(idMenuRadio1, _("Radio &1\tF5"), _("Item radio 1"), wxITEM_RADIO);
    fileMenu->Append(idMenuRadio2, _("Radio &2\tF6"), _("Item radio 2"), wxITEM_RADIO);
    fileMenu->Append(idMenuRadio3, _("Radio &3\tF7"), _("Item radio 3"), wxITEM_RADIO);

Obtener estado de la marca

Podemos conocer el estado de la marca de cada ítem del grupo de forma individual, como hicimos con los ítems con check, usando el método IsChecked().

Pero esto es poco eficiente, ya que nos obliga a consultar cada ítem del grupo hasta que encontremos en que está marcado.

Con los ítem con radio es preferible procesar los eventos que se producen al seleccionarlos. Pero en lugar de usar un método para cada ítem, podemos procesar en un único método un rango de ítems usando EVT_MENU_RANGE. Para ello debemos asegurarnos de que los identificadores sean correlativos, o al menos que en rango no haya identificadores de otra cosa que no sean los ítems del grupo.

    EVT_MENU_RANGE(idMenuRadio1, idMenuRadio3, wxFrame::OnRadio)

Esto invocará a un único método para cualquiera de los ítems que provoque el evento.

Para procesar el evento podemos usar un switch que verifique cual de los ítems ha provocado el evento. Para ello usaremos el método GetId() del CommandEvent, o, si nuestros identificadores son correlativos, bastará con hacer una resta:

void wx001Frame::OnRadio(wxCommandEvent& event) {
    radio = event.GetId() - idMenuRadio1;
}

Establecer marca

Por defecto, cuando se crea el menú, se marcará el primer ítem del grupo. Podemos marcar otro ítem usando el método wxMenuItem::Check(), igual que con los ítems con marca. Si se activa cualquiera de los ítems de radio de un grupo, se desactivará el que estaba marcado previamente.

mbar->FindItem(idMenuRadio2)->Check();

Habilitar y deshabilitar opciones de menú

Muy a menudo será conveniente deshabilitar ciertas opciones de menú, de modo que no puedan ser seleccionadas. Por ejemplo, si no hemos abierto un documento, no tiene sentido que la opción de guardar esté disponible, o si solo podemos editar un documento, no tendrá sentido disponer de opciones para crear o abrir documentos, etc.

Para habilitar o deshabilitar una opción de menú usaremos el método Enable() de wxMenuItem, indicando mediante un parámetro de tipo bool si queremos activar o desactivar la opción.

mbar->FindItem(idMenuRadio3)->Enable(false);

Alternativamente, podemos usar el método Enable() de wxMenu, indicando en este caso el identificador del ítem a activar o desactivar y el parámetro bool.

fileMenu->Enable(idMenuRadio3, false);

O incluso disponemos de un método Enable() en la clase wxMenuBar, con los mismos parámetros que el de la clase wxMenu.

mbar->Enable(idMenuRadio3, false);

Añadir mapas de bits

Añadir imágenes a los ítems de menú puede ser una ayuda para el usuario, o simplemente una mejora estética. Es posible añadir un mapa de bits a los ítems de menú mediante el método SetBitmap de wxMenuItem.

Indicaremos como parámetro un objeto wxBitmapBundle, que contendrá una o más imágenes en diferentes resoluciones, de modo que el sistema pueda escoger la más adecuada en cada caso.

Hay varias formas de crear un objeto wxBitmapBundle, y dedicaremos un capítulo a este tema más adelante. Para nuestros propósitos, y para aplicaciones Windows, usaremos un fichero de recursos, que tiene la ventaja de integrar los mapas de bits en el mismo ejecutable que el resto de la aplicación.

Usaremos ficheros de gráficos en formato png, aunque wxWidgets proporciona formas de leer otros formatos, como bmp o xpm.

Para que nuestra aplicación pueda procesar ficheros en formato png tendremos que incluir el fichero de cabecera "imagpng.h", y añadir un manipulador mediante el método AddHandler de la clase wxImage:

#include <wx/imagpng.h>
...
wx001Frame::wx001Frame(wxFrame *frame, const wxString& title)
...
wxImage::AddHandler(new wxPNGHandler);
...

Añadiremos nuestros mapas de bits al fichero de recursos, resource.rc:

bmabrir16    RCDATA  "open-file16x16.png"
bmabrir32    RCDATA  "open-file32x32.png"

Por último asignaremos el mapa de bits al ítem de menú usando el método SetBitmap, usando la macro wxBITMAP_PNG para crear un objeto wxBitmapBundle a partir de un recurso.

    wxMenuItem *abrir = fileMenu->Append(idMenuAbrir, _("&Abrir...\tCtrl-F"), _("Abrir un documento"));
    abrir->SetBitmap(wxBITMAP_PNG(bmabrir16));

O, más directamente:

    fileMenu->Append(idMenuAbrir, _("&Abrir...\tCtrl-F"), _("Abrir un documento"))->SetBitmap(wxBITMAP_PNG(bmabrir16));

Una segunda forma es cargar el fichero con el bitmap durante la ejecución, que tiene la ventaja de ser más fácil de hacerlo compatible con otras plataformas, aunque deja expuestos los ficheros.

Para ello usaremos el método FromFiles de la clase wxBitmapBundle.

    wxMenuItem *guardar = fileMenu->Append(idMenuGuardar, _("&Guardar...\tCtrl-S"), _("Guardar un documento"));
    wxBitmapBundle bmb;
    guardar->SetBitmap(bmb.FromFiles("save-file16x16.png"));

Podemos hacer que cuando el usuario pulse el botón derecho del ratón se muestre en esas coordenadas un menú emergente (popup), cuyas opciones dependan de la ventana, diálogo o control sobre el que se encuentre el ratón en ese momento.

Para cada ventana que queramos que pueda mostrar un menú contextual deberemos procesar el evento EVT_CONTEXT_MENU y añadir un método para procesar ese evento, que recibirá un parámetro de la clase wxContextMenuEvent.

BEGIN_EVENT_TABLE(wx001Frame, wxFrame)
...
EVT_CONTEXT_MENU(wx001Frame::OnContextMenu)
END_EVENT_TABLE()
...
void OnContextMenu(wxContextMenuEvent& event);

Por supuesto, también deberemos crear un objeto wxMenu y los métodos necesarios para procesar cada una de las acciones asociadas con sus ítems.

    wxMenu *popupMenu;

Nuestro método para procesar el evento puede usar el miembro pos para seleccionar el menú a mostrar, existen métodos para averiguar sobre qué control está el puntero del ratón.

Para mostrar el menú usaremos el método PopupMenu.

void wx001Frame::OnContextMenu(wxContextMenuEvent& event) {
    PopupMenu(popupMenu);
}

Ejemplo 1

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo 1 wx001.zip 2024-12-02 7978 bytes 5