Dec 19 2008

Programando un WebBrowser multiplataforma

Hace un tiempo les comente de una aplicación con un ActiveX WebBrowser basado en Gecko y Mozilla. Esta aplicación esta hecha en .NET (NetFramework de Microsoft) con Windows Forms y el control principal, pues es un ActiveX. Obviamente esta aplicación es para Windows, pero por ciertas necesidades que aquí no vienen a cuento pero requieren que el explorador quede abierto después de una manipulación inicial al contenido de la página mostrada, se requirio un navegador así pero en Linux, entonces ¿Podria hacer una aplicación multiplataforma en lugar de dos especializadas? Me aventure a intentarlo. Aclaro desde ahora que aquí solo incluyo un ejemplo simple de como montar crear un explorador basico y nada más.

Empece por contemplar posibilidades para hacerlo. Hay algunas como por ejemplo WebControl con Mono 2.0 porque en esta versión se incluyo en el framework un control basado en Mozilla a modo de emular la manera en que el NetFramework de Microsoft puede manejar el motor de Internet Explorer para incluir navegación web en controles windows forms. Mono 2.0 incluye soporte para windows forms permitiendo usar aplicaciones windows como multiplataforma.

Del caso anterior no me gusto mucho el uso de Windows forms porque después de todo en mi caso particular mi prioridad es que funcione en linux me parecio mejor algo con GTK así que escogí probar a incluir un control de Gecko en una ventana GTK. Para esto use el control Gecko#. Hay un control similar llamado GtkMozEmbed que también permite incluir browsers en aplicaciones GTK pero me parecio que esta algo verde, aunque no la probe solo lei la documentación de su página y reconocen que esta limitada. No encontre como manejar el DOM, solo cargar páginas.

También existe en desarrollo un control para incluir WebKit en ventanas GTK#. WebKit también es un motor de navegadores web y es usado por Safari y puede ser incluido en las aplicaciones .NET consiguiendolo para agregarlo como referencia en la aplicación. El componente se llama webkit-sharp para quien quiera profundizar en sus posibilidades. Esta opción la encontre cuando ya tenia un poco avanzado el desarrollo y por eso no indague más pero me parece bueno mencionarla también.

AMBIENTACIÓN Y REQUERIMIENTOS

Desarrollo esta aplicación usando SharpDevelop (2.2.1), compilando con Mono 2.0 y creando el entorno grafico con GTK#; todo sobre Windows XP en español, el sistema operativo que instalaron en mi trabajo. Extraño el diseñador de ventanas Stetic que trae MonoDevelop pero ya que, si lo necesito en mi casa tengo Ubuntu con MonoDevelop y ambiente grafico Gnome que me servira para pruebas complementarias de compatibilidad y también dispongo de una computadora con la distribución slackware con el ambiente grafico KDE 3.4 donde también probare el desarrollo. Uhm, me falta un Mac… ademas no me atrae nada de nada seguirle con esto en casa, salvo la curiosidad de ver si funciona o no, cosa que no me llevará mucho tiempo.

Dando por hecho que es obvio que hay que tener instalado MONO (estoy usando el 2.0.1) y de preferencia un IDE como MonoDevelop (GNU/Linux) o Sharp Develop (Windows) para manejarlo, aunque te las podrias arreglar con un editor de texto y compilando por linea de comandos si te gusta “lo retro“, te gusta presumir que programas “como los hombres” o si por alguna razón particular no hay una mejor opción en tu caso.

Para poder compilar adecuadamente con SharpDevelop y Mono en Windows XP en español tuve que hacer unos ajustes a las variables de entorno de Windows XP. Hay que crear la variable MONO_EXTERNAL_ENCODINGS para que mono trabaje perfectamente con una PC que no tenga el sistema operativo por default en ingles. si, ya se, que chafa, decia, es así:

Ah, no faltara quien no sepa donde y como poner las variables así que… Click derecho a Mi PC -> Propiedades -> Pestaña Opciones Avanzadas -> Variables de entorno. Con eso les aparecera la ventana donde poner variables de entorno para tu usuario y globales para todos. Si una misma variable existe en ambos, se le da prioridad a la del usuario sobre la global. La global solo se toma si en tu usuario no existe. Toma en cuenta esto. En mi caso declare a MONO_EXTERNAL_ENCODINGS como global.

Variables de entorno

