Tag Archive: C#

Dec
24

Migrando páginas web ASP.NET de Windows a Linux

¿Recuerdan una entrada titulada “Migrando una página web ASP.NET de Windows a Linux“? Esta entrada es como una continuación de aquella así que si esta interesado en el tema tal vez prefiera leer esa primero a modo de antecedentes comenzare por relatar que tuve que migrar varias páginas web hechas en ASP.NET en Windows con NetFramework y servidas mediante IIS hacia servidores Linux con Mono y servirlas mediante Apache + Mod mono.

En el principio del proyecto original hace años se intento pero Apache pero en ese entonces no podía servir páginas ASP.NET por si mismo (aun no puede) y no existía algún complemento como Mod mono y aunque Mono tenia su propio servidor llamado XSP este no era lo suficientemente estable para soportar la carga de trabajo de una empresa como esta, de hecho aun no lo es todavia, y por eso ahora usamos Mod Mono.

Usando SharpDevelop en lugar de Visual Studio para probar las aplicaciones con Mono y XSP previamente a probar en los servidores Linux se pudo afinar las instrucciones y entonces si pasar a ambientes más reales donde solo que hubo que ajustar algunas características de seguridad que resultaron más altas que en Windows y entonces vino el problema. Parecía no haber poder humano que hiciera que los caracteres especiales, ñ y letras acentuadas se vieran correctamente.

La base de datos. Muchos trabajamos con bases de datos PostGreSQL y estas suelen ser creadas por default (desconozco si en algunas versiones pone alguna codificación por default más avanzada) con codificación (encoding) SQL_ASCII, que según la documentación de PostGreSQL con SQL_ASCII el servidor interpretara los valores 0 al 127 del estándar ASCII, así que más que una declaración de que juego de caracteres usar es una orden de ignorar la codificación lo que puede causar problemas cuando se usen caracteres no ASCII como letras acentuadas, la ñ o símbolos. Debido a esto de ser posible hay que tener cuidado de definir desde el principio la base de datos con un juego de caracteres, de preferencia UTF8, que es UNICODE porque es apto para todos los idiomas, y esto es importante independientemente del tipo de tecnología de su base de datos que ustedes utilicen.

Seguramente su manejador de base de datos tendrá alguna opción para consultar el juego de caracteres de su base de datos pero si no lo encuentran, en PostGreSQL pueden ver la codificación de su base de datos con la siguiente instrucción:

SELECT encoding FROM pg_database where datname='[nombre de la base de datos]';

Esto arrojara un número que puede traducirse así, por poner ejemplos de encodings comunes, aunque esta tabla es solo una muestra muy limitada.

0 SQL_ASCII
6 UTF8 (UNICODE)
8 LATIN1
16 LATIN9 (ISO 8859-15)

La codificación de la base de datos también puede hacerse a base de datos ya creadas actualizando (Update) el campo “encoding” de “pg_database” que consultamos con la instrucción anterior pero esto no afecta de inmediato y “automagicamente” a toda la información previamente guardada en las tablas de la base de datos, eso lo tendremos que ajustar por nuestros propios medios. Además de que las conexiones abiertas antes del cambio no se verán afectadas, para que tome efecto deberán ser cerradas y volver a establecer la conexión para que se conecten usando el nuevo juego de caracteres, y dependiendo del tipo de cambio de encoding en las consultas podría haber problemas al tratar de leer caracteres que no existen en un juegos de caracteres pero si en el otro o que en ambos encodings un mismo código represente caracteres diferentes y eso altere nuestra información; son cosas a tomar en cuenta.

Lo mejor, de ser posible es empezar la base de datos con el encoding adecuado y grabarle los datos desde un respaldo, si se pasa de SQL_ASCII a un encoding mayor como UTF8, Latin1 o Latin9 no debería haber problemas… en teoría, en la practica ya sabemos como son las cosas.

La codificación de la base de datos

La codificación de la base de datos

ODBC y la conexión a la base de datos. Aunque los datos estén correctamente guardados en la base de datos estos no vienen solos a nuestras aplicaciones si no que necesitamos conectarnos y la mayoría de las veces lo hacemos usando un ODBC. No creo que halla necesidad de explicar eso pero si de recordar o advertir que hay ODBC ANSI y los hay UNICODE y tengo experiencias de que si consultas una base de datos con caracteres UNICODE usando una conexión ANSI los caracteres especiales no llegan bien sino que llegan símbolos de interrogación (?) y como ya vienen así para nuestras aplicaciones será imposible distinguir que símbolo era originalmente y eso será un gran problema. Los ODBC ANSI funcionan de maravilla con bases de datos SQL_ASCII pero si van a usar una con UNICODE asegúrense de que el ODBC valla a la par.

