Eventos "a la" .NET

Dado que últimamente ni Word ni el Windows Live Writer ni siquiera la edición a mano en la plantilla de este blog me deja escribir algo coherente (cuando no son los acentos, son los ejemplos de código, etc), y como no vivo para la informática, sino que la informática vive para mí, esta entrada está en PDF.


La podéis bajar de aquí.

C++/CLI VI: Delegados e hilos

Esta no va a ser una entrada larga explicando qué es un delegado y cómo hacer cositas con él. Esta va ser una entrada de hombres para hombres, es decir, una entrada en la que explico algo que he leído en C++/CLI in Action y que considero muy interesante y muy cuca.


Vamos a utilizar un delegado para crear un hilo… pero sin hilos; voy a explicar cómo utilizar un delegado asíncrono para bifurcar el hilo principal de la aplicación y realizar dos tareas simultáneas.


Seguro que más de uno en sus programas utiliza hilos para realizar tareas fuera de la secuencia principal de su aplicación, como por ejemplo guardar un archivo muy grande mientras se deja al usuario que continúe con sus cosas. El procedimiento habitual es crearse un método miembro a la clase que hará en trabajo duro, seguido de una variable de tipo Thread que se instanciará más o menos así:


Thread ^hilo=gcnew Thread(gcnew ThreadStart(this,&Clase::Metodo));
hilo->Start();


Y luego es cosa nuestra monitorizar y esperar a que acabe el citado hilo.


Pero con delegados la cosa se puede simplificar mucho. Primero vamos a tratar la versión reducida:


using namespace System;


delegate void DoWorkDelegate(void);


void DoWork(void)


{


    Console::WriteLine(«Start Job»);


    System::Threading::Thread::Sleep(5000);


    Console::WriteLine(«End Job»);


}


int main(array<System::String ^> ^args)


{


    DoWorkDelegate ^dw=gcnew DoWorkDelegate(&DoWork);


    Console::WriteLine(«Before Invocation»);


    dw();


    Console::WriteLine(«After Invocation»);


    Console::ReadKey(true);


    return 0;


}


El código está claro. Primero definimos la firma del delegado, luego creamos un método que lo implementa y finalmente dentro de main() hacemos la llamada tras crearlo. La salida por pantalla debería ser similar a esta:


Before Invocation
Start Job
End Job
After Invocation


Nos damos cuenta de que primero se inicia el trabajo, luego se termina y finalmente cerramos el programa. De esta forma estamos bloqueando el hilo principal del programa. Si estuviéramos trabajando con WindowsForms, Windows nos diría que nuestra aplicación no responde y el usuario también estaría esperando a que la llamada al delegado terminara.


Pero si lo hacemos así:


delegate void DoWorkDelegate(void);


void DoWork(void)


{


    Console::WriteLine(«Start Job»);


    System::Threading::Thread::Sleep(10000);


    Console::WriteLine(«End Job»);


}


void WorkEnded(IAsyncResult ^)


{


    Console::WriteLine(«The work has ended»);


}


int main(array<System::String ^> ^args)


{


    DoWorkDelegate ^dw=gcnew DoWorkDelegate(&DoWork);


    Console::WriteLine(«Before Invocation»);


    dw->BeginInvoke(gcnew AsyncCallback(&WorkEnded),nullptr);


    Console::WriteLine(«After Invocation»);


    Console::ReadKey(true);


    return 0;


}


estamos realizando la llamada de forma asíncrona. El código sólo es un poco más complejo, pero a veces puede valer la pena.


Ahora añadimos un nuevo método que es una implementación del delegado AsyncCallback cuya firma ya trae el .NET preparada. Dicho callback se encargará de notificarnos de que la operación ha terminado.


Ya dentro de main() la invocación del delegado se realiza mediante el método BeginInvoke(), que toma una instancia del callback que nos notificará la finalización del proceso y otro objeto definido por el usuario que será pasado al parámetro de dicho callback.


