XNA y Windows Forms
Ya tenemos un nuevo tutorial en XNACommunity gentileza de Javier Cantó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:

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:

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:


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:

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.

El código de ejemplo lo podeis descargar de aquí.
Saludetes 