Simular teclado y ratón en aplicaciones de terceros


English version here.

La simulación de una pulsación de una combinación de teclas o de una pulsación del ratón en una aplicación propia es relativamente sencillo: basta con hacer uso del método SendKeys para que nuestra aplicación reciba datos desde teclado y la invocación explícita del método adecuado que esté asociado al evento del ratón para simular un click del ratón.

Sin embargo, si deseamos crear un programa cuya misión consista en realizar estas operaciones en un programa de terceros, deberemos hacer uso de código no administrado.

Código administrado y no administrado.

El código administrado es aquel que se ejecuta bajo el control del CLR (Common Language Runtime), es decir, aquel código escrito en .NET que es compilado a un código intermedio MSIL o CIL (dependiendo de la versión del Framework) y que en tiempo de ejecución es transformado en código nativo. Este es el proceso natural de la programación .NET, con el que supongo que todos o casi todos estamos familiarizados.

El código no administrado, en cambio, es aquel código ajeno a este ciclo de vida, tal como el incluido en componentes COM/COM+, C++, ActiveX o la propia API de Windows (que es donde centraremos el objetivo de este artículo).

Windows expone su API a través de un conjunto de bibliotecas de enlace dinámico (dll) tales como user32.dll (permite manejar ventanas y sus eventos), shell32.dll (procesos), winspool.drv (impresión)… Para hacer uso del teclado y el ratón en aplicaciones de terceros nos centraremos en la biblioteca user32.dll, que como podemos imaginar, se tratará como código no administrado.

Utilizando código no administrado

Para utilizar un método perteneciente a una biblioteca cuyo código sea no administrado es necesario realizar dos operaciones:

  • Declarar el método con la misma firma que el método original (es decir, declararlo tal y como se creó originalmente en la dll) agregándole el atributo extern. Este modificador indicará al compilador que el método se implementa de forma externa. Para indicar dónde se implementa se utiliza el siguiente paso.
  • Decorar mediante el atributo DllImport el nombre del fichero dll que implementa el método.

Así, si quisiésemos utilizar la función FindWindow, que proporciona un puntero a una ventana concreta y que está expuesta en el fichero user32.dll, escribiríamos el siguiente código:


        [DllImport("user32.dll")]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

El fichero user32.dll contiene, como explicamos previamente, la implementación de los métodos de la API de Windows encargados de interactuar con ventanas y sus eventos. Para un listado pormenorizado de los métodos que expone esta biblioteca, basta con visitar este enlace.

De este modo, si quisiésemos obtener un puntero a una ventana de la cual conocemos su clase y/o nombre, bastaría con invocar el método de la siguiente forma:


        IntPtr handle;
        string className = System.Configuration.ConfigurationManager.AppSettings["class"];
        string windowName = System.Configuration.ConfigurationManager.AppSettings["window"];

        handle = FindWindow(className, windowName);

        if (handle == IntPtr.Zero)
        {
            MessageBox.Show("Ventana no encontrada.");
            return;
        }

 

En el código anterior asumimos que el nombre de la clase y de la ventana (ambas cadenas de texto) se encuentran codificados como parámetros en el app.config. Sin embargo, seguramente en este momento nos estemos preguntando un pequeño detalle: ¿de dónde obtenemos el nombre de la clase y de la ventana? Si bien existen métodos en la API para realizar esta exploración, haremos uso de una de las herramientas incluidas con Visual Studio: Spy++.

Spy++

Esta herramienta suele localizarse dentro de la carpeta Visual Studio Tools, y requiere permisos de administración, ya que se encargará de acceder a todos los metadatos manejados por cada una de las ventanas que se encuentran abiertas dentro de nuestro sistema operativo, incluyendo los mensajes que envían y reciben.


Al lanzar la aplicación nos encontraremos algo como lo siguiente:

Imaginemos que queremos obtener la clase y/o nombre de una ventana en particular. Para realizar esto, seleccionaremos la opción Spy > Find Windows. Esto abrirá una ventana similar a la siguiente:

Si hacemos click en el punto de mira y mantenemos pulsado el botón izquierdo del ratón, esta ventana mostrará información acerca de cualquier ventana que seleccionemos a partir de este momento. Por ejemplo:

Como podemos observar, tenemos una ventana cuya clase es “Notepad++” y su nombre (caption) es “C:\Users\Dani\Desktop\config.ini – Notepad++”.

A partir del nombre de la clase o del de la ventana será posible, mediante el método FindWindow, obtener un puntero a la ventana y de este modo poder realizar operaciones sobre ella.

Importando métodos de la API