Si ejecutáramos esto, obtendríamos la siguiente salida por pantalla:


Before Invocation
After Invocation
Start Job
End Job
The work has ended


Como podemos ver el código dentro de main() se ejecuta sin esperar a que lo haga el delegado, de modo que no estamos interrumpiendo el devenir del bucle principal. Deteniéndonos un poco en la salida por pantalla, primero se pintan las tres primeras líneas, transcurren los diez segundos de rigor e inmediatamente se pintan las dos siguientes. La última es el callback que nos indica que el delegado ha terminado de realizar su trabajo.


Como ha comentado Augusto Ruiz, hay que tener en cuenta que el método WorkEnded() se está ejecutando desde el nuevo hilo y no desde el principal, así que debemos tener cuidado a la hora de hacer según qué cosas dentro de dicho método, así como en el propio delegado (como por ejemplo, acceder a la UI mediante llamadas a Invoke() en lugar de operar directamente con los componentes de la ficha).


Pero esto es solo el principio. La documentación explica más formas de realizar este tipo de llamadas asíncronas y aunque los ejemplos están hechos en C#, son fácilmente traducibles a C++/CLI.


Puedes obtener una copia de este artículo en PDF aquí.

Windows CE, "accel_c", el catálogo y LMQMP

Sí, ya sé que el título es extraño, pero me ha hecho perder tres días completos como si nada… Pero comencemos por el principio.


A veces veo muertos… digoooo… a veces me tengo que pelear con el Windows CE para modificar nuestra plataforma para algún cliente. Lo cierto es que debería hacerlo la empresa propietaria de la placa, pero éste prefiere delegar en mi, seguro que para quitarse el muerto de encima.


Teóricamente la creación o modificación de una plataforma existente debería ser algo trivial, tan trivial como abrir el Platform Builder con el proyecto adecuado, quitar y poner cosas del catálogo, recompilar y listo. Al cliente se le distribuye el eboot.nb0 y el nk.bin.


Pero de la teoría a la práctica va un gran trecho. En general un cambio en algunos componentes significa que otros dejan de funcionar por dependencias incompatibles, y también en general dichas dependencias ni están todo lo claras que deberían, ni a veces existen.


En mi caso actual me han pedido un servidor ftp y uno web con asp y javascript, así como soporte para ATL y ASP. El asunto es teóricamente sencillo: ir añadiendo los componentes desde el catálogo, recompilar y listo.


Pues no. Tras una compilación aparentemente exitosa, el proceso «gwes.exe» se caía al arrancar, con lo que dejábamos de tener bucle de mensajes y sonido… Como esta compilación es sin pantalla, no disponemos de ella desde el principio, pero si la tuviéramos también la habríamos perdido.


Tras solucionar el tema del «gwes.exe» (un pequeño cambio en el driver dummy de vídeo de la placa, cosa de Cirrus Logic), resulta que ahora el CE no ve ni la tarjeta SD ni es capaz de conectarse al Visual Studio 2005SP1 para depurar… porque la placa no es capaz de cambiarse la IP y porque sigue habiendo algo mal.


Vale, lo de la SD ya me ha pasado antes: me falta incluir a mano en el «project.bib» una DLL que si no está el sistema no es capaz de verla y no encima no protesta…


Seguimos con lo nuestro. Ahora falta que el TCP/IP vuelva a funcionar, porque finalmente, tras añadir todo lo anterior, ni el servidor ftp ni el web funcionan, y menos aún la opción de conectar al Visual Studio 2005…


Y comienza el baile. Nada de lo que hago hace que funcione. Ni siquiera volviendo a la plataforma original: por algún motivo algo dentro del CE se ha ido al carajo. La solución en este caso siempre ha sido regenerar la plataforma desde cero, es decir, un clean total y volver a compilarlo todo. Nada, sigue fallando. La solución drástica: máquina virtual nueva y todo desde cero.