Además recuerden o sepan que en la cadena de conexión podemos dar indicaciones sobre la codificación de caracteres a utilizar al conectarnos a la base de datos. Así podemos indicarle si queremos que se conecte usando SQL_ASCII, Latin1, UTF8 o lo que sea que nuestra base de datos soporte.

DRIVER={[ODBC]};Server=[IP];Encoding=[Encoding];DATABASE=[DB];UID=[Usuario];PWD=[contraseña];

* Nota extra para desarrolladores en .NET: Ver tutorial para conectarse a bases de datos PostGreSQL sin utilizar ODBC.

Consultando la base de datos. Si no podemos controlar la codificación de caracteres en la base de datos o solo vamos a hacer unos pocos querys ocasionales que no ameriten tanto esfuerzo pero aun así necesitemos el texto consultado a la base de datos en un encoding especifico podemos recurrir a indicarlo dentro de la instrucción de consulta, por ejemplo con la función convert de SQL de la siguiente manera:

SELECT convert( 'texto', 'SQL_ASCII', 'UTF8')

El código anterior convierte el primer parámetro, ‘texto’ de su codificación original a UTF8.

La codificación de caracteres de las páginas web. Los navegadores web traducen el código de la página en algo visible al usuario y el texto también debe ser interpretado y para eso también manejan los estándares de codificación de caracteres que ya mencionamos antes como podemos ver en la imagen a continuación.

La mayoría de los navegadores te dejan elegir un juego de caracteres por default o ellos mismos lo ponen pero también tienen la capacidad de detectar el juego de caracteres usado por la página para mostrar correctamente el contenido ¿Y cómo es que las páginas le dicen al navegador cual es el estándar que deben usar? Usando una cabecera especial en su código fuente.

<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″>

Aquí estamos indicando que el texto en la página es UTF8 y el browser así lo interpretará así que tómenlo en cuenta.

* Nota extra para desarrolladores en .NET: En el archivo Web.config de nuestras aplicaciones web podemos indicar explicitamente más detalles sobre el idioma de nuestra página agregando las siguientes configuraciones que ajustan el contenido de la página al formato regional del idioma y país además de en los caracteres especiales también en cosas como las fechas, los signos y formatos de moneda y cosas así.

<globalization
requestEncoding=”utf-8″
responseEncoding=”utf-8″
fileEncoding=”utf-8″
uiCulture=”es-MX”
culture=”es-MX”
/>

Con lo anterior debería de bastar pero si además desean indicarle al compilador cual es el encoding que debe reconocer cuando genere su dll o ejecutable pueden hacerlo así, aunque en lo personal a mi me parece ya exagerar un poquito pero no esta demás eliminar los posibles cabos sueltos ¿verdad?

<compilation defaultLanguage=”c#” debug=”true”>
<compilers>
<compiler language=”cs;csharp”
extension=”.cs”
type=”Microsoft.CSharp.CSharpCodeProvider,System”
compilerOptions=”/codepage:utf8″ />
</compilers>
</compilation>

Y hablando de no dejar cabos sueltos, queda un ultimo punto que comentar que si bien no es exactamente el mismo tema de conservar información compatible entre sistemas y plataformas si puede ser un complemento para la parte visual de nuestras aplicaciones web.

HTML. El HTML no es un lenguaje de programación si no uno más limitado para la estructuración y formato de las páginas web donde indicamos donde va cada elemento en pantalla y como se debe de ver, y tiene sus limitaciones. Para HTML la ñ o la á no existen pero si en el código de tu página…

Para ver esto en pantalla… debes poner
á &aacute;
é &eacute;
í &iacute;
ó &oacute;
ú &uacute;
Á &Aacute;
É &Eacute;
Í &Iacute;
Ó &Oacute;
Ú &Uacute;
ñ &ntilde;
Ñ &Ntilde;
ü &uuml;
Ü &Uuml;

Claro, los caracteres arriba mencionados solo son unos pocos, y cambiarlos todos manualmente es tedioso pero podríamos hacer funciones para automatizar el cambio o mejor aún, aprovechar funciones ya incluidas en los lenguajes de programación, por ejemplo en C# tenemos la función HtmlEncode que reemplaza en una cadena de caracteres los caracteres que tienen algún equivalente en código HTML de modo que la cadena resultante donde se han reemplazado sea aceptada y bien interpretada por los navegadores, hasta cambia los saltos de linea por su código HTML.

