System.Drawing - 参数无效。

8
在进行更多测试后,我发现这个问题可能是由于图像没有被及时加载到位以便克隆成位图并显示。这种情况是否可能发生呢?
注意:是的,有其他问题的标题中会出现这个错误,但经过一番调查发现该错误是一个含糊不清的错误,有很多可能的原因。我没有找到与我的场景相同的问题。
我遇到了以下错误。
System.ArgumentException was unhandled
HResult=-2147024809
Message=Parameter is not valid.
Source=System.Drawing

这是由代码引起的。看似是随机的(即有时它能正常工作,有时则不能。每次在未重新启动VS和重建项目的情况下连续运行次数越多,它就越可能失败):

private Bitmap GetSprite(bool anim, int tsIndex, int tileIdx) {
    System.Drawing.Rectangle cloneRect;
    string prefix = (anim) ? "A" : "S";
    using (Bitmap b = new Bitmap(prefix + tsIndex.ToString() + ".png")) {
        if (anim) {
            cloneRect = new System.Drawing.Rectangle(BaseObjects.A_AnimSpriteSets[tsIndex].StaticRecs[tileIdx].X, BaseObjects.A_AnimSpriteSets[tsIndex].StaticRecs[tileIdx].Y, BaseObjects.A_AnimSpriteSets[tsIndex].RecWidth, BaseObjects.A_AnimSpriteSets[tsIndex].RecHeight);
        } else {
            cloneRect = new System.Drawing.Rectangle(BaseObjects.A_StaticSpriteSets[tsIndex].StaticRecs[tileIdx].X, BaseObjects.A_StaticSpriteSets[tsIndex].StaticRecs[tileIdx].Y, BaseObjects.A_StaticSpriteSets[tsIndex].RecWidth, BaseObjects.A_StaticSpriteSets[tsIndex].RecHeight);
        }
        return b.Clone(cloneRect, b.PixelFormat);
    }
}

具体来说,第四行:
using (Bitmap b = new Bitmap(prefix + tsIndex.ToString() + ".png"))

简化的目标是从精灵集中返回包含精灵的位图,基于精灵集索引和精灵索引。该位图在PictureBox中显示,直到它被更改为不同的图像。我知道逻辑是正确的,问题不在于此。我使用的.png大小为384x256。
所有参数都设置正确,所有引用的文件都在那里,一切似乎都按顺序进行。最奇怪的是有时它起作用,有时它不起作用。这让我相信它可能是System.Drawing本身内存泄漏,但我似乎无法追踪它。
编辑:更新了代码并添加了StackTrace。尽管在不再使用Bitmap时处理它们(请参阅下面的代码示例以了解如何处置Bitmap),但仍然存在相同的问题。
if (Sprite.Image != null) { Sprite.Image.Dispose(); }
    Sprite.Image = GetSprite(true, tsIdx, tileIdx);

堆栈跟踪:

System.ArgumentException was unhandled
  HResult=-2147024809
  Message=Parameter is not valid.
  Source=System.Drawing
  StackTrace:
       at System.Drawing.Bitmap..ctor(String filename)
       at CreationTool.Main.GetSprite(Boolean anim, Int32 tsIndex, Int32 tileIdx) in F:\~\~\CreationTool\Main.cs:line 420
       at CreationTool.Main.Input_EnemySprite_SelectedIndexChanged(Object sender, EventArgs e) in F:\~\~\CreationTool\Main.cs:line 107
       at System.Windows.Forms.ComboBox.OnSelectedIndexChanged(EventArgs e)
       at System.Windows.Forms.ComboBox.set_SelectedIndex(Int32 value)
       at CreationTool.States.State_Enemy.populateForm() in F:\~\~\CreationTool\States\State_Enemy.cs:line 28
       at CreationTool.States.State_Enemy.Load(String name) in F:\~\~\CreationTool\States\State_Enemy.cs:line 22
       at CreationTool.Main.btnLoad_Click(Object sender, EventArgs e) in F:\~\~\CreationTool\Main.cs:line 174
       at System.Windows.Forms.Control.OnClick(EventArgs e)
       at System.Windows.Forms.Button.OnClick(EventArgs e)
       at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
       at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
       at System.Windows.Forms.Control.WndProc(Message& m)
       at System.Windows.Forms.ButtonBase.WndProc(Message& m)
       at System.Windows.Forms.Button.WndProc(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
       at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
       at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
       at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr dwComponentID, Int32 reason, Int32 pvLoopData)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.Run(Form mainForm)
       at CreationTool.Program.Main() in F:\~\~\CreationTool\Program.cs:line 15
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()

2
你需要在克隆完位图(Bitmap)后调用 Dispose 方法。位图使用非托管资源,这可能会导致内存压力问题。我建议用 using 语句来封装它。 - pstrjds
谢谢,看起来问题得到解决了。把它改成一个回答? - Djentleman
你应该在应用程序的Main方法中添加一个try/catch块,然后将异常信息输出到控制台或记录下来,这样你就可以查看堆栈跟踪并准确地知道它的来源。 - pstrjds
等等,算了吧。还没解决。再做几个测试,回到原点。 - Djentleman
你能为异常添加堆栈跟踪吗?它是来自这个方法内部吗?你是否在其他地方泄漏了GDI对象?你是否创建了Graphics对象但没有释放Pens、Brushes等? - pstrjds
显示剩余2条评论
4个回答