Variables de entorno

Todavia no cierres la ventana, aun hay unas variables de entorno que necesite modificar. Primero la variable Path, donde agregue la dirección del directorio bin de mi instalación de MONO. Las otras variables de entorno que modifique son TEMP y TMP. Estas ultimas indican donde Windows pondra y buscará los archivos temporales que valla creando. En mi caso lo cambie porque en las variables de entorno de mi usuario, al estar el sistema operativo en español, me dejaba los archivos temporales en C:\Documents and Settings\adan\Configuración local\Temp. Esto es un problema por los acentos y espacios en la ruta por lo mismo de que al parecer no validaron que hay programadores fuera de los paises de habla inglesa, y que provoca un error al compilar (creo que era el CS2011) proyectos GLADE# y GTK# (por lo menos en esos me dio problema a mi, tal vez halla más) así que le di la misma dirección que existia en las variables de entorno globales.

Entonces las variables de entorno me modificadas por mi quedan así:

MONO_EXTERNAL_ENCODINGS=default_locale
Path=C:\Archivos de programa\Mono-2.0.1\bin
TEMP=%SystemRoot%\TEMP
TMP=%SystemRoot%\TEMP

Instalando Gecko#

Vamos por partes como el descuartizador, primero necesitamos el Gecko Runtime Enviroment, o GRE para los amigos. Esto nos provee de las librerias necesarias para incluir el motor de navegación de Gecko en nuestras aplicaciones. En Linux esto ya es parte de la distribución en casos que ya traen exploradores basados en Mozilla desde el principio. En Windows se necesita instalar un GRE especial porque ademas de que no esta y al parecer el oficial tiene un problema con pasarle cadenas en UTF8 a Gecko#

Yo use el instalador que se encuentra en la página de Novell, GRE-GeckoSharp-1.7.12-0.1.exe. Si todo salio bien tendran también una nueva variable de entorno que dirá GECKOSHILLA_BASEPATH=C:\Archivos de programa\Archivos comunes\gtkmozembed-win32 y también también debe estar en el Mono Global Assembly Cache. Busquen la dll en la carpeta GAC de la instalación de MONO en Archivos de programa. Esta variable de entorno es requerida en Windows para que la aplicación encuentre la libreria gtkembedmoz.dll, necesaria para usar el WebControl.

Agregando DLL al GAC de MONO

Puedes agregar la dll de Gecko# al GAC de MONO desde linea de comandos con el comando gacutil -i gecko-sharp.dll desde la carpeta donde quedo instalado originalmente. Algo así como en la imagen mostrada arriba.

Ahora si, ya podemos poner el Gecko# en nuestras aplicaciones. Cree un proyecto que usa GTK# para la creación de ventanas y agregue la referencia a la .dll de Gecko# para poder usar el Gecko.WebControl en nuestro código.

agregar referencia

Agregando referencia a Gecko# en nuestro proyecto

Ocurrio un extraño problema que lo deja a uno con cara de WTF? Resulta que a la hora de compilar la aplicación con la dll de Gecko# agregada como referencia el compilador arroja lo siguiente:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets(0,0) : Advertencia MSB3247: Found conflicts between different versions of the same dependent assembly.

¿Pero que rayos? Estoy compilando con MONO ¿Por qué se mete el NetFramework aquí? Solo he instalado una vez el Gecko#, no veo porque deberia tener versiones diferentes regadas por la PC. Basta de preguntas, voy por soluciones. Voy a buscar documentación en internet un rato.

Ok. Al parecer no hay manera de usar MONO al ejecutar el debugger en SharpDevelop, solo con el NetFramework de Microsoft. Para poder usar a MONO y que el .NET de Hasecorp no me tire errores lo que he hecho es lo siguiente.

Primero seleccionar a Mono 2.0 como mi framework indicado. Para eso edito las propiedades del proyecto como se ve a continuación.

Usando el Framework de MONO

Seleccionando MONO 2.0 como mi framework a usar

Con eso usara el Gmcs (o Mcs con MONO 1.1) para correr la aplicación después de compilarla. Pero si uso las opciones de debug sigue recurriendo al de Microsoft y por eso me marca el error antes mencionado arriba. Para poder correr la aplicación con el Framework debo preparar los parametros de linea de comando de esta manera, como muestro en la imagen siguiente hay que indicar que se va a iniciar con un programa externo (MONO) así que indico la ruta donde esta en la instalación y también un par de parametros por linea de comando ${TargetPath} y ${TargetDir}

