This project is read-only.
English Version

XNA y Windows Forms

Introducción

En este tutorial aprenderemos a crear aplicaciones Windows Forms que usen XNA a las que podríamos catalogar como aplicaciones “híbridas”. Este tipo de aplicaciones se están volviendo cada vez más y más común en el mundo de la informática.
Empezaremos comentando que el framework de XNA no viene preparado para que se pueda usar junto con Windows Forms, ya que el graphicsDeviceManager que es el que se encarga de crear la ventana sobre la que pinta XNA y de inicializar el device no nos permite especificar el handle de la superficie sobre la que queremos pintar. Tenemos la opción inicializar nosotros a mano nuestro device, pero estaríamos perdiendo gran parte de la funcionalidad que nos aporta el graphicsDeviceManager, como puede ser el ContentManager. En la página de ejemplos de Microsoft http://creators.xna.com podemos encontrar dos ejemplos en los que utilizan Windows Forms junto con XNA. En el primero llamado WindowsForms Series 1: Graphics Device podemos aprender a crear el device a mano y a poder usarlo para pintar un triangulo:

XNA_WinForms1.jpg

El problema que nos encontramos con este ejemplo es que hemos perdido con ContentManager (no podemos cargar contenido como texturas, modelos, sonidos, etc usando los contentloaders de XNA) y por lo tanto tenemos que crear ese triangulo a mano. Esto nos llevaría a desaprovechar gran parte del Framework de XNA. Sin embargo en el siguiente ejemplo WinForms Serie 2: Content Loading que proponen en dicha web podemos ver como cargan un FBX con textura:

XNA_Winform.jpg

Y si nos vemos el código de dicho ejemplo es mucho más extenso que el del anterior ya que han creado se crean sus propias clases encargadas de hacer unos de ContentBuilder. ¿Y esto en qué consiste?, pues en coger todos los ficheros de content que hay en nuestro proyecto, usar el loader adecuado para compilarlo y generar los ficheros xnb en el directorio de trabajo de nuestra solución.

Personalmente tras ver estas dos propuestas no me convenció ninguna de las dos, ya que en la primera desperdicias el framework y para eso usas directamente DirectX, y en la segunda en la implementación que haces uso del ContentBuilder me parece muy engorroso. Tras investigar un poco encontré una solución parcialmente buena, su principal característica es que es bastante sencilla de implementar y puedes seguir usando el ContentManager por defecto.

Pongámonos manos a la obra para explicar cómo sería este nuevo método, bien lo primero que tenemos que hacer es crear un nuevo proyecto XNA, el cual si lo ejecutamos pues ya nos creará una ventana en donde pintará con XNA. Pero nosotros no queremos pintar en esa ventana, nosotros queremos crear por ejemplo un editor en donde lo que necesitamos es que XNA pinte sobre un panel de nuestra ventana. Por ello lo primero que vamos a hacer es añadir un formulario a nuestra solución y como una imagen vale más que mil palabras:

Image1.jpg
Image2.jpg

Y ahora le añadiremos el aspecto de un pequeño editor con un menuStrip, un propertyGrid a la izq y un panel a la derecha separados por un splitter para luego poder redimensionar el espacio, todo esto quedaría de la siguiente forma:

Image3.jpg

Una vez que tenemos esto ahora tenemos que hacer que XNA pinte sobre nuestro panel. Para ello primero nos dirigiremos al código de nuestro formulario y le añadiremos la siguiente propiedad:

public Control Panel
{
   get { return splitContainer.Panel; }
}


Después de hacer esto lo único que nos hace falta saber es que existe una forma de cambiar el handle de la superficie sobre la que pinta el device de XNA, y esta es:

GraphicsDeviceInformation.PresentationParameters.DeviceWindowHandle


Propiedad que intersectaremos justo cuando la cuando el Graphics esté preparando los parámetros para crear el device. Los cambios que vamos a realizar a continuación serán sobre el fichero program.cs y sobre el game.cs.

Empecemos por el program:

    static class Program
    {
        static Game1 game;

        [STAThread]
        static void Main(string[] args)
        {
            Main form = new Main();
            form.Disposed += new EventHandler(form_Disposed);
            using (game = new Game1(form))
            {
                form.Show();
                form.TopMost = true;
                game.Run();
            }
        }

        static void form_Disposed(object sender, EventArgs e)
        {
            game.Exit();
        }
    }


Como se puede ver hemos creado una variable static de tipo Game1, y luego dentro del Main hemos inicializado nuestro formulario. Hemos suscrito un método al enveto Disposed de nuestro formulario ya que vamos a dejar que Game lleve el thread de nuestra aplicación mediante el Game.Run () y por lo tanto tendremos que avisarle de que queremos salir de esta cuando el usuario cierre el formulario.