Ya empiezan a salir cosas, dependencias que faltan. Compilaciones que fallan. Todo ello debido a referencias que faltan. Es decir, ATL necesita MINGWES o GWES, pero no lo dice. MINGWES necesita MINGDI pero tampoco lo dice. Y así con unas cuantas dependencias, hasta que al final me quedo con tres funciones relativas a los aceleradores de teclados (es decir, el que funcionen los atajos de teclado como CTRL-C y que una aplicación pueda procesarlos a través de su bucle de mensajes, que no tiene mucho sentido en un dispositivo que no va a llevar ni teclado ni ratón ni monitor, pero bueno).


INTERLUDIO: Por todo ello deduzco que tanto el Platform Builder como la propia máquina virtual estaban corrompidas de algún modo; la cuestión pendiente es porqué una VM se ha corrompido de esa forma… Pero dejemos el tema y volvamos a lo nuestro:


La cuestión ahora consiste en que no encuentro esas dependencias de ninguna manera, pero hay truquitos, sacados de blogs de gente de Microsoft. Primero vamos a encontrar a qué componente del catálogo pertenecen esas funciones.


Nos vamos a C:WINCE500PRIVATEWINCEOSCOREOSCOREDLLcoredll.def y buscamos una de las funciones que faltan. El valor es COREDLL_ACCEL_C, es decir, «accel_c». Esa es la dependencia que tenemos que buscar.


Nos vamos a la MSDN y efectivamente, aquí esta. ¡Tachán! El componente se llama «Shell»… Añadimos la dependencia y… y… y… nones, seguimos teniendo el problema. Probamos varias shells: ninguna va, o más bien ninguna tiene dentro lo que la documentación dice que debe tener.


Pero uno, como es perro viejo, mira un poco, pone en marcha su sexto sentido y la lógica más abstrusa y añade el componente MINWMGR, que, evidentemente, tampoco funciona.


Hala, a buscar en los foros. No hay nada de nada. Alguien comenta con otro problema similar que se añada a CESYSGEN.BAT la variable de entorno COREDLL_COMPONENTS y se añada a mano el «accel_c». Que si quieres arroz Catalina.


Finalmente, la solución drástica: una búsqueda total en todo el árbol del Windows CE, pese a haber encontrado algo en la documentación pero que tampoco funciona; la documentación decía que para encontrar dependencias «extrañas» hay que ir al CESYSGEN.BAT del Sistema Operativo y buscar dicha dependencia y ver cuál es su padre: pues resulta que en ningún CESYSGEN.BAR está «accel_c», por lo que decido la búsqueda total por contenido de archivo.


Al final me aparece otro fichero BAT que sí la tiene: C:WINCE500PUBLICCEBASEOAKMISCwinceos.bat. Y, pese a la mala documentación, a los blogs mentirosos y a mucha gente que no tiene casi ni idea de lo que habla, el menda la ha encontrado, y tras mirarse con detalle las cosas, determina


¡que la dependencia está en MINWMGR!


Ya es el colmo. ¡Está ahí pero no compila!… ¿Más corrupción de ficheros? Pues sí, tras marcar «Clean before building» en las opciones del Platform Builder, la cosa compila bien…


Pero el proceso «gwes.exe» continúa petando, justo como al principio de todo.


Para evitar que el proceso «gwes.exe» explote, la solución correcta es añadir el driver de vídeo de la BSP completo, así que lo añado y ya no peta… pero sigo sin poder conectar con el VS2005SP1 y ahora tengo otros errores relativos a un teclado y un ratón PS/2 que no existen y que no pueden existir ya que la placa no tiene puertos para ello. Tras cambiar el driver completo por el dummy, ahora tengo errores sobre que no he definido ningún lenguaje por defecto… y tengo el English(US) definido…


Y llevo cuatro días con esto.


Y los que me quedan. Seguiremos informando, pero ya os podéis hacer una idea de la gente que está haciendo PDAs y aparatos similares con Windows CE… Y ahora que no me salga ningún fundamentalista linuxero diciendo que con Linux eso no pasa, os puedo asegurar que las aventuras bajo Linux son mucho, pero mucho peores…