Preparando para correr con MONO

Preparando el proyecto para correrlo con MONO

Pero como mencione antes, si tratas de usar debugger entonces SharpDevelop recurre al Framework de Microsoft, el cual lanza excepciones raras, así que uso la opción Ejecutar sin depurador o Run without debugger para los que lo tengan en ingles. De este modo no pasará por el debugger y generará el ejecutable compilandolo con MONO, pero siempre que uses el debbuger NetFramework seguira marcando el mismo problema antes mencionado.

Hecho todo esto, ya estoy listo para programar.

DESARROLLO DE LA APLICACIÓN

Me puse a codificar para crear un explorador basico de prueba, muestro el código fuente a continuación. Es solo un explorador sencillo que ejecuto con MONO y enviandole la dirección URL de la página que desee abrir y nada más. En esto han venido a resumirse las horas de documentación y desarrollo en las que he trabajado ultimamente.

using Gtk;
using System;
using Gecko;
namespace GeckoBrowser
{
/// Ejemplo sencillo de explorador web basado en Gecko#
public class MainWindow : Window
{
#region Variables de la clase
/// Explorador web incluido
WebControl web;
/// Direccion de la pagina cargada en el explorador
static string UrlPagina;
#endregion
#region Punto de entrada de la aplicacion
[STAThread]
public static void Main(string[] arg)
{
Application.Init();
if (arg.Length > 0)
{
UrlPagina = arg[0]; // Obtener la pagina indicada por parametros
}
new MainWindow();
Application.Run();
}
#endregion
#region Ventana y sus eventos
public MainWindow() : base(“MainWindow”)
{
DeleteEvent += new DeleteEventHandler(MainWindowDeleteEvent);
#region Codigo requerido por GRE and Gecko# en Windows
string mozillaEnvPath = System.Environment.GetEnvironmentVariable(“GECKOSHILLA_BASEPATH”);
if (mozillaEnvPath != null && mozillaEnvPath.Length != 0)
{
Gecko.WebControl.CompPath = mozillaEnvPath;
}
#endregion
web = new WebControl(); // Inicializar el explorador
web.LoadUrl(UrlPagina); // Abrir la pagina indicada
this.Add(web); // Agregar el control a la ventana
ShowAll();
}
void MainWindowDeleteEvent(object o, DeleteEventArgs args)
{
Application.Quit();
args.RetVal = true;
}
#endregion
}
}

Para que este código funcione he agregado como referencia en mi proyecto las siguientes DLL que he usado y no estan incluidas en NetFramework. Yo las tengo por MONO y por el mismo GTK#

  • atk-sharp
  • gdk-sharp
  • gecko-sharp
  • glib-sharp
  • gtk-sharp
  • pango-sharp

Ademas de estas dll, también pongo en la carpeta junto al ejecutable a gtkembedmoz.dll debido a que cuando lo probe en GNU/Linux (un Ubuntu 8.10 con Gnome para más señas) me arrojo un mensaje diciendo que me faltaba ese archivo. Esta dll la tome de la carpeta donde se instala GTK# como explique al principio de la nota. En linux puedes tomarla de la instalación de Mozilla o FireFox que seguramente estará en /usr/lib/[carpeta de tu navegador] por lo regular.

Navegador

Así se ve el navegador

La aplicación me funciono bien excepto en un caso, cuando la ejecute para abrir una página segura me solicito la instalación del Personal Security Manager (PSM) como requisito para mostrar las páginas HTTPS, con las demas sin problema. Esto es cosa del navegador, Gecko en este caso. Este paquete consiste en un set de librerias para operaciones Cryptograficas, incluida SSL por lo que es necesaria para visualizar “páginas seguras” (https) y manejo de certificados. Al parecer los navegadores como Mozilla FireFox incluyen este componente pero lo hacen en si mismos, en una capa por encima de Gecko así que por eso no lo estoy usando de manera predeterminada.

En GNU/linux me dio una excepción debida a falta de ambientación. El mensaje es este:

Unhandled Exception: System.TypeInitializationException: An exception was thrown by the type initializer for Gecko.WebControl —>
System.DllNotFoundException: gtkembedmoz.dll
at (wrapper managed-to-native) Gecko.WebControl:gtk_moz_embed_get_type ()
at Gecko.WebControl.get_GType () [0x00000]
at GtkSharp.GeckoSharp.ObjectManager.Initialize () [0x00000]
at Gecko.WebControl..cctor () [0x00000] — End of inner exception stack trace —
at GeckoBrowser.MainWindow..ctor () [0x00000]
at GeckoBrowser.MainWindow.Main (System.String[] arg) [0x00000]

Entonces abri el archivo

/etc/environment para agregar la variable LD_LIBRARY_PATH=”[inserte aquí la ruta de los .so de mozilla, firefox o donde sea que este]” y ya con eso funciona.

Explorador solicitando Personal Security Manager

Otro problema que puede llegar a darse y que aun no he resuelto es cuando el navegador trabaja tras un firewall y necesites darle una IP particular al browser como salida a Internet. Aun no se como indicarselo.

Estos casos aun no los resuelvo, así como tampoco he implementado control de los eventos del navegador y del DOM de las páginas deplegadas en él, pero esos puntos me interesa trabajarlos también aunque este ultimo no estoy seguro que tan profundamente se pueda manejar con Gecko# o si será mejor cambiar de enfoque. En cualquier caso practicar el uso de GTK# me cayo bien para refrescar memoria pues hace tiempo no lo usaba más que en GNU/Linux con el diseñador de ventanas STETIC de MonoDevelop.

Acepto sugerencias de mejora y resolución de lo que falta.

Nov 04 2008

Un motivo más para evitar Internet Explorer, 2da parte.

FireFox

Mozilla FireFox

Ya encontre como poder obtener el código fuente de la página cargada en el explorador (Mozilla) montado en nuestra aplicación y como poder sobreescribirlo por otro modificado a nuestro gusto.

// Obtener el contenido de la página cargada en el explorador
mshtml.IHTMLDocument2 pagina = (mshtml.IHTMLDocument2)WebBrowser.Document;
// Obtener su código fuente
string codigoFuente = pagina.body.outerHTML;

///
/// TODO: Modifica el código fuente a tu gusto y necesidad
///

// Sobreescribir el codigo fuente original con nuestro código
doc.body.innerHTML = nuevoCodigoFuente;

La clave esta en usar el objeto de tipo mshtml.IHTMLDocument2. Tuve que volver a agregar la referencia a mshtml.dll porque en las de Mozilla nada más no encontre el objeto IHTMLDocument2, sospecho que no existe equivalente aun o por lo menos no se como se llama. Para agregarla solo puse la referencia como si fuera a hacer un proyecto con el ActiveX de IExplorer. De ahí en fuera, el de Mozilla hace lo demas y entiende los resultados dados por la manipulación de este objeto.

Modifique la aplicación que publique anoche para agregar esta funcionalidad y hacer pruebas. Para usarla hay que enviar como parametros por linea de comando la dirección de la página, el pedazo de código que se desea remplazar y luego el texto que remplazará al parametro anterior.

El código fuente
El ejecutable de prueba

Bueno, y eso es lo que hice con el Mozilla.

Nov 03 2008

Un motivo más para evitar Internet Explorer

Buenas noches mis cinco lectores. Disculparan ustedes que los aburra de nuevo con una entrada de programación. Hagan una flamewar de comentarios fanboys si se aburren.

Resulta que como ya comente antes cree un explorador web personalizado para automatizar unos procesos de unas páginas web que son repetitivos y muy comunes para que me paguen a mi en lugar de a un mono que haga esos rutinarios trabajos. Para hacer esto, inclui en mi aplicación un control ActiveX WebBrowser para manejar el nucleo de Internet Explorer (en este caso el la versión 6) dentro de un formulario, y más importante poder manipular los eventos del explorador (especialmente importante para mi caso es el evento DocumentComplete) y manipular el DOM de las páginas cargadas en mi aplicación para controlar los procesos y manipular su contenido a mi conveniencia. Era lo que estaba a la mano en lugar de construir algo más sofisticado con APIs y a más bajo nivel como uno similar que tenemos en C++ pero es más complejo de entender y dificil de adaptar a casos rebuscados.

