绘制图像时出现问题:System.Runtime.InteropServices.ExternalException:GDI发生通用错误

11

我有一个由Panel创建的全局Graphics对象。定期从磁盘上获取一张图像,并使用Graphics.DrawImage()方法将其绘制到面板中。它在几次迭代中正常工作,然后出现以下有用的异常:

System.Runtime.InteropServices.ExternalException: A generic error occurred in GDI+.
at System.Drawing.Graphics.CheckErrorStatus(Int32 status)
at System.Drawing.Graphics.DrawImage(Image image, Int32 x, Int32 y)
at System.Drawing.Graphics.DrawImage(Image image, Point point)

我排除了内存泄漏的可能性,因为在完成操作后我会释放图像对象。我知道图片没有损坏,并且在程序运行一段时间后面板停止显示之前,程序可以正常执行。

当我使用PictureBox时,我遇到了相同的问题,但这次至少我得到了一个错误,而不是什么也没有。

我在任务管理器中检查了GDI对象和USER对象,但当应用程序正常工作或不正常工作时,它们的数量始终保持在约65个用户对象和165个GDI对象左右。

我需要尽快找到问题的根本原因,但并不像我可以在.NET系统库中设置断点并查看执行失败的确切位置。

提前致谢。

编辑:这是显示代码:

private void DrawImage(Image image)
{
  Point leftCorner = new Point((this.Bounds.Width / 2) - (image.Width / 2), (this.Bounds.Height / 2) - (image.Height / 2));
  _graphics.DrawImage(image, leftCorner);
}

图片加载代码:

private void LoadImage(string filename, ref Image image)
{
  MemoryStream memoryStream = DecryptImageBinary(Settings.Default.ImagePath + filename, _cryptPassword);

  image = Image.FromStream(memoryStream);

  memoryStream.Close();
  memoryStream.Dispose();
  memoryStream = null;
}

_image是全局变量,它的引用在LoadImage中被更新。这些参数是作为参数传递的,因为我想尽可能少地改变全局引用,只保留其他方法的自包含性。_graphics也是全局的。

我还有一个WebBrowser控件用于显示网站,一次只能显示一张图像或网站。当有时间显示图像时,以下代码会执行:

webBrowser.Visible = false;
panel.Visible = true;
DrawImage(_image)
_image.Dispose();
_image = null;

_image引用一个预加载的图片。

希望这能帮到你。

2个回答

16

你的问题与我原先想的类似,但不完全相同。当你加载图像时,是从MemoryStream中加载的。必须在图像生命周期内保持流的打开状态,参见MSDN Image.FromStream

你必须在Image的生命周期内保持流的打开状态。

解决方法是在FromImage函数中对图像进行复制:

private void LoadImage(string filename, ref Image image)
{
  using (MemoryStream memoryStream = DecryptImageBinary(Settings.Default.ImagePath + filename, _cryptPassword))
  {
      using (tmpImage = Image.FromStream(memoryStream))
      { 
         image = new Bitmap(tmpImage);
      }
  }

}

类似于我之前提到的处理问题,当基础流被垃圾回收时,图像似乎能够正常工作,但随机失败。


我会尝试一下,谢谢。我们不需要显式地释放tmpImage吗?释放memoryStream会同时释放底层图像吗? - Michael
是的,你应该显式地处理tmpImage,尽管memoryStream的处理将垃圾回收大部分底层数据。我已经更改了答案来展示这一点。 - Kris Erickson
1
使用“using”结构比显式调用Close()和Dispose()更好。 - Brian Low
程序泄漏了10,000个句柄后,Windows将不再提供它。您可以使用TaskMgr.exe的“进程”选项卡来诊断这些类型的泄漏。选择“查看”+“选择列”以添加句柄、用户对象和GDI对象的列。 GDI对象是稳步增加的那一个。 - rboy
除了Kris的答案之外,如果您发现GDI错误在一段时间后被抛出,那么这里是如何确认它正在泄漏GDI内存并导致异常的方法。程序泄漏了10,000个句柄后,Windows将不再提供它。您可以使用TaskMgr.exe诊断这些类型的泄漏。单击“进程”选项卡,右键单击列并选择添加GDI对象列。如果GDI对象持续增加,则存在GDI内存泄漏。 - rboy

2

没有更多的代码,无法正确诊断问题,但是需要注意的一点是您可能已经在之前对绘图使用的图像进行了处理,只有在垃圾收集器运行后,您的代码才会出现故障。您是否在任何地方使用了克隆图像?我惊讶地发现,如果您直接克隆一张图像,那么您并没有克隆图像所依赖的底层位图,只是克隆了图像结构,要创建一个正确的图像副本,您必须创建一个新的图像:

var newImage = new Bitmap(img)

as

var newImage = oldImg.Clone();
oldImg.Dispose();
...
gr.DrawImage(newImage, new Rectangle(0,0,newImage.Width,newImage.Height);

会有一段时间能够正常工作,但随机出现故障...


我已经添加了一些代码。希望这能让事情变得更加明朗。 - Michael
为了保持清晰,我已经在下面发布了你问题的答案。 - Kris Erickson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接