Luego hemos creado una instancia de nuestra clase Game1 a la que como véis se le pasa el formulario como parámetro, hemos mostrado el formulario, activado la opción TopMost de este y llamado a Game.Run(). La pregunta que todos os podréis hacer aquí es porque hemos activado la opción TopMost, vayamos a explicar esto antes de continuar. El graphicsDeviceManager creará una ventana por defecto al arrancar pero no es esta sobre la que queremos que pinte pero sin embargo dicha ventana se creará y podrán verse las dos ventanas abiertas en nuestra pantalla, para disimular esto vamos a hacer un pequeño truco que consistirá en decirle a nuestro formulario que se cree en pantalla por encima de todas las ventanas de forma que la ventana por defecto quedará escondida detrás de la nuestra, en ese momento ya le habremos cambiado a XNA el handle sobre el que debe pintar y por lo tanto pintará sobre el panel de nuestro formulario. Y como no existe forma de decirle a el graphicsDeviceManager que no cree su ventana por defecto pues una vez que esta reciba el foco del sistema la pondremos invisible mediante mediante la propiedad Visible y restauraremos la propiedad TopMost de nuestro formulario a false.

Si no hacéis este pequeño truco se notará un pequeño parpadeo al arrancar la aplicación que consistirá en la ventana de XNA que se crea y desaparece rápidamente, por este motivo cuando hable de esta solución la califiqué como parcialmente buena pero es la única pega.

Bueno pues una vez que lo tenemos todo claro solo queda escribir código en Game. Para empezar necesitamos un atributo del tipo de nuestro formulario, que inicializaremos en nuestro constructor:

Main form;
public Game1(Main form)
{
   this.form = form;


Una vez que tenemos esto tenemos que realizar tres tareas importantes:
- Cambiar el handle sobre el que pintará XNA.
- Cuando la ventana por defecto reciba el foco hacerla invisible y cambiar la propiedad TopMost de nuestro formulario.
- Y adaptar el device y el aspectRatio de la cámara cada vez que el tamaño del panel del formulario cambia, para no perder las proporciones.

Cambiar el handle sobre el que pintará XNA

La constructor de Game la añadimos la siguiente línea para suscribirnos al evento de preparación de parámetros del device:

graphics.PreparingDeviceSettings += new EventHandler<PreparingDeviceSettingsEventArgs>(graphics_PreparingDeviceSettings);


Luego dentro del método cambiamos el handle sobre el que el device pintará:

void graphics_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
{
    e.GraphicsDeviceInformation.PresentationParameters.DeviceWindowHandle = form.Panel.Handle;
}


Cuando la ventana por defecto reciba el foco hacerla invisible y cambiar la propiedad TopMost de nuestro formulario

En el constructor escribiremos lo siguiente para obtener el puntero a la ventana que XNA crea por defecto:

Form xnaWindow = (Form)Control.FromHandle((this.Window.Handle));


Y después nos suscribiremos al método GotFocus un método que se encargará de poner la ventana invisible y de volver a cambiar la propidad TopMost de nuestro formulario:

xnaWindow.GotFocus += new EventHandler(xnaWindow_GotFocus);
void xnaWindow_GotFocus(object sender, EventArgs e)
{
    ((Form)sender).Visible = false;
    form.TopMost = false;
}


Y adaptar el device y el aspectRatio de la cámara cada vez que el tamaño del panel del formulario cambia, para no perder las proporciones

En el constructor añadimos también la suscripción al evento Resize del panel, para después cambiar las propiedades de tamaño del device y las del aspectRatio de la cámara:

form.Panel.Resize += new EventHandler(Panel_Resize);
void Panel_Resize(object sender, EventArgs e)
{
   graphics.PreferredBackBufferWidth = form.Panel.Width;
   graphics.PreferredBackBufferHeight = form.Panel.Height;
   aspectRatio = (float)form.Panel.Width / form.Panel.Height;
   graphics.ApplyChanges();
}


Bien pues ya está todo implementado para hacer uso de XNA y su ContentManager para crear aplicaciones hibridas junto con Windows Forms, aquí os dejo un pequeño ejemplo en donde se pone en práctica todo lo explicado.

Image4.jpg


Ejemplo

Editor31.rar (XGS 3.1)

Editor.rar (XGS 2.0)



Created by Javier Cantón Ferrero.
MVP XNA-DirectX 2007/2009
MSP 2006/2008
Date 06/09/2008
Web www.codeplex.com/XNACommunity
Email javiuniversidad@gmail.com
blog: mirageproject.blogspot.com

Last edited Aug 31, 2009 at 11:34 AM by khronos, version 5

Comments

bradpako Sep 19, 2012 at 4:42 PM 
Hola,
me parece muy interesante el sistema y lo estoy intentando utilizar con XNA 4.0

Todo va bien hasta que cierro el formulario, va a el evento Disposing pero parece ser que el game.exit() no funciona, por que el programa sigue en ejecución, pero sin formulario :s

¿Alguna solución?

Gracias.