承载 XNA 在窗口中






4.64/5 (6投票s)
2007年7月28日
4分钟阅读

70770
我解决在 WinForm UserControl 中承载 XNA 的旅程
引言
在撰写第一篇关于 XNA 的文章后,我对 XNA 的下一个兴趣是弄清楚如何在 WinForm 中托管一个 XNA 窗口,这样我就可以在该概念中提供额外的控件和其他 UI 元素。这已被证明是一项相当艰巨的任务。
虽然我写了一篇关于我研究的博文,但我认为,即使我没有找到最终的解决方案,为 Code Project 社区提供一篇关于该主题的简短文章也会有所裨益。相反,本文
- 简要描述了问题
- 快速回顾了我尝试过的另外两种解决方案
- 列出了我一路发现的有用的参考文献
最终,我找到了这个网站,它提供了将 XNA 窗口托管为用户控件的 C# 源代码。我在这里不提供下载,因为我没有写它,所以请访问 Nuclex 网站下载。
因此,将此作为 Code Project 文章的目的是将我访问过的所有博客和网站汇总到一篇文章中,以理解问题并找到解决方案。我绝对不是通过搜索“XNA WinForm”来找到解决方案的。希望在几天之内,其他人也能通过这些关键词找到这篇 Code Project 文章!
第一个问题:Game 类构造函数
主要问题是 `Game` 类初始化自己的 `Form`,而不是让您指定您的 Form/Control。这是设计者的一个愚蠢的疏忽。让我们使用 Reflector 来理解这个问题
- `Game` 构造函数调用 `EnsureHost`
- `EnsureHost` 的实现如下:
if (this.host == null) { this.host = new WindowsGameHost(this);
- `WindowsGameHost` 实例化一个 `WindowsGameWindow`
public WindowsGameHost(Game game) { this.game = game; this.LockThreadToProcessor(); this.gameWindow = new WindowsGameWindow();
- 而 `WindowsGameWindow` 最后实例化 `WindowsGameForm`,它继承自 `Form`
public WindowsGameWindow() { this.mainForm = new WindowsGameForm();
这就是第一个问题。
第二个问题:GraphicsDeviceManager CreateDevice 方法
第二个问题是 `GraphicsDeviceManager`。在您的游戏派生的 `Game` 类中,构造函数中会有
graphics = new GraphicsDeviceManager(this);
`GraphicsDeviceManager` 类添加了两个服务
game.Services.AddService(typeof(IGraphicsDeviceManager), this);
game.Services.AddService(typeof(IGraphicsDeviceService), this);
然后,在 `CreateDevice` 方法中,`Game` 构造函数创建的 `Form` 控件被指定为控件宿主
GraphicsDevice device = new GraphicsDevice(
newInfo.Adapter,
newInfo.DeviceType,
this.game.Window.Handle, // <=====
newInfo.CreationOptions,
newInfo.PresentationParameters);
所以问题是双重的——`Game` 类创建了自己的 `Form`,然后该窗口被传递到 `GraphicsDevice` 构造函数。
愚蠢的解决方案 #1:向 Game 窗口添加控件
我遇到的第一个想法是将 `GameWindow` 转换为 `Control` 并向其添加控件。这些控件实际上渲染在 XNA 窗口上,并且适用于按钮等,但对编辑控件无效。我怀疑是因为宿主窗体在消息处理方面存在问题,尽管我从未进一步研究过。
一个更好的解决方案,但不是很好
我找到的第二个解决方法几乎是对的,它挂钩 `PreparingDeviceSettings` 并重写 `DeviceWindowHandle`
public Game1(Form form)
{
graphics = new GraphicsDeviceManager(this);
content = new ContentManager(Services);
this.form = form;
graphics.PreparingDeviceSettings +=
new EventHandler<PreparingDeviceSettingsEventArgs>
(OnPreparingDeviceSettings);
}
void OnPreparingDeviceSettings(object sender,
PreparingDeviceSettingsEventArgs e)
{
e.GraphicsDeviceInformation.PresentationParameters.DeviceWindowHandle =
form.Handle;
}
这(以及我遇到的其他解决方案)都可以正常工作,但 XNA 窗口仍然会显示,需要在 `Draw` 窗口中进行一些技巧来隐藏它
protected override void Draw(GameTime gameTime)
{
if (firstTime)
{
System.Windows.Forms.Control xnaWindow =
System.Windows.Forms.Control.FromHandle((this.Window.Handle));
xnaWindow.Visible = false;
firstTime = false;
}
...
以及正确地处理它
static class Program
{
static Game1 game;
/// <summary>
/// The main entry point for the application.
/// </summary>
static void Main(string[] args)
{
Form form = new Form();
form.Disposed += new EventHandler(OnDisposed);
form.ClientSize = new System.Drawing.Size(800, 600);
form.Show();
using (game = new Game1(form))
{
game.Run();
}
}
static void OnDisposed(object sender, EventArgs e)
{
game.Exit();
}
}
其他实现(实际上非常直接,请参阅参考文献)通常会消除 `Game` 类。这并不是我的目标,因为我想尽可能保留 `Game` 类。这让我得出结论,我可能需要使用 Reflector 来重新创建框架的特定类。此时,我搁置了整个工作,回去处理真正能赚钱的工作。
Nuclex 解决方案
在完成一些计费工作后,我重新审视了这个问题,并在下面参考文献中的一个回复中找到了一个链接,我之前没有检查过,令我惊讶的是,它正是我想要的。事实上,作者确实使用了 Reflector 来重新创建某些类,以便控制上述双重问题。链接在此处,实现非常出色——它是一个用户控件,您可以从中派生自己的类,并像使用 `Game` 类一样使用可重写的方法。我使用 `Nuclex.GameControl` 类在几分钟内就将我的测试应用程序移植了过来。
不过我必须说,我仍然不满意不得不重写框架的主要部分。希望 XNA 框架的架构师将来会修复这个问题,其次,我仍然有一种挥之不去的感觉,肯定有更简单的方法。
参考文献
各种参考文献(我也从评论中找到了很多好信息)