TIP: Filtrar números en un campo de edición

Si uno quiere aplicar una máscara dentro de un campo de edición, tiene a su disposición el componente MaskEdit, que posee una propiedad llamada Mask y que, asignando ahí una serie de símbolos predefinidos, nos guiará a la hora de qué texto podemos introducir. Perfectamente válida en muchas situaciones, en otras es un engorro de lo más complicado, ya que, aunque la potencia del filtrado es enorme, a veces no nos permite una flexibilidad adecuada. En mi caso no he podido encontrar la máscara adecuada a la introducción de cualquier valor numérico entero sin tener problemas con los sombreados, los valores por defecto y un largo etcétera.


Por eso, siempre que necesito un campo numérico cualquiera sin formato, es decir, un campo que me permita introducir valores como «0», «33», «56789,89» de forma libre sin los problemas de la máscara -el que haya usado el componente sabrá a qué me refiero-, puede utilizar una técnica alternativa bastante sencilla.


Si tiene varios controles en una misma ficha que sólo permitan la introducción de valores numéricos, lo primero a realizar es seleccionar todos esos controles de edición de forma conjunta, irse a la ventana de propiedades y seleccionar la pestaña de eventos. Haciendo doble clic en el evento KeyDown obtendrá un manejador único para todos los controles seleccionados.


Otra forma consiste en, una vez generado dicho evento para un solo control, seleccionar uno a uno (o varios a la vez) los demás controles de edición y, en el mismo lugar que antes, abrir el desplegable del evento KeyDown y seleccionar el evento anterior.


Ya solo nos queda la edición del código que nos interesa. En mi caso, el código es:



void ConfigForm::EEditKeyDown(Object ^,KeyEventArgs^ e)
{
  if
(!
    (
      (e->KeyCode>=Keys::D0 && e->KeyCode<=Keys::D9
)||
      (e->KeyCode>=Keys::NumPad0 && e->KeyCode<=Keys::NumPad9
)||
       e->KeyCode==Keys::NumLock
|| 
       e->KeyCode==Keys::Left
|| 
       e->KeyCode==Keys::Right
|| 
       e->KeyCode==Keys::Insert
|| 
       e->KeyCode==Keys::Delete
||
       e->KeyCode==Keys::Tab
|| 
       e->KeyCode==Keys::Escape
|| 
       e->KeyCode==Keys::OemPeriod
|| 
       e->KeyCode==Keys::
Back
   
)
 
)
    e->SuppressKeyPress=true
;
}


Básicamente estamos eliminando cualquier tecla que no se corresponda a las listadas. Las dos primeras líneas de la sentencia condicional miran si el valor introducido es un número, y las siguientes si se ha presionado dicha tecla especial. Como la sentencia está negada, el condicional sólo se ejecutará en el caso de que la tecla pulsada no sea ninguna de ellas.


Evidentemente, siempre podremos añadir o quitar las teclas que queramos a la lista.


Otra opción quizás más sencilla, es la de colocar todos los valores válidos dentro de un array estático dentro de la ficha (o incluso fuera de ella, en una clase propia) y realizar la comprobación mediante un bucle o incluso ordenar el array y utlizar el método BinarySearch junto a un predicado. Esa parte la deja el autor como ejercicio para el lector, y si éste tiene dudas para hacerlo, que me lo comunique y pondré una posible solución.


Y todavía podemos afinar más, mediante la realización de un componente que de alguna forma nos de la opción de elegir qué teclas permitimos (por ejemplo mediante un editor visual que nos permita marcar dichas teclas, o mediante una lista a la que podemos añadir/quitar elementos), pero aquí el autor considera que la ineficiencia del control (que llevaría mucho código sin utilizar), no supera la facilidad de uso; además, dicho control sería un ensamblado a acompañar a nuestra aplicación…