6

哇,这段代码泄漏了大量的句柄。您需要处理所有实现IDisposable接口的类型,而在System.Drawing程序集(GDI+)中有很多这样的类型:

private Bitmap GetSprite(bool anim, int tsIndex, int tileIdx)
{
    Rectangle cloneRect;
    string prefix = (anim) ? "A" : "S";
    using (Bitmap b = new Bitmap(prefix + tsIndex.ToString() + ".png"))
    {
        if (anim)
        {
            cloneRect = new Rectangle(BaseObjects.A_AnimSpriteSets[tsIndex].StaticRecs[tileIdx].X, BaseObjects.A_AnimSpriteSets[tsIndex].StaticRecs[tileIdx].Y, BaseObjects.A_AnimSpriteSets[tsIndex].RecWidth, BaseObjects.A_AnimSpriteSets[tsIndex].RecHeight);
        }
        else
        {
            cloneRect = new Rectangle(BaseObjects.A_StaticSpriteSets[tsIndex].StaticRecs[tileIdx].X, BaseObjects.A_StaticSpriteSets[tsIndex].StaticRecs[tileIdx].Y, BaseObjects.A_StaticSpriteSets[tsIndex].RecWidth, BaseObjects.A_StaticSpriteSets[tsIndex].RecHeight);
        }

        return b.Clone(cloneRect, b.PixelFormat);
    }
}

同时,请确保您已经通过将调用者包装在 using 语句中处理了此函数返回的位图:

using (Bitmap b = GetSprite(true, 0, 5))
{
    // do whatever you needed to do with the bitmap here
}

我试图清理一下代码,并添加using。哦,好吧,我会点赞你的 :) - pstrjds
根据建议修改了所有对GetSprite()的调用代码,但仍然在完全相同的位置遇到了相同的错误。现在,我只看到红色叉号代替图像。 - Djentleman
从我的回答中可以看出,这段代码的简化目标是根据精灵集索引和精灵索引返回一个包含精灵的位图。然后,我将该精灵(即GetSprite()的结果)显示在PictureBox中。我希望当我从ListBox中选择精灵索引时,PictureBox能够动态地改变。 - Djentleman
好的,所以你正在一个PictureBox中显示这个函数的结果。那么你不应该立即将其处理掉。你应该在分配新的图片之前处理旧的picturebox位图:pictureBox1.Image.Dispose(); pictureBox.Image = GetSprite(...) - Darin Dimitrov
好的,我想我明白了。我需要添加一些事件,但我应该能够做到。我完成后会发布更新。谢谢! - Djentleman
显示剩余4条评论

4
这些泄漏的处理句柄最终会导致内存问题,但在这种情况下它们并不是问题(感谢那些指出它们的人,我学到了新东西)。
问题在于,由于我加载实际图像到内存的方式,大多数测试中图像没有被充分加载。成功的那些是允许足够时间加载图像的那些。
我通过一个简单的try/catch解决了这个问题。
private Bitmap GetSprite(bool anim, int tsIndex, int tileIdx) {
    string prefix;
    System.Drawing.Rectangle cloneRect;
    SpriteSet set;
    if (anim) {
        prefix = "A";
        set = BaseObjects.A_AnimSpriteSets[tsIndex];
    } else {
        prefix = "S";
        set = BaseObjects.A_StaticSpriteSets[tsIndex];
    }
    cloneRect = new System.Drawing.Rectangle(set.StaticRecs[tileIdx].X, set.StaticRecs[tileIdx].Y, set.RecWidth, set.RecHeight);
    try {
        using (Bitmap b = new Bitmap(prefix + tsIndex.ToString() + ".png")) {
            return b.Clone(cloneRect, b.PixelFormat);
        }
    } catch (Exception ex) {
        MessageBox.Show("Error: " + ex.Message + "\n\nCause: " + "SpriteSet not yet loaded.");
        return null;
    }
}

这是我需要的完整程序。

pstrjds,也感谢你的清理。混乱可能是在某次重构期间出现的。我猜我只是忘记了它。


1

1
对我来说,这是因为引用了相对路径,例如@"Resources\ImageName.png"。在debug模式下,我没有加载图像的问题,但正如OP所建议的那样,这可能实际上是资源加载时间问题。我正确地使用和处理我的对象,但这与问题实际上无关。一旦它在Release中运行,错误“参数无效”将再次出现。

我怀疑编译器优化是这样的:在调试模式下,图像流确实能够在构造函数解析之前扩展,但在发布构建中,路径仍然需要在cctor准备解析路径时进行扩展,这导致路径无效错误。将路径从相对路径更改为固定或预先扩展的路径即可解决此问题。


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