Introduction
There are many ways to display 3D scenes based on XNA in a WPF environment. Some suffer from problems of speed (with the use of WindowsFormHost
), others allow only limited interaction with the WPF controls and interfaces. Implementing multiple displays as seen in programs like Maya becomes problematic:
There is a relatively simple way to do this. This way is simply to give a visual impression to the user of a perfect integration of XNA in a WPF widget when it is not. The key to this lies in the perfect handling of windows.
Theory
We want to integrate our XNA scene in a WPF user interface in the same way that we integrate a canvas or any widget. But the best way to view a 3D scene in XNA is to incorporate it in a window. It is impossible to obtain the handle of any WPF control as can be done with a WinForm. The trick then is to rewrite part of the XNA framework revolving around the Game
class. The goal is to inherit a new Game
class from Panel
(in our case, a Canvas
) to be able to include it in the WPF visual tree. The visual bounds of the panel will be the viewing area of the XNA scene. Yet, we have just said that it is not possible to obtain a handle to a visual control that does not inherit from Window
. How do we display 3D with XNA then? We will simply display a window without border just above the panel. This window will always be on top when the application has focus and the panel is visible and will be hidden in this case. Similarly, when the panel is not visible, we will halt the activity of the game.
Each change of size or position of the panel will cause a change equivalent to the window above it.
This window is located exactly above the panel and has the same size: the illusion is perfect.
Implementation
The first step is to rewrite part of the classes in the assemblies Microsoft.Xna.Framework and Microsoft.Xna.Framework.Game. The project Arcane.Xna.Presentation has some classes of these assemblies for use with WPF.
Nothing very complicated. Only the Game
and GameHost
classes are really interesting here.
The Game
class, as stated above, is the display of 3D scenes in WPF user interfaces. It inherits from Canvas
. Using Canvas
meets a particular need that we present below. The Game
class is distinguished from the Game
class in the Microsoft.Xna.Framework.Game assembly by only a few members. First, it has a member type named GameHost
which is the window that is positioned just above. It also has a member _tichGenerator
which will update the display at regular intervals.
The constructor initializes its members as follows:
this._window = new GameHost(this);
this._window.Closed += new EventHandler(_window_Closed);
this._tickGenerator = new DispatcherTimer();
this._tickGenerator.Tick += new EventHandler(_tickGenerator_Tick);
It starts by creating the window to be located above it, and registers the Closed
event to close the 3D scene. The object DispatcherTimer
is used to recreate a loop game by regularly calling the Update
and Draw
methods. Its velocity depends on the property IsFixedTimeStep
.
The last important element, the recording of the event IsVisibleChange
:
this.IsVisibleChanged += new
DependencyPropertyChangedEventHandler(GameCanvas_IsVisibleChanged);
The activation of DispatcherTimer
is based on visibility.
The GameHost
class is simple too. It creates a borderless window not visible in the taskbar, and records the SizeChanged
event of the panel and the XNA LocationChanged
event from the window at the highest level. Both events allow it to always be above the panel XNA by making a call to the method UpdateBounds
:
public void UpdateBounds()
{
if (this.IsVisible)
{
GeneralTransform gt = this.game.TransformToVisual(this.TopLevelWindow);
this.Width = this.game.ActualWidth;
this.Height = this.game.ActualHeight;
this.Left = this.TopLevelWindow.Left + gt.Transform(new Point(0, 0)).X;
this.Top = this.TopLevelWindow.Top + gt.Transform(new Point(0, 0)).Y;
}
}
This method determines the position of the current window by using the top level window (the window containing the panel of the XNA Game
class). It also ensures the same width and height as the panel XNA.
Once again, the window registers on the event IsVisibleChange
if it to be visible or not.
First example
We will base our example on the framework AvalonDock (http://www.codeplex.com/AvalonDock), an effective way to create interfaces to be dockable on Visual Studio easily. Also, a way for us to show the power and simplicity of our system in WPF interfaces.
Our solution contains a project called Demo which corresponds to an example of AvanlonDock, nut slightly modified. We added a class inheriting from Game
that will display a rotating cube on itself.
This class was simply extracted from a pure XNA application to be added in this project, without any change (or almost):
public class RotatingCubeGame : Arcane.Xna.Presentation.Game
{
#region Fields
Arcane.Xna.Presentation.GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
BasicEffect effect;
VertexPositionColor[] vertices;
Vector3 position = Vector3.Zero;
Vector3 size = Vector3.One;
VertexBuffer vertexBuffer;
IndexBuffer indexBuffer;
#endregion
#region Constructors
public RotatingCubeGame()
{
if (!(System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)))
{
graphics = new Arcane.Xna.Presentation.GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
}
#endregion
protected override void Initialize()
{
base.Initialize();
this.graphics.IsFullScreen = false;
this.graphics.PreferredBackBufferWidth = 800;
this.graphics.PreferredBackBufferHeight = 600;
this.graphics.ApplyChanges();
this.Window.Title = "";
this.InitializeVertices();
this.InitializeIndices();
}
private void InitializeVertices()
{
vertices = new VertexPositionColor[8];
vertices[0].Position = new Vector3(-10f, -10f, 10f);
vertices[0].Color = Color.Yellow;
vertices[1].Position = new Vector3(-10f, 10f, 10f);
vertices[1].Color = Color.Green;
vertices[2].Position = new Vector3(10f, 10f, 10f);
vertices[2].Color = Color.Blue;
vertices[3].Position = new Vector3(10f, -10f, 10f);
vertices[3].Color = Color.Black;
vertices[4].Position = new Vector3(10f, 10f, -10f);
vertices[4].Color = Color.Red;
vertices[5].Position = new Vector3(10f, -10f, -10f);
vertices[5].Color = Color.Violet;
vertices[6].Position = new Vector3(-10f, -10f, -10f);
vertices[6].Color = Color.Orange;
vertices[7].Position = new Vector3(-10f, 10f, -10f);
vertices[7].Color = Color.Gray;
this.vertexBuffer = new VertexBuffer(this.graphics.GraphicsDevice,
typeof(VertexPositionColor), 8, BufferUsage.WriteOnly);
this.vertexBuffer.SetData(vertices);
}
private void InitializeIndices()
{
short[] indices = new short[36]{
0,1,2,
0,2,3,
3,2,4,
3,4,5,
5,4,7,
5,7,6,
6,7,1,
6,1,0,
6,0,3,
6,3,5,
1,7,4,
1,4,2};
this.indexBuffer = new IndexBuffer(this.graphics.GraphicsDevice,
typeof(short), 36, BufferUsage.WriteOnly);
this.indexBuffer.SetData(indices);
}
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
this.effect = new BasicEffect(graphics.GraphicsDevice, null);
this.effect.View = (Matrix.CreateLookAt(new Vector3(20, 30, -50),
Vector3.Zero, Vector3.Up));
this.effect.Projection = (Matrix.CreatePerspectiveFieldOfView(
MathHelper.PiOver4, this.GraphicsDevice.Viewport.AspectRatio, 0.1f, 100f));
this.effect.VertexColorEnabled = true;
}
protected override void Update(GameTime gameTime)
{
if (Keyboard.GetState()[Keys.Up] == KeyState.Down)
position += Vector3.Up;
if (Keyboard.GetState()[Keys.Down] == KeyState.Down)
position += Vector3.Down;
if (Keyboard.GetState()[Keys.Left] == KeyState.Down)
position += Vector3.Left;
if (Keyboard.GetState()[Keys.Right] == KeyState.Down)
position += Vector3.Right;
if (Keyboard.GetState()[Keys.PageUp] == KeyState.Down)
size += new Vector3(0.1f, 0.1f, 0.1f);
if (Keyboard.GetState()[Keys.PageDown] == KeyState.Down)
size -= new Vector3(0.1f, 0.1f, 0.1f);
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
float fAngle = (float)gameTime.TotalGameTime.TotalSeconds;
Matrix world = Matrix.CreateRotationY(fAngle) * Matrix.CreateRotationX(fAngle)
* Matrix.CreateScale(size)
* Matrix.CreateTranslation(position);
this.effect.World = (world);
base.Update(gameTime);
}
protected override void Draw(GameTime gameTime)
{
this.graphics.GraphicsDevice.Vertices[0].SetSource(this.vertexBuffer, 0,
VertexPositionColor.SizeInBytes);
this.graphics.GraphicsDevice.Indices = this.indexBuffer;
this.graphics.GraphicsDevice.VertexDeclaration =
new VertexDeclaration(this.graphics.GraphicsDevice,
VertexPositionColor.VertexElements);
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
this.effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
this.graphics.GraphicsDevice.DrawIndexedPrimitives(
PrimitiveType.TriangleList, 0, 0, 8, 0, 12);
pass.End();
}
effect.End();
base.Draw(gameTime);
}
}
Nothing very complicated here for anyone who knows XNA. We just show here a rotating cube. The first change is to inherit the class RotatingCubeGame
from the Game
class of our assembly and not the Game
class of the Microsoft.Xna.Framework.Game assembly. The second modification is to surround the initializations made in the constructor:
System.ComponentModel.DesignerProperties.GetIsInDesignMode(this);
to ensure that our game will not be created as part of the Visual Studio designer. The rest is very simple. We just replace the contents of each DockablePane
in the XAML code in Window1
by:
<Demo:RotatingCubeGame></Demo:RotatingCubeGame>
The result gives us:
Obviously, our system complies with the advantage of allowing AvalonDock docking strong and without disrupting our 3D scene:
Not so bad, but we can do better.
Widgets integration
Why not try to display widgets (Button
, Label
, Grid
, Canvas
, ...) in our 3D scene for a perfect integration with WPF?
We might be tempted to add these items directly to the window GameHost
. But we would have flicker problem (two different types of displays -3D and vector- on the same clip area is not necessarily good ...). We will simply add a new window above the existing window:
Its content will be directly connected to the content of the panel XNA (the Canvas
). The class GameHost
will have a new member named _frontWindow
(which is a Window
). It sets out in the internal property named WPFHost
giving access to the content of this window:
internal object WPFHost
{
get
{
return this._frontWindow.Content;
}
set
{
this._frontWindow.Content = value;
}
}
The Game
class will also expose the content of this window with a property using the same name:
public object WPFHost
{
get
{
if (!(System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)))
return this.Window.WPFHost;
else
return (base.Children[0] as ContentControl).Content;
}
set
{
if (!(System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)))
this.Window.WPFHost = value;
else
(base.Children[0] as ContentControl).Content = value;
}
}
This property determines if we are in Design mode (in Visual Studio) or in runtime mode. In Design mode, we use the Canvas
class which inherits Game
; in runtime mode, we directly target the window. This allows us, in the Visual Studio Designer, to be able to see and modify the UI of our control with the mouse.
In addition, we mark the class attribute of Game
:
[System.Windows.Markup. ContentProperty ( "WPFHost" )]
[System.Windows.Markup. ContentProperty ( "WPFHost")]
We enable direct content in XAML:
Window1.xaml is modified to add more content to the RotatingCubeGame
canvas as shown in the image above. We added pure shapes and paths to reproduce the character orange and yellow, symbolizing XNA, buttons, and labels associated with events, and FlowDocument
with scrolling.
Conclusion
The assembly Arcane.Xna.Presentation presents a simple way to integrate professional XNA applications to WPF. The only real flaws that can be found is the code, realized very quickly due to time and the creation of two windows by XNA game classes. The number of displayable windows under Windows is unfortunately limited. The result still works perfectly, and can be used for professional applications:
Screenshot of my editor world
You can download the latest source code here: http://msmvps.com/cfs-file.ashx/__key/CommunityServer.Blogs.Components.WeblogFiles/ valentin.articles.CoursXna.annexe3/7026.XnaInWpf.zip.