El pero esta en que resulta que IExplorer realiza muchas conexiones para abrir la página y el servidor web lo reciente y si la cantidad de trabajo es muy masiva las salidas se saturan. En cambio FireFox para hacer lo mismo solo necesita menos de una cuarta parte del total de conexiones. ¿Qué hace IE con tantas conexiones? ¿Le envia reportes a Redmond de lo que hacemos? No se, pero para 1′000 procesos el IE realiza 30′000 conexiones o más al servidor web mientras que FireFox apenas no anda sobre las 5′200 o menos para las mismas tareas.

¿Es posible usar a FireFox dentro de la aplicación?

Pues si, encontre una manera muy sencilla de hacerlo recurriendo a un ActiveX preparado para cumplir la función del ActiveX de IE ya existente. Lo he probado y en lo general funciona. Advierto de una vez que no es un substituto 1:1 totalmente funcional. Hay diferencias como las hay entre IE y FireFox, ademas de que se nota que le falta trabajo en las cosas más dificiles de implementar. Es decir, para navegación esta bien, pero por lo pronto para manipulación del DOM no he obtenido resultados ya sea por mi desconocimiento de equivalencias o porque estos metodos no estan desarrollados aun o tal vez nunca. Debido a estos inconvenientes es que la investigación continua, pero por lo pronto ya tengo algo interesante. Un WebBrowser personalizado usando el motor de Mozilla, lo cual me parece interesante a mi porque combinando el motor de Mozilla en lugar de IE, usando MONO en lugar de NetFramework y GTK# en lugar de Windows forms podria desarrollar las versiones futuras de modo que sean multiplataforma por lo menos para curiosidad mia. Ademas, entre SharpDevelop que estoy usando ultimamente y MonoDevelop no hay muchas diferencias porque son, como la versión multiplataforma de un IDE común.

FireFox, GTK, MONO

¿Combinación adecuada?

Ahora a lo que te truje. El ActiveX para manejar el motor de Mozilla lo encontre en esta página, desde donde pueden descargar la instalación, que no es otra cosa que la parte indispensable de Gecko (el motor de renderizado) y un registrador de .dll para que puedas usarlas desde los IDE de programación como un control más. Yo estoy usando la versión “Mozilla ActiveX Control 1.7.12” en combinación con FireFox 3. Para integrarlas a Visual estudio solo tuve que agregar una herramienta más en la pestaña de componentes de la caja de herramientas, buscar la .dll entre los componentes COM (previamente he corrido el instalador, claro) y ¡Voila! Ya tengo el control en mi toolbox para agregarlo solo con click y arrastre como cualquier caja de texto. También se pueden integrar a otros IDE para otros lenguajes no .NET y en la página arriba referenciada hay instrucciones para Visual Studio, C++ por código, Delphi y Visual Basic 6.

Si optas por construir desde cero, solo hay que manejar el control y sus eventos como a cualquier otro control de Visual Studio, y es identico en casi todo al de Microsoft incluido en mshtml.dll, hasta se agregan igual a la barra de herramientas, y por eso en aplicaciones existentes basta con borrar los controles de IE, poner en su lugar uno nuevo de FireFox y ponerle el mismo nombre que el anterior, aunque en este caso necesite regenerar los eventos manualmente; y claro. También las referencias a los espacios de nombres AxMOZILLACONTROLLib y MOZILLACONTROLLib

El manejo de este ActiveX es muy similar al de IExplorer, por ejemplo. La declaración del control:

// Declarar el control ActiveXWebBrowser para IExplorer
AxSHDocVw.AxWebBrowser browser = new AxSHDocVw.AxWebBrowser()

// Declarar el control ActiveXWebBrowser para Mozilla
AxMOZILLACONTROLLib.AxMozillaBrowser browser = new AxMOZILLACONTROLLib.AxMozillaBrowser()

Lo unico que cambia es el nombre del objeto y la .dll referenciada. Pero que eso no te engañe, aunque a primera vista y aunque el debuger no te arroje errores, en el evento .Navigate hay un detalle.

// Abrir la página de NGA
browser.Navigate(“http://gamersla.net/”, ref objeto, ref objeto, ref objeto, ref objeto);

En este caso, para IE dos de ellos son tipo object con valor cero y dos son cadenas de caracteres (string) vacias. En cambio, al tratar de usarlo así para Mozilla, solo obtenia ventanas en blanco. Para que el FireFox funcionara tuve que enviar cuatro null de tipo object y entonces si trabajo bien.

ActiveX Mozilla Firefox

Aplicación que utiliza el ActiveX para usar a Mozilla

Construi para hacer pruebas una aplicación sencilla, no es más que una ventana WindowsForm con el control ActiveX para usar a Mozilla donde se abre una página dada como parametro por linea de comando. Les comparto:

El código fuente
El ejecutable de prueba

Ya puedo abrir las páginas, ahora lo que toca es tratar de controlar el DOM y poder hacer cambios y autosubmit en la página controlando los eventos del browser.

Oct 25 2008

Capturando una imagen desde una página web

Sharp Develop

Sharp Develop

Buenas noches mis queridos cinco lectores. Como ya alguna vez he comentado antes, una parte de mi trabajo que me gusta es la investigación y desarrollo de soluciones nuevas. Soy programador, así que eso implica el desarrollo de sistemas o programas que hagan cosas que ninguno de los que ya tenemos desarrollados hacen.

Esto me gusta porque es muy interesante e incluso hasta si el objetivo no es alcanzado (cosa que no recuerdo que halla pasado, solo se tarda más) siempre aprendo algo, y por otro lado, al ser una cosa novedosa, no hay una medida fiable de los tiempos por lo que este tipo de desarrollos no me imponen un cronograma de trabajo y entregas que cumplir a rajatabla. Estará cuando este. También tiene la ventaja de que “estará cuando este” no le gusta a los jefes, así que me dan recursos y facilidades para acelerar el trabajo: internet libre y que yo no atienda llamadas si no es indispensable.

Bueno, basta de charla introductoria y vamos a lo que te truje… ¿Y de qué nos vas a hablar ahora Gnoblis? Pues de la investigación de turno que me ha absorbido los últimos días y que da titulo a esta entrada.

LA SITUACIÓN

La situación es esta; necesito hacer un repetitivo tramite en una página que implica el llenado de un formulario en una página web que no esta bajo mi control por pertenecer a un tercero, entonces le hago llegar la información a un web browser personalizado que hice basándome en el control ActiveX WebBrowser para controlar el motor de Internet Explorer (esto correrá sobre un Windows XP, es seguro que tenga IE), y así controlo automáticamente todo el proceso. La novedad es que introdujeron en la página una de esas imágenes de códigos generadas al azar con texto muy ofuscado para que no lo entienda ningún proceso automático de reconocimiento de caracteres.

El plan es poder obtener la imagen que el usuario debe reconocer, enviársela y obtener lo que el usuario tecleo. Para obtener la imagen probé varias cosas. La primera fue apoyarme con una aplicación que desarrolle para tomar una captura de pantalla automáticamente y con la capacidad de tomar todo el escritorio o recortar un recuadro indicado como parámetros. Lo primero fue solo incluir una llamada a esta aplicación a la que bautice ScreenShoter y de la que tal vez les cuente en otra ocasión. Pues bien, funciono pero solo es un uso temporal mientras se desarrolla la solución definitiva porque tiene dos graves inconvenientes.

- El explorador debe estar al frente al momento de la captura de la imagen.
- No funciona si se cierra la sesión, es decir, requiere un monitor al servidor siempre.

Debido a esto se requiere un diseño en la aplicación que permita la convivencia de múltiples instancias del programa de manera que tomen turnos para tomar el foco y ponerse al frente, y no se puede trabajar en esa PC porque si atraviesas algo frente a los exploradores la imagen que deseamos no saldrá. Por eso, esto fue un parche temporal para mantener la producción mientras al investigación real seguía.

Tenemos algunas limitantes, como el tiempo de desarrollo. Necesitamos una solución razonablemente rápida. También el uso de C# con el Framework 1.1 debido a que la plataforma de desarrollo de la empresa es Visual Studio 2003 y porque la aplicación base (el explorador personalizado) esta desarrollada con esto, y esta funcionalidad solo es un agregado al programa ya existente.

No se ha avanzado a un framework mayor porque las aplicaciones se distribuyen a muchísimas terminales y enviarlas requeriría la instalación de ese framework a todas las terminales, cosa que cansa solo de pensarlo. Se hará algún día pero hoy no.

INTENTOS QUE FRACASARON MISERABLEMENTE

Un compañero intento realizar una aplicación para captura de imagen de ventanas utilizando C++ y el control de PIXEL para tomar directamente la imagen desde la memoria de vídeo, a un nivel más bajo que el actual ScreenShoter. Esta aplicación recibiría por parámetro el Handler de la ventana a la que queríamos tomar la imagen y así se controlaba el uso de múltiples instancias o que la ventana escogida no tuviera el foco. Y funciono hasta que se cerraba la sesión, a partir de ahí solo obtenemos recuadros negros. Al parecer si no hay sesión abierta, esta memoria no se utiliza, así que este método seguía necesitando un monitor y una sesión abierta.

Al tiempo que pasaba eso yo empecé a desarrollar un método para obtener la imagen directamente del axWebBrowser de la misma aplicación que controla la imagen.

Usando el objeto IHTMLElementRender y su metodo DrawToDC se puede pasar a una imagen de mapa de bits la figura de un elemento de la página abierta en el ActiveX WebBrowser o en una instancia activa de Internet Explorer (mediante handlers). Aunque en realidad solo logre hacerlo exitosamente en C++, donde puedes montar el mismo control AxWebBrowser usando MFC o API para crear ventanas pero esa es otra historia y no era el caso ponerse a reescribir la aplicación entera a C++ solo porque yo no encontré el modo de hacerlo funcionar en C#, que se debe de poder si investigo más a fondo pero no hay tiempo.

// Obtener el contenido de la página cargada en el explorador
IHTMLDocument2 doc = (IHTMLDocument2)axWebBrowser.Document;
IHTMLElement body = (IHTMLElement)doc.body;
IHTMLElementRender render = (IHTMLElementRender)body;

El código anterior copia el cuerpo de la página a un objeto IHTMLElementRender, que usaremos para dibujar el objeto en una imagen de mapa de bits que será igual a lo que se ve en el IExplorer. No es necesario usar el render para toda la página, pueden manejarse objetos individuales como en mi caso que solo quiero una imagen en especial. Por ejemplo, puedes localizar un elemento por su posición en la página si conoces sus coordenadas invocando a IHTMLDocument2.elementFromPoint(int x, int y); donde X y Y son las coordenadas. También esta IHTMLDocument2.images para obtener la colección de imágenes en la página y ya de ahí tomar la necesaria. Yo use esto ultimo porque así no me afecta la resolución de la maquina donde ponga la aplicación. Hay otras formas de obtener elementos específicos y ya no profundizare en ese aspecto.

El metodo DrawToDC para dibujar el control necesita un apuntador a una imagen de mapa de bits donde guardar la imagen que dibujará, así que además del elemento cargado en el render, también necesitamos un apuntador, una imagen de mapa de bits y un objeto Graphics.

// Imagen de mapa de bits que contendra el elemento dibujado
Bitmap imagen = new Bitmap(ancho, alto);
Graphics grafico = Graphics.FromImage(imagen); // Ligar el Bitmap con el objeto Graphics
IntPtr hdc = grafico.GetHdc(); // Crear el apuntador al objeto grafico

Hasta aquí todo va muy bien, pero todos sabemos que cuando algo va tan bien es porque hay algo muy malo que no viste, y en este caso el punto esta en que el parámetro que recibe el metodo DrawToDC del render no es un puntero cualquiera, no, que va. Es un objeto del tipo mshtml._RemotableHandle que nunca supe bien como manejar. Si envías un puntero normal a DrawToDC, este simplemente marca error por parametro de tipo incorrecto. Intente crear una instancia de mshtml._RemotableHandle y pasar el valor del puntero común a este, pero a pesar de que ya no marcaba error, en realidad nunca logre resultados exitosos.

// Inicializar un objeto _RemotableHandle
mshtml._RemotableHandle manejador = new _RemotableHandle;
manejador.fContext = 0×48746457;
// Esto depende de tu sistema operativo y procesador si es de 32 o 64 bits
manejador.u.hInproc = grafico.GetHdc().ToInt32();
render.DrawToDC(ref manejador);

En C++ no tienes estos problemas porque puedes generar el puntero con la instrucción CreateCompatibleDC que funcionan para DrawToDC, así que me plantee agregar la DLL gdi32.dll como referencia a mi proyecto para utilizar el método como en C++ pero otra cosa que probé antes me detuvo. Procedí a sobrecargar el método DrawToDC para que aceptara punteros comunes. Agregue el siguiente código al principio de mi clase

[Guid("3050f669-98b5-11cf-bb82-00aa00bdce0b"), InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown),
ComVisible(true),ComImport]
interface IHTMLElementRender
{
void DrawToDC([In] IntPtr hDC);
void SetDocumentPrinter([In, MarshalAs(UnmanagedType.BStr)] string bstrPrinterName, [In] IntPtr hDC);
};

Ya con esto podemos enviar variables IntPtr al metodo DrawToDC del objeto render como algo normal.

// Guardar la página en la imagen bitmap
IHTMLDocument2 doc = (IHTMLDocument2)axWebBrowser.Document;
IHTMLElement body = (IHTMLElement)doc.body;
IHTMLElementRender render = (IHTMLElementRender)body;
Bitmap imagen = new Bitmap(ancho, alto);
Graphics grafico = Graphics.FromImage(imagen);
IntPtr hdc = grafico.GetHdc();
// Liberar memoria que ya no necesitamos
grafico.ReleaseHdc(hdc);
grafico.Dispose();
// Guardar la imagen en el disco duro como .jpg
imagen.Save(“c:\\captura_de_pantalla.jpg”, ImageFormat.Jpeg);

En la bella teoría funciona, pero ya en la practica, al igual que la primera aplicación, al cerrar la sesión solo salva recuadros negros. Hora de aplicar el plan C… solo que en ese momento me habría gustado tener más claro cual era ese plan.

EXITO

Después de investigar un rato dí con el método DrawToBitmap. Esta instrucción es capaz de dibujar un control de una ventana como una imagen de mapa de bits, solo que no existe en el NetFramework 1.1, pues apareció con la versión 2.0

En fin, me la jugue. Para poder hacer el desarrollo instale SHARP DEVELOP 2.2, un IDE de programación .NET opensource gratuito. Hice una copia del proyecto, la cargue con Sharp Develop y lo actualice al nuevo framework y me puse a implementar el cambio.

// Declarar una imagen del tamaño del explorador
Rectangle area = new Rectangle(0, 0, axWebBrowser.Width, axWebBrowser.Height);
Bitmap imagen = new Bitmap(axWebBrowser.Width, axWebBrowser.Height);
// Obtener la imagen de la página cargada en el explorador
axWebBrowser.DrawToBitmap(imagen, area);

¡Y voila!, con eso tenemos la imagen de la parte visible de la página. Según investigue, para obtener toda la página y no solo la parte visible esta la posibilidad de usar un método sobrecargado para inicializar el objeto Rectangle para tomar toda la página.

Rectangle area = new Rectangle(0, 0, axWebBrowser.Document.ActiveElement.ScrollRectangle);

Lo intente pero siempre me dio una excepción “‘object’ does not contain a definition for ‘ActiveElement’ (CS0117)” y como yo no necesito toda la página pues no me puse a buscar a fondo porque ni como usarlo, pero ahí queda para que quien necesite una imagen de toda la página completa pueda ver si le sirve como punto de partida.

Bueno, como ya había mencionado, yo no quería toda la página, solo una parte. Para obtener la parte que quiero hice lo siguiente.

// Imagen que guardara el recuadro que me interesa
Bitmap recorte = new Bitmap(ancho, alto, PixelFormat.Format24bppRgb);
// Área de la imagen que quiero obtener
area = new Rectangle(x, y, ancho, alto);
// Tomar este recuadro de la imagen original completa
recorte = imagen.Clone(area, PixelFormat.Format24bppRgb);
// Guardar la imagen en el disco duro como .jpg
recorte.Save(“c:\\imagen.jpg”, ImageFormat.Jpeg);

La función Bitmap.Clone() me permite obtener una copia de una imagen, ya sea completa o solo de las coordenadas y tamaño de un rectángulo indicado. También use en esta aplicación el parámetro PixelFormat.Format24bppRgb porque la aplicación que hará uso de estas imagenes solo soporta esa compresión como máxima calidad. El poder dar formato desde aquí nos ahorro un paso porque el ScreenShoter toma las imágenes a 32 bpp y usábamos una instrucción de ImageMagick para convertirlas antes de que la aplicación final empezará a trabajar con las imágenes.

Y bueno, mis cinco lectores. Perdón por aburrirlos pero esta semana eso hice en el trabajo y queria publicar lo que encontré por si a alguien más le sirve, pues yo también busque en páginas durante la semana, todo la investigación y desarrollo termina expresado como unas lineas de código.