Server.HtmlEncode(string);

En caso de usar PHP también tenemos un par de funciones que pueden hacer un trabajo similar. htmlspecialchars(string) y htmlentities(string) son algo “parecido pero diferente”, pues estas funciones convierten ciertos caracteres que tienen un significado especial en HTML y deben ser representados por entidades HTML si se desea preservar su significado para que se muestren así en pantalla en lugar de ser tomados por código fuente, descuadren la página y no se vean. Estas funciones devuelven una cadena de caracteres con algunas de estas conversiones realizadas. htmlspecialchars solo afecta a los caracteres más útiles para la programación web.

Las traducciones realizadas son:

  • ‘&’ (ampersand) se convierte en ‘&amp;’
  • ‘”‘ (comillas dobles) se convierten en ‘&quot;’
  • “‘” (comilla simple) se convierte en ”’
  • ‘<’ (menor que) se convierte en ‘&lt;’
  • ‘>’ (mayor que) se convierte en ‘&gt;’

En cambio se usa htmlentities si se requieren traducir todas las entidades de caracteres HTML. Ambas funciones son útiles para prevenir que texto suministrado por el usuario, un archivo o desde la base de datos contenga código HTML.

¿Algo más? Por ultimo recordarles que si su página además usa JavaScript para mostrar algo en pantalla consideren que también maneja códigos para representar los caracteres especiales y que no son iguales que los de HTML.

Para ver esto en pantalla… debes poner
á á
é é
í í
ó ó
ú ú
ñ ñ

Para que lo tomen en cuenta si de repente en algún control dinámico les aparece un carácter extraño.

Y bueno, con eso finalizamos de revisar esta serie de puntos a considerar al desarrollar una página web lo más universal posible.

Nov
16

Migrando una página web ASP.NET de Windows a Linux

Hola estimad@s lectoras. Aquí esta Gnoblis de nuevo aburriéndolos como ostras con una entrada que se acerca un poco a temas de programación que solo a el interesan como documentación de las cosas que hace y le parece interesante guardar como auto-consulta a futuro. Si a alguien también le sirve y/o interesa es puro daño colateral, así que discúlpenme por no poner un tutorial claro con ejemplos y todos los enlaces.

¿Recuerdan que les dije que la semana pasada fue pesada? Si, pero al menos hubo una cosa interesante. Pasar páginas construidas hace años a servidores Linux. En su tiempo Mono estaba muy verde y se hicieron en Windows pero les llego su hora. Una de las ventajas que se dicen de .NET es su portabilidad semi-artificial porque es dependiente del Proyecto Mono, y sin entrar en detalles sobre las ventajas de otras tecnologías como PHP o JAVA -qué indudablemente se que tienen y he aprovechado en algunos desarrollos, como todo lo que ya esta hecho en PHP y lo combinable que es cuando necesitas controles personalizados- por ejemplo o mantenerse puro e incontaminado de todo contacto con Microsoft y cosas así. Esas son otras historias y aquí solo me dispongo a hablar de una experiencia pasando algo hecho y funcional en lugar de empezar de cero algo nativo o mejor, aunque también me parece justo reconocer que con .NET hicieron algo bastante mejor que con las tecnologías sueltas que tenían antes como ASP o Visual Basic pero eso también es otra historia.

Centrándome en el tema, tenemos sitios web construidos con ASP.NET y C# como base aderezada de JavaScript y hasta algún ActiveX (que solo funcionan en el terrible IE, lo se, pero en un ambiente cerrado, interno y sin Internet puede resultar menos riesgoso) por ahí ¿Cómo habría de comportarse eso corriendo en Linux? Esa era la pregunta de una migración teóricamente posible pero de esas que podemos imaginar que nunca fluyen sin obstáculos pero el caso es que no sean irrazonablemente difíciles.

En Windows el entorno funciona nativamente combinando IIS + NetFramework. En Linux tenemos como opciones a Mono en lugar del NetFramework y en lugar de IIS tenemos a XSP como opción más sencilla pero de menor capacidad así que para probar estuvo bien pero no para confiar en eso para un ambiente productivo real así que la opción fue el siempre confiable servidor Apache complementado con Mod mono.

