为什么位图中的数据会超出范围?

5
我正在为Skyrim创建一个存档管理器,遇到了一个问题。当我单独创建一个SaveGame对象时,存档的位图部分工作正常。然而,当我在循环中调用该方法时,位图就会出现错误值,主要是类似于另一个存档的错误值。
简而言之,我的表单的列表框为角色存档正确显示除嵌入的图片外的所有信息。它似乎选择了最后处理的图片,而不是选择正确的图片。这个过程与通过打开文件对话框选择的过程有何不同?
编辑:更新-我查看了每个SaveGame对象存储的位图,并发现在scanDirectoryForSaves中创建SaveGames时会出错。是否有关于位图和使用字节指针的对象范围问题,我不知道的?
这是我保存游戏对象静态工厂的代码:
public string Name { get; private set; }
    public int SaveNumber { get; private set; }
    public int PictureWidth { get; private set; }
    public int PictureHeight { get; private set; }
    public Bitmap Picture { get; private set; }
    public DateTime SaveDate { get; private set; }
    public string FileName { get; private set; }

    public static SaveGame ReadSaveGame(string Filename)
    {

        SaveGame save = new SaveGame();
        save.FileName = Filename;

        byte[] file = File.ReadAllBytes(Filename);

        int headerWidth = BitConverter.ToInt32(file, 13);
        save.SaveNumber = BitConverter.ToInt32(file, 21);
        short nameWidth = BitConverter.ToInt16(file, 25);

        save.Name = System.Text.Encoding.UTF8.GetString(file, 27, nameWidth);
        save.PictureWidth = BitConverter.ToInt32(file, 13 + headerWidth - 4);
        save.PictureHeight = BitConverter.ToInt32(file, 13 + headerWidth);
        save.readPictureData(file, 13 + headerWidth + 4, save.PictureWidth, save.PictureHeight);

        save.SaveDate = DateTime.FromFileTime((long)BitConverter.ToUInt64(file, 13 + headerWidth - 12));

        return save;
    }

    private  void readPictureData(byte[] file, int startIndex, int width, int height)
    {
        IntPtr pointer = Marshal.UnsafeAddrOfPinnedArrayElement(file, startIndex);
        Picture =  new Bitmap(width, height, 3 * width, System.Drawing.Imaging.PixelFormat.Format24bppRgb, pointer);
    }

在我的表格中,我使用一种方法来读取特定目录中的所有保存文件,将它们转换为SaveGame对象,并基于角色名称将它们存储到字典中。

private Dictionary<string, List<SaveGame>> scanDirectoryForSaves(string directory)
    {
        Dictionary<string, List<SaveGame>> saves = new Dictionary<string, List<SaveGame>>();
        DirectoryInfo info = new DirectoryInfo(directory);

        foreach (FileInfo file in info.GetFiles())
        {
            if (file.Name.ToLower().EndsWith(".ess") || file.Name.ToLower().EndsWith(".bak"))
            {
                string filepath = String.Format(@"{0}\{1}", directory, file.Name);
                SaveGame save = SaveGame.ReadSaveGame(filepath);

                if (!saves.ContainsKey(save.Name))
                {
                    saves.Add(save.Name, new List<SaveGame>());
                }
                saves[save.Name].Add(save);
            }
        }

        foreach (List<SaveGame> saveList in saves.Values)
        {
            saveList.Sort();
        }

        return saves;
    }

我将键添加到列表框中。当在列表框中选择一个名称时,表单上会显示该角色的最新保存。每个角色的名称、日期和其他字段都是正确的,但位图是某个角色保存游戏图片的变化。

我在从打开文件对话框中选择保存和列表框中调用相同的方法来更新表单字段。

private void updateLabels(SaveGame save)
    {
        nameLabel.Text = "Name: " + save.Name;
        filenameLabel.Text = "File: " + save.FileName;
        saveNumberLabel.Text = "Save Number: " + save.SaveNumber;

        saveDateLabel.Text = "Save Date: " + save.SaveDate;

        saveGamePictureBox.Image = save.Picture;
        saveGamePictureBox.Image = ScaleImage(
            saveGamePictureBox.Image, saveGamePictureBox.Width, saveGamePictureBox.Height);
        saveGamePictureBox.Invalidate();
    }
1个回答

4
当您使用接受IntPtr参数的构造函数创建位图时,IntPtr必须指向在位图对象生命周期内保持有效的内存块。您负责确保内存块不会被移动或释放。
但是,您的代码正在传递指向file的IntPtr,它是托管字节数组。因为在ReadSaveGame返回后没有引用file,垃圾收集器可以自由地回收内存并将其重用于下一个文件。结果:位图损坏。
尽管您可以通过使用GCHandle将数组固定在内存中来解决此问题,但让位图管理自己的内存可能更容易、更安全。首先创建一个空的位图,然后设置其位。
private void readPictureData(byte[] file, int startIndex, int width, int height)
{
    Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb);
    BitmapData data = bitmap.LockBits(
        new Rectangle(0, 0, width, height),
        ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
    Marshal.Copy(file, startIndex, data.Scan0, width * height * 3);
    bitmap.UnlockBits(data);
    Picture = bitmap;
}

只是为了确保我理解,我的代码中,文件内容在函数结束时会超出范围,因为它被分配在 byte[] file = File.ReadAllBytes(Filename); 中?非常感谢您的答案! - Gilbrilthor
是的,“file”是对字节数组的唯一引用,因此当方法返回时,垃圾收集器可以随时回收内存。(尽管您的“IntPtr”指向数组,但它不是托管引用,因此垃圾收集器会忽略它。)但是GC可能不会立即运行,这就是为什么在只有一个文件时您可以逃脱的原因。 - Michael Liu

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