Nuestra intención es la de hacer uso de teclado y ratón sobre una ventana concreta. Ya hemos visto cómo obtener un puntero a una ventana. Ahora deberemos averiguar qué hacer con ese puntero. Queremos pasar la ventana seleccionada a primer plano para poder interactuar con ella, para lo cual usaremos el método SetForegroundWindow. Referenciaremos este método de la siguiente forma:


        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        public static extern bool SetForegroundWindow(IntPtr hWnd);

Lo siguiente que necesitaremos será un conjunto de métodos que nos permita interactuar con el ratón. Pero primero deberemos definir un par de constantes que codifiquen ciertos valores que se corresponden con eventos del ratón. Estos valores serán:

  • Mover el ratón.
  • Presión del botón izquierdo del ratón.
  • Levantar el botón izquierdo del ratón.
  • Presión del botón derecho del ratón.
  • Levantar el botón derecho del ratón.
  • Indicar que las coordenadas indicadas son absolutas. En caso de no incluir este valor, los valores X e Y se sumarán a la posición actual del ratón.

Estas constantes están predefinidas, y sus valores son los siguientes:


        private const uint MOUSEEVENTF_MOVE      = 0x0001;
        private const uint MOUSEEVENTF_LEFTDOWN  = 0x0002;
        private const uint MOUSEEVENTF_LEFTUP    = 0x0004;
        private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008;
        private const uint MOUSEEVENTF_RIGHTUP   = 0x0010;
        private const uint MOUSEEVENTF_ABSOLUTE  = 0x8000;

Los métodos que usaremos para hacer uso del ratón serán los siguientes: uno para situar el puntero en una posición determinada (SetCursorPos) y otro para enviar un evento del ratón (mouse_event). Recordemos que los valores anteriores se corresponden con los eventos correspondientes a las pulsaciones de los botones izquierdo y derecho.


        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        static extern bool SetCursorPos(uint x, uint y);

        [DllImport("user32.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
        public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, UIntPtr dwExtraInfo);

Utilizando el ratón

Para usar el ratón en una ventana determinada, bastaría un método similar al siguiente:


        private void performClick(uint x, uint y)
        {
            SetCursorPos(x, y);
            mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_LEFTDOWN, x, y, 0, UIntPtr.Zero);
            Thread.Sleep(200);
            mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_LEFTUP, x, y, 0, UIntPtr.Zero);
        }

El método SetCursorPos(x, y) es similar a escribir lo siguiente:


        private void moveToPos(uint x, uint y)
        {
            mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE, x, y, 0, UIntPtr.Zero);
        }

De este modo, situaríamos el cursor en la posición (x, y), enviaríamos el evento MOUSEEVENTF_LEFT_DOWN (pulsación del botón izquierdo), esperaríamos 200 milisegundos y a continuación enviaríamos el evento MOUSEEVENTF_LEFTUP (soltar el botón izquierdo), que equivaldría a un click del ratón. Un doble click sería similar, haciendo uso de este mismo código:


        private void performDoubleClick(uint x, uint y)
        {
            performClick(x, y);
            Thread.Sleep(400);
            performClick(x, y);
        }

Por lo tanto, el código necesario para realizar doble click sobre el punto 200, 400 de la ventana cuya clase es “Notepad++” sería el siguiente:


            handle = FindWindow("Notepad++", null);

            if (handle == IntPtr.Zero)
            {
                MessageBox.Show("Ventana no encontrada.");
                return;
            }

            SetForegroundWindow(handle);
            performDoubleClick(200, 400);

Escribiendo texto

Simular la escritura de texto a través del teclado es mucho más sencilla: bastará con utilizar la clase SendKeys para enviar texto a la ventana activa. Así, si queremos escribir “Hola mundo”, bastará con realizar lo siguiente:


            handle = FindWindow("Notepad++", null);

            if (handle == IntPtr.Zero)
            {
                MessageBox.Show("Ventana no encontrada.");
                return;
            }

            SetForegroundWindow(handle);
            SendKeys.SendWait("Hola, mundo");

A través de SendKeys es posible enviar también teclas especiales (tabulaciones, retornos de carro…) y combinaciones de teclas (como CTRL+ALT+SUPR). Una referencia a las posibilidades que ofrece esta clase puede consultarse en este enlace.

Por último, podemos consultar una referencia rápida a las tareas comunes que pueden realizarse mediante invocaciones a métodos de user32.dll a través de este enlace. Están pensadas para Visual Basic 6, pero son fácilmente adaptables a cualquier lenguaje .NET.

Anuncios

9 comments

    1. Hola, Eduardo.

      El método se importa de la biblioteca user32.dll, tal y como indicamos al principio de la sección “Importando métodos de la API”:

      [DllImport(“user32.dll”, CharSet = CharSet.Unicode)]
      public static extern bool SetForegroundWindow(IntPtr hWnd);

      Una vez realizada la importación, podremos hacer uso de la función de forma normal.

      Un saludo.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s