使用 GDI+ 在表单外渲染图形

Posted

技术标签:

【中文标题】使用 GDI+ 在表单外渲染图形【英文标题】:Graphics are rendered outside the form with GDI+ 【发布时间】:2012-12-14 18:06:08 【问题描述】:

我目前正在使用 GDI+ 制作游戏,我知道这不是开发游戏的最佳解决方案,但由于这是一个学校项目,我别无选择。

大约每运行十次游戏,图形就会呈现在屏幕左上角的表单之外。

如果这有助于缩小问题范围,我会使用双缓冲。

渲染代码如下所示:

while (true)
       
    // Create buffer if it don't exist already
    if (context == null)
    
        context = BufferedGraphicsManager.Current;
        this.buffer = context.Allocate(CreateGraphics(), this.DisplayRectangle);
    

    // Clear the screen with the forms back color
    this.buffer.Graphics.Clear(this.BackColor);

    // Stuff is written to the buffer here, example of drawing a game object:
    this.buffer.Graphics.DrawImage(
        image: SpriteSheet,
        destRect: new Rectangle(
            this.Position.X
            this.Position.Y
            this.SpriteSheetSource.Width,
            this.SpriteSheetSource.Height),
        srcX: this.SpriteSheetSource.X,
        srcY: this.SpriteSheetSource.Y,
        srcWidth: this.SpriteSheetSource.Width,
        srcHeight: this.SpriteSheetSource.Height,
        srcUnit: GraphicsUnit.Pixel);

    // Transfer buffer to display - aka back/front buffer swapping 
    this.buffer.Render();

用截图更容易解释:

【问题讨论】:

是直接渲染到窗体还是控件上? BufferedGraphicsManager 是什么样的? 它被直接渲染到表单,代码放在从Form基类派生的类中。 @Austin 我不太确定,它是 System.Drawing.dll 的一部分 听起来您有时会渲染到 Windows 桌面上下文 (DC)(如果您直接使用 WinAPI,则桌面的窗口句柄将为 0)。在开始游戏循环之前,您是否正在等待创建表单?不确定为什么要在游戏绘制循环中创建上下文/缓冲区。有什么理由可以在创建后的任何时间为空? this.buffer.Render();在这个调用中会发生什么? 【参考方案1】:

在 Winforms 中将 BufferedGraphicsXxx 类公开是一个设计错误。它们是 Winforms 中双缓冲支持的一个实现细节,并且它们对于错误使用它们并不是非常有弹性。

您肯定使用了从 Allocate() 错误返回的 BufferedGraphics。您在游戏循环中以高速率创建缓冲区。但是您忘记在循环结束时处理您使用的缓冲区。这将以高速率消耗设备上下文 (HDC)。这不会永远持续下去,如果你的程序没有让垃圾收集器运行,那么 Windows 会拔掉插头,不会让你创建新的设备上下文。内部 CreateCompatibleDC() 调用将失败并返回 NULL。否则 BufferedGraphicsContext 类会错过检查此错误的代码并继续使用 NULL 句柄。并开始绘制到桌面窗口而不是窗体。

解决方法是将 Allocate() 调用移出循环,这样您只需执行一次。但是现在当用户更改窗口大小时,您将遇到一个新问题,缓冲区不再是正确的大小。

更好的捕鼠器是不使用 BufferedGraphics 类,而是将其留给 Winforms 来解决。在 Winforms 中获取游戏循环的方法有多种,但最简单的方法是使用 OnPaint() 方法渲染场景并立即请求另一个绘制,这样它就会被一遍又一遍地调用。类似这样:

public partial class Form1 : Form 
    public Form1() 
        InitializeComponent();
        this.DoubleBuffered = true;
        this.ResizeRedraw = true;
    
    protected override void OnPaint(PaintEventArgs e) 
        RenderScene(e.Graphics);
        this.Invalidate();
    

RenderScene() 应该在哪里绘制游戏对象,使用传递的 Graphics 实例。请注意,您不再需要使用 Clear(),这已经完成了。

【讨论】:

OnPaint 末尾调用Invalidate() 不会在没有任何动作的情况下消耗大量能量吗? 当然,它会燃烧 100% 核心,尽可能快地绘制帧。就像他原来的游戏循环一样。否则表单会完全响应输入,绘画是一个低优先级的任务。如果这是一个问题,那么最简单的解决方案是一个 Timer,它的 Tick 事件处理程序调用 Invalidate()。假设可以跟上,15 毫秒的间隔获得 64 fps,30 毫秒获得 32 fps。【参考方案2】:

大约每运行一次游戏,图形就会被渲染一次 在屏幕左上角的表单之外。

根据屏幕截图和您的描述,您偶尔会绘制到 Window 的桌面设备上下文 (DC);这是在获取 DC 时使用零窗口句柄 (IntPtr.Zero) 的效果。 这让我相信您可能在创建表单窗口之前开始游戏循环,从而导致图形上下文指向零窗口句柄。

正如评论中所确认的那样,您正在为您的游戏循环使用单独的线程,从而导致这种情况发生的随机行为。一旦处理线程,在启动和完成线程的时间(尤其是当线程可以通过多核/cpu 计算机并行运行时)时,您并不总是两次得到相同的结果。每次运行游戏应用程序时,游戏循环线程都有可能在 UI 线程上的表单窗口被创建和显示之前启动并执行。

【讨论】:

以上是关于使用 GDI+ 在表单外渲染图形的主要内容,如果未能解决你的问题,请参考以下文章

如何调试 GDI 对象泄漏?

带有 GDI 或 GDI+ 的高级图形

将 webbrowser 控件渲染到屏幕外(或隐藏)

Flask--(一对多)模型渲染表单数据

检测 winforms 中的 gdi / 用户处理程序泄漏

在屏幕外渲染 uiview 以捕获为 uiimage