创建的 C# 图标看起来不错,但 Windows 目录缩略图看起来不正确

C# Icon created looks fine, but Windows directory thumbnails don't look right

我写了一些代码来从任何 png、jpg 等图像创建 ico 文件。在 Paint3d 中打开时,图标似乎创建正确,看起来几乎与原始图像一样。外观如下:

但是当将图像设置为文件夹的缩略图时,它看起来很奇怪而且闪闪发光。

这是它在 windows 文件浏览器中的样子:

首先,我想知道这是Windows本身的问题,还是与代码有关?如果这与 Windows 相关,则代码无关紧要。如果没有,这里是:

我从互联网上收集了几个代码片段,所以可能是一些未优化的代码,但这是我的代码的主要部分:

//imagePaths => all images which I am converting to ico files
imagePaths.ForEach(imgPath => {
    //create a temp png at this path after changing the original img to a squared img
    var tempPNGpath = Path.Combine(icoDirPath, imgName.Replace(ext, ".png"));
    var icoPath = tempPNGpath.Replace(".png", ".ico");

    using (FileStream fs1 = File.OpenWrite(tempPNGpath)) {
        Bitmap b = ((Bitmap)Image.FromFile(imgPath));
        b = b.CopyToSquareCanvas(Color.Transparent);
        b.Save(fs1, ImageFormat.Png);

        fs1.Flush();
        fs1.Close();

        ConvertToIco(b, icoPath, 256);
    }
    File.Delete(tempPNGpath);
});


public static void ConvertToIco(Image img, string file, int size) {
    Icon icon;
    using (var msImg = new MemoryStream())
        using (var msIco = new MemoryStream()) {
            img.Save(msImg, ImageFormat.Png);
            using (var bw = new BinaryWriter(msIco)) {
                bw.Write((short)0);           //0-1 reserved
                bw.Write((short)1);           //2-3 image type, 1 = icon, 2 = cursor
                bw.Write((short)1);           //4-5 number of images
                bw.Write((byte)size);         //6 image width
                bw.Write((byte)size);         //7 image height
                bw.Write((byte)0);            //8 number of colors
                bw.Write((byte)0);            //9 reserved
                bw.Write((short)0);           //10-11 color planes
                bw.Write((short)32);          //12-13 bits per pixel
                bw.Write((int)msImg.Length);  //14-17 size of image data
                bw.Write(22);                 //18-21 offset of image data
                bw.Write(msImg.ToArray());    // write image data
                bw.Flush();
                bw.Seek(0, SeekOrigin.Begin);
                icon = new Icon(msIco);
            }
        }
    using (var fs = new FileStream(file, FileMode.Create, FileAccess.Write))
        icon.Save(fs);
}

在扩展 class 中,方法是:

public static Bitmap CopyToSquareCanvas(this Bitmap sourceBitmap, Color canvasBackground) {
    int maxSide = sourceBitmap.Width > sourceBitmap.Height ? sourceBitmap.Width : sourceBitmap.Height;

    Bitmap bitmapResult = new Bitmap(maxSide, maxSide, PixelFormat.Format32bppArgb);
    using (Graphics graphicsResult = Graphics.FromImage(bitmapResult)) {
        graphicsResult.Clear(canvasBackground);

        int xOffset = (maxSide - sourceBitmap.Width) / 2;
        int yOffset = (maxSide - sourceBitmap.Height) / 2;

        graphicsResult.DrawImage(sourceBitmap, new Rectangle(xOffset, yOffset, sourceBitmap.Width, sourceBitmap.Height));
    }

    return bitmapResult;
}

缩放的差异是您没有自己进行缩放的结果。

图标格式在技术上仅支持最大 256x256 的图像。你有代码从给定的输入中制作一个方形图像,但你从未将它的大小调整为 256x256,这意味着你最终得到一个图标文件,其中 header 表示图像是 256x256,但这确实很多更大。这违反了格式规范,因此您正在创建技术上损坏的 ico 文件。您看到的奇怪差异是 OS 在不同情况下使用不同的缩小方法来补救这种情况的结果。

所以解决方法很简单:将图片调整为 256x256,然后再放入图标中。

如果您想更好地控制图标的任何较小的显示尺寸,您可以添加代码以将其调整为多种 classic 使用的格式,例如 16x16、32x32、64x64 和 128x128,然后将它们都放在一个图标文件中。我已经写了另一个问题的答案,详细说明了将多个图像放入单个图标的过程:

A: Combine System.Drawing.Bitmap[] -> Icon

不过,您的代码中还有很多其他奇怪之处:

  • 我认为没有理由将您的 in-between 图像保存为 png 文件。整个 fs1 流根本没有用。您从不使用或加载临时文件;您只需继续使用 b 变量,它不需要将任何内容写入磁盘。
  • 首先将图标制作成MemoryStream,然后通过其文件加载功能将其加载为Icon class,然后将其保存到文件中是没有意义的。您可以将该流的内容直接写入文件,或者,哎呀,立即使用 FileStream
  • 正如我在评论中指出的那样,Bitmap 是一次性的 class,因此您创建的任何位图 object 也应该放在 using 语句中。

修改后的加载代码,删除了 temp png 文字,并添加了 using 语句和调整大小:

public static void WriteImagesToIcons(List<String> imagePaths, String icoDirPath)
{
    // Change this to whatever you prefer.
    InterpolationMode scalingMode = InterpolationMode.HighQualityBicubic;
    //imagePaths => all images which I am converting to ico files
    imagePaths.ForEach(imgPath =>
    {
        // The correct way of replacing an extension
        String icoPath = Path.Combine(icoDirPath, Path.GetFileNameWithoutExtension(imgPath) + ".ico");
        using (Bitmap orig = new Bitmap(imgPath))
        using (Bitmap squared = orig.CopyToSquareCanvas(Color.Transparent))
        using (Bitmap resize16 = squared.Resize(16, 16, scalingMode))
        using (Bitmap resize32 = squared.Resize(32, 32, scalingMode))
        using (Bitmap resize48 = squared.Resize(48, 48, scalingMode))
        using (Bitmap resize64 = squared.Resize(64, 64, scalingMode))
        using (Bitmap resize96 = squared.Resize(96, 96, scalingMode))
        using (Bitmap resize128 = squared.Resize(128, 128, scalingMode))
        using (Bitmap resize192 = squared.Resize(192, 192, scalingMode))
        using (Bitmap resize256 = squared.Resize(256, 256, scalingMode))
        {
            Image[] includedSizes = new Image[]
                { resize16, resize32, resize48, resize64, resize96, resize128, resize192, resize256 };
            ConvertImagesToIco(includedSizes, icoPath);
        }
    });
}

CopyToSquareCanvas保持原样,这里就没有复制了。 Resize功能相当简单:只需在different-sizedcanvas上设置所需的插值模式后,使用Graphics.DrawImage绘制图片即可。

public static Bitmap Resize(this Bitmap source, Int32 width, Int32 height, InterpolationMode scalingMode)
{
    Bitmap result = new Bitmap(width, height, PixelFormat.Format32bppArgb);
    using (Graphics g = Graphics.FromImage(result))
    {
        // Set desired interpolation mode here
        g.InterpolationMode = scalingMode;
        g.PixelOffsetMode = PixelOffsetMode.Half;
        g.DrawImage(source, new Rectangle(0, 0, width, height), new Rectangle(0, 0, source.Width, source.Height), GraphicsUnit.Pixel);
    }
    return result;
}

最后,above-linked Bitmap[] 到 Icon 函数,稍微调整为直接写入 FileStream 而不是将结果加载到一个 Icon object:

public static void ConvertImagesToIco(Image[] images, String outputPath)
{
    if (images == null)
        throw new ArgumentNullException("images");
    Int32 imgCount = images.Length;
    if (imgCount == 0)
        throw new ArgumentException("No images given!", "images");
    if (imgCount > 0xFFFF)
        throw new ArgumentException("Too many images!", "images");
    using (FileStream fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
    using (BinaryWriter iconWriter = new BinaryWriter(fs))
    {
        Byte[][] frameBytes = new Byte[imgCount][];
        // 0-1 reserved, 0
        iconWriter.Write((Int16)0);
        // 2-3 image type, 1 = icon, 2 = cursor
        iconWriter.Write((Int16)1);
        // 4-5 number of images
        iconWriter.Write((Int16)imgCount);
        // Calculate header size for first image data offset.
        Int32 offset = 6 + (16 * imgCount);
        for (Int32 i = 0; i < imgCount; ++i)
        {
            // Get image data
            Image curFrame = images[i];
            if (curFrame.Width > 256 || curFrame.Height > 256)
                throw new ArgumentException("Image too large!", "images");
            // for these three, 0 is interpreted as 256,
            // so the cast reducing 256 to 0 is no problem.
            Byte width = (Byte)curFrame.Width;
            Byte height = (Byte)curFrame.Height;
            Byte colors = (Byte)curFrame.Palette.Entries.Length;
            Int32 bpp;
            Byte[] frameData;
            using (MemoryStream pngMs = new MemoryStream())
            {
                curFrame.Save(pngMs, ImageFormat.Png);
                frameData = pngMs.ToArray();
            }
            // Get the colour depth to save in the icon info. This needs to be
            // fetched explicitly, since png does not support certain types
            // like 16bpp, so it will convert to the nearest valid on save.
            Byte colDepth = frameData[24];
            Byte colType = frameData[25];
            // I think .Net saving only supports colour types 2, 3 and 6 anyway.
            switch (colType)
            {
                case 2: bpp = 3 * colDepth; break; // RGB
                case 6: bpp = 4 * colDepth; break; // ARGB
                default: bpp = colDepth; break; // Indexed & greyscale
            }
            frameBytes[i] = frameData;
            Int32 imageLen = frameData.Length;
            // Write image entry
            // 0 image width. 
            iconWriter.Write(width);
            // 1 image height.
            iconWriter.Write(height);
            // 2 number of colors.
            iconWriter.Write(colors);
            // 3 reserved
            iconWriter.Write((Byte)0);
            // 4-5 color planes
            iconWriter.Write((Int16)0);
            // 6-7 bits per pixel
            iconWriter.Write((Int16)bpp);
            // 8-11 size of image data
            iconWriter.Write(imageLen);
            // 12-15 offset of image data
            iconWriter.Write(offset);
            offset += imageLen;
        }
        for (Int32 i = 0; i < imgCount; i++)
        {
            // Write image data
            // png data must contain the whole png data file
            iconWriter.Write(frameBytes[i]);
        }
        iconWriter.Flush();
    }
}