De Mono ya habré hablado antes en algunas entradas, y mod_mono es un módulo para que Apache pueda servir páginas ASP.NET usando a Mono, y funcional en Linux. No voy a explicar aquí como montar un servidor Apache, supongo que a quien le interese el tema debe manejarse lo suficiente para hacerlo o saber donde buscar… creo. Y además del Apache hay que instalar el módulo de Mono libapache2-mod-mono y la interfaz entre Mono y servidor mono-apache-server.

La página oficial de Mod mono es www.mono-project.com/Mod_mono, hay le buscan en la sección de descargas del sitio.

Bueno, una vez que tenemos montado nuestro server Linux con Apache y Mod mono podemos pasarle nuestra aplicación y empezar a probar, usando de preferencia la versión 2 de ASP.NET.

Antes de pasar la versión de la página compile y revise el código usando SharpDevelop y Mono 2.0 para asegurarme de ajustar lo que necesitara ajustar para mayor seguridad, pues en mi caso encontre que dice que la instrucción “ConfigurationSettings.AppSettings.Get” esta obsoleta. Actualizar eso fue fácil.

Invalid postback or callback argument

Lo que si requirió un poco más de investigación fue este error.

Invalid postback or callback argument. Event validation is enabled using in configuration or in a page. For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them. If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation.

Lo que en lengua común significa que hay una validación de seguridad en el Framework 2 que en 1.1 no existía y que revisa cuando se carga la página y si hubo un cambio sospecha que algo malicioso ocurrió del lado del cliente como un intento de ataque de inyección de código y que por eso los datos estan alterados, y por motivos de seguridad detiene todo y da ese aviso. Lo mejor es corregir el problema en lugar de desactivarlo como esta la alternativa pero en mi caso lo que ocurre es que tengo unos scripts del lado del cliente que cambian el valor de los controles en base a ciertas cosas y eso es lo que disparaba la validación. Así que mi solución paso por poner en la cabecera de la página afectada EnableEventValidation=”false” para que solo en esa página no hiciera la validación.

 <%@ Page EnableEventValidation="false" ... %>

En mi caso lo hice así porque este sistema trabaja en un ambiento interno y cerrado, con scripts controlados por nosotros y en un par de páginas bien identificadas. En caso de requerir que toda la aplicación funciones así es mejor ponerlo en el web.config para todo el proyecto.

<system.web>
	<pages enableEventValidation="false"/>
</system.web>

Bueno, con eso funciono la página excepto por los acentos que en lugar de poner la letra acentuada ponía un cuadrito, pero revisando las configuraciones regionales se arregla.

Oct
06

Saber el número de serie de disco duro SATA con C#

Esto resulto un poco más difícil que la entrada anterior sobre la dirección MAC de la PC, pues hay múltiples ejemplos de código para obtener el número de serie del disco duro pero solo funcionan con IDE y no para SATA y ¿Quien tiene un PC con disco duro IDE en estos días? Yo tengo uno pero no me considero un estándar representativo de la sociedad.

La verdad, aquí hice trampa. Por que parece ser que WMI por si mismo no tiene la capacidad de obtener el número de serie de un disco SATA y al menos yo no encontré una alternativa viable así que utilice un código en C++ para llegar a un nivel más bajo de lo que pude con C# y así obtener la información, unmanaged code o código no administrado le llaman al uso de códigos así.

WMI significa Windows Management Instrumentation y sirve también para obtener otras cosas como el número de serie de la tarjeta madre por ejemplo, trabajando mediante una instancia de ManagementObjectSearcher y consultas tipo SQL para obtener una colección de los objetos Win32_BaseBoard. Para eso necesitamos el espacio de nombres System.Management.

Aquí no vamos a ver este método pero creo que ya le di suficientes palabras clave para que busquen en el Google si les interesa saber más.

Regresando a este código que les decía, la parte de C++ que comento no lo programe yo, si no que lo encontré en The Code Project y lo adapte a mis necesidades. Empecemos con esta parte.

Pueden descargar el código fuente y el programa ejemplo original de aquí para tener la dll DriveInfoEx.dll que vamos a necesitar. En esta dll se tienen los métodos necesarios para obtener la información del disco duro que queremos. Hay que agregarla como referencia a nuestro sistema en C# así como también la dll System.Management.dll, que si no la tienes por default para poder usar el espacio de nombres puedes buscarla en C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ si tienes al menos .NET framework 2.

Les dejo aquí una función de muestra de como aprovechar a DriveInfoEx.dll para obtener el número de serie hasta de discos SATA.

public static string getHardDiscInfo()
{
    #region Variables de uso local

    DriveListEx listado = null;
    string informacion = string.Empty;
    ulong capacidad = 0;
    ulong capacidadReal = 0;

    #endregion

    listado = new DriveListEx(); // Inicializar la lista
    // Obtener la informacion de los discos duros
    listado.Load();

    // Revisar si hay informacion de los discos duros
    if (listado != null && listado.Count > 0)
    {
        // Recorrer la lista de discos duros
        foreach (DriveInfoEx disco in listado)
        {
            // Capacidad del disco redondeada a miles
            capacidad = disco.DriveSize/1000000000;
            // Capacidad del disco calculada a 1024
            capacidadReal = disco.DriveSize/1073741824;

            informacion += "Modelo: " + disco.ModelNumber + "\r\n";
            informacion += "N° Revisión: " + disco.RevisionNumber + "\r\n";
            informacion += "N° de Serie: " + disco.SerialNumber + "\r\n";
            informacion += "Capacidad: " + capacidad + "GB\r\n";
            informacion += "Capacidad Real: " + capacidadReal + "GB\r\n";
            informacion += "Tamaño del Buffer: " + disco.BufferSize + "\r\n";
            informacion += "Tipo: " + disco.DriveType + "\r\n\r\n";
        }
    }
    else
    {
        informacion = "No se encontro información de discos duros";
    }

    return informacion;
}

Oct
06

Obtener la dirección MAC con C#

mac addressHace mucho que no hacia una entrada sobre códigos fuente pero últimamente muy de vez en cuando me piden algunas cosas un poco curiosas para sacar alguna información rápida y en este caso ocupamos sacar el número de serie del disco duro de un montón de PCs y su dirección MAC en la red así que pidieron una aplicación que lo sacará. Dado que son computadoras con Windows use .NET para lograrlo y como me pareció interesante y posiblemente algún día futuro querré explicar como lo hice y se me va a olvidar, procedo a documentar el asunto. Empecemos por el asunto de la MAC address que es el más sencillo de los dos casos.

Para obtener la información de las tarjetas de red necesitamos el espacio de nombres System.Net.NetworkInformation para poder utilizar los métodos GetAllNetworkInterfaces y GetPhysicalAddress para averiguar la información que necesitamos.

A modo de ejemplo dejo la siguiente clase, es muy sencilla pero no se olviden de agregar los espacios de nombres System.Collections y System.Net.NetworkInformation para que funcione.

public static ArrayList getMacAddress()
{
	// Contador para un ciclo
	int i = 0;
	// Colección de direcciones MAC
	ArrayList DireccionesMAC = new ArrayList();
	// Información de las tarjetas de red
	NetworkInterface[] interfaces = null;

	// Obtener todas las interfaces de red de la PC
	interfaces = NetworkInterface.GetAllNetworkInterfaces();

	// Validar la cantidad de tarjetas de red que tiene
	if (interfaces != null && interfaces.Length > 0)
	{
		// Recorrer todas las interfaces de red
		foreach (NetworkInterface adaptador in interfaces)
		{
			// Obtener la dirección fisica
			PhysicalAddress direccion = adaptador.GetPhysicalAddress();
			// Obtener en modo de arreglo de bytes la dirección
			byte[] bytes = direccion.GetAddressBytes();
			// Variable que tendra la dirección visible
			string mac_address = string.Empty;

			// Recorrer todos los bytes de la direccion
			for (i = 0; i < bytes.Length; i++)
			{
				// Pasar el byte a un formato legible para el usuario
				mac_address += bytes[i].ToString("X2");

				if (i != bytes.Length-1)
				{
					// Agregar un separador, por formato
					mac_address += "-";
				}
			}

			// Agregar la direccion MAC a la lista
			DireccionesMAC.Add(mac_address);
		}
	}

	// Valor de retorno, la lista de direcciones MAC
	return DireccionesMAC;
}

Si además de la MAC necesitamos más datos porque la PC tenga más de una tarjeta de red y queremos diferenciarlas fácilmente podemos recurrir a propiedades extra como Description o NetworkInterfaceType entre otras. En esta entrada mostre una pantalla de mi pequeño ejemplo mostrando información en pantalla. La MAC esta censurada porque ¿Y ustedes para que quieren saber la MAC de esa PC?

Dec
19

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

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

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

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.