C#で、文字と図形からなるWindowsアイコン(.Ico)【透過】を作成するプログラムを作ってみた

# やれること

こんな感じのWindowsアイコンが作れる。
UIを作りこむのが大変なので、下記のソースコード(`EasyDrawIcon.cs`)を直接修正して使う方向で。

⇒ ツールにしました。 https://qiita.com/kob58im/items/7129810d3237e65b424d

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/87e894ba-f9ab-a995-c484-5a7825c9368a.png)

# 注意事項

作ったアイコンについては著作権に注意してください。特にフォントには著作権があります。
どのフォントがセーフなのかは自分はわかりません。

# ソースコード

[以前のコード](https://qiita.com/kob58im/items/1d3420b51ff6cb5c3871)をちょっとリファクタリングして、アイコンデータ作成部分を`IconUtility.cs`として分離した。

<details><summary>ソースコード - IconUtility.cs</summary><div>

```CSharp:IconUtility.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;


namespace IconUtility
{
    public class Icons
    {
        class IconEntry
        {
            [StructLayout(LayoutKind.Sequential)]
            public struct IconDir
            {
                public short  icoReserved; // must be 0
                public short  icoResourceType; //must be 1 for icon
                public short  icoResourceCount;

                public IconDir(int n)
                {
                    icoReserved = 0;
                    icoResourceType = 1;
                    icoResourceCount = (short)n;
                }
            }

            [StructLayout(LayoutKind.Sequential)]
            public struct IconDirEntry
            {
                byte   _Width;
                byte   _Height;
                public byte   ColorCount;
                public byte   reserved1;
                public short  reserved2;
                public short  reserved3;
                public int    icoDIBSize;
                public int    icoDIBOffset;

                public int Width{get{return (_Width>0)?_Width:256;}}
                public int Height{get{return (_Height>0)?_Height:256;}}

                public IconDirEntry(int w, int h)
                {
                    if ( w<0 || w>256 || h<0 || h>256 ) {
                        throw new Exception("Size parameter error");
                    }
                    _Width  = (byte)w;
                    _Height = (byte)h;
                    ColorCount=0;
                    reserved1=0;
                    reserved2=0;
                    reserved3=0;
                    icoDIBSize=0;
                    icoDIBOffset=0;
                }
            }

            [StructLayout(LayoutKind.Sequential)]
            struct BitmapInfoHeader
            {
                public int    biSize; // must be 40
                public int    biWidth;
                public int    biHeight;
                public short  biPlanes; // must be 1
                public short  biBitCount; // color
                public int    biCompression; // 0:not compress
                public int    biSizeImage;
                public int    biXPixPerMeter;
                public int    biYPixPerMeter;
                public int    biClrUsed;
                public int    biClrImportant;

                public BitmapInfoHeader(int w, int h)
                {
                    biSize = 40;
                    biWidth = w;
                    biHeight = h*2; // 本体とmaskを含むため2倍とする決まりらしい
                    biPlanes = 1;
                    biBitCount = 32;
                    biCompression=0;
                    biSizeImage=0;
                    biXPixPerMeter=0;
                    biYPixPerMeter=0;
                    biClrUsed=0;
                    biClrImportant=0;
                }
            }

            IconDirEntry     iconDirEntry;
            BitmapInfoHeader bitmapInfoHeader;
            byte[] bitmapBody;
            byte[] bitmapMask;

            public System.Drawing.Size Size{get{return new System.Drawing.Size(iconDirEntry.Width, iconDirEntry.Height);}}
            public int Width{get{return iconDirEntry.Width;}}
            public int Height{get{return iconDirEntry.Height;}}
            int BitPerPixel{get{return bitmapInfoHeader.biBitCount;}}

            int CalcDIBSize()
            {
                return Marshal.SizeOf(typeof(BitmapInfoHeader)) + bitmapBody.Length + bitmapMask.Length;
            }

            public int UpdateIconDirEntry(int icoDIBOffset)
            {
                iconDirEntry.icoDIBOffset = icoDIBOffset;
                iconDirEntry.icoDIBSize   = CalcDIBSize();
                return iconDirEntry.icoDIBSize;
            }

            public void WriteIconDirEntryTo(BinaryWriter writer)
            {
                Icons.CopyDataToByteArray<IconDirEntry>(writer, iconDirEntry);
            }

            public void WriteDataTo(BinaryWriter writer)
            {
                Icons.CopyDataToByteArray<BitmapInfoHeader>(writer, bitmapInfoHeader);
                writer.Write(bitmapBody);
                writer.Write(bitmapMask);
            }

            IconEntry(IconDirEntry _iconDirEntry, BitmapInfoHeader _bitmapInfoHeader, byte[] _bitmapBody, byte[] _bitmapMask)
            {
                iconDirEntry = _iconDirEntry;
                bitmapInfoHeader = _bitmapInfoHeader;
                bitmapBody = _bitmapBody;
                bitmapMask = _bitmapMask;
            }

            // 本体
            public IconEntry(Bitmap bmp, Color? alphaColor)
            {
                int w = bmp.Width;
                int h = bmp.Height;

                if ( w>256 || h>256 ) {
                    throw new Exception("size parameter error");
                }

                iconDirEntry = new IconDirEntry(w, h);
                bitmapInfoHeader = new BitmapInfoHeader(w, h);
                bitmapBody = new byte[Icons.GetBitmapBodySize(w, h, bitmapInfoHeader.biBitCount)];
                bitmapMask = new byte[Icons.GetBitmapMaskSize(w, h)];

                Draw32bppBitmapToData(bmp, alphaColor);
            }

            void Draw32bppBitmapToData(Bitmap bmp, Color? alphaColor)
            {
                Array.Clear(bitmapMask, 0, bitmapMask.Length);
                Array.Clear(bitmapBody, 0, bitmapBody.Length);

                BitmapData bd = bmp.LockBits(new Rectangle(0,0,bmp.Width,bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

                try {
                    IntPtr ptr = bd.Scan0;
                    byte[] pixels = new byte[bd.Stride * bmp.Height];
                    Marshal.Copy(ptr, pixels, 0, pixels.Length);

                    int maskStride = (((Width+7)/8+3)/4)*4;
                    int icoStride = Width*4;

                    for (int y = 0; y < bd.Height; y++) {
                        for (int x = 0; x < bd.Width; x++) {
                            int posIco = y * icoStride + 4*x;

                            int bytePosMask = y * maskStride + x/8;
                            int bitPosMask = 7-(x%8);

                            int pos = (bd.Height-1-y) * bd.Stride + x * 4;
                            bitmapBody[posIco  ] = pixels[pos];   //blue;
                            bitmapBody[posIco+1] = pixels[pos+1]; //green;
                            bitmapBody[posIco+2] = pixels[pos+2]; //red;
                            bitmapBody[posIco+3] = pixels[pos+3]; //alpha

                            if ( pixels[pos+3] == 0 ||
                            (alphaColor != null &&
                                pixels[pos]  ==alphaColor.Value.B &&
                                pixels[pos+1]==alphaColor.Value.G &&
                                pixels[pos+2]==alphaColor.Value.R  )) {
                                //bitmapMask[bytePosMask] |= (byte)(1<<bitPosMask);
                                // 32bit色のiconだとmaskではなくalpha channelが使用されるっぽい
                                bitmapBody[posIco+3] = 0x00;
                            }
                        }
                    }
                }
                finally {
                    bmp.UnlockBits(bd);
                }
            }
        }

        int UpdateIconDirEntries()
        {
            iconDir.icoResourceCount = (short)iconEntries.Count;

            int offset  =  Marshal.SizeOf(typeof(IconEntry.IconDir))  +  iconEntries.Count * Marshal.SizeOf(typeof(IconEntry.IconDirEntry));

            for (int i=0;i<iconEntries.Count;i++) {
                offset += iconEntries[i].UpdateIconDirEntry(offset);
            }
            return offset;
        }

        static int GetBitmapBodySize(int w, int h, int bitCount)
        {
            return ((((w*bitCount + 7)/8)+3)/4)*4 * h;
        }

        static int GetBitmapMaskSize(int w, int h)
        {
            return ((((w+7)/8)+3)/4)*4 * h;
        }

        static TStruct CopyDataToStruct<TStruct> (BinaryReader reader) where TStruct : struct
        {
            var size = Marshal.SizeOf(typeof(TStruct));
            var ptr = IntPtr.Zero;

            try {
                ptr = Marshal.AllocHGlobal(size);
                Marshal.Copy(reader.ReadBytes(size), 0, ptr, size);
                return (TStruct)Marshal.PtrToStructure(ptr, typeof(TStruct));
            }
            finally {
                if (ptr != IntPtr.Zero) {
                    Marshal.FreeHGlobal(ptr);
                }
            }
        }

        static void CopyDataToByteArray<TStruct>(BinaryWriter writer, TStruct s) where TStruct : struct
        {
            var size = Marshal.SizeOf(typeof(TStruct));
            var buffer = new byte[size];
            var ptr = IntPtr.Zero;

            try {
                ptr = Marshal.AllocHGlobal(size);
                Marshal.StructureToPtr(s, ptr, false);
                Marshal.Copy(ptr, buffer, 0, size);
            }
            finally {
                if (ptr != IntPtr.Zero) {
                    Marshal.FreeHGlobal(ptr);
                }
            }
            writer.Write(buffer);
        }



        IconEntry.IconDir iconDir;
        List<IconEntry> iconEntries;
        
        // ------------------------------------------------------------------------------
        // public members
        
        public int Count {get{return iconEntries.Count;}}

        public Icons()
        {
            iconDir      = new IconEntry.IconDir(0);
            iconEntries  = new List<IconEntry>();
        }

        public void AddIcon(Bitmap bmp, Color alphaColor)
        {
            iconEntries.Add(new IconEntry(bmp, alphaColor));
            UpdateIconDirEntries();
        }

        public void AddIcon(Bitmap bmp)
        {
            iconEntries.Add(new IconEntry(bmp, null));
            UpdateIconDirEntries();
        }

        public bool SaveToFile(string path)
        {
            //int size = 
            UpdateIconDirEntries();

            using ( var fs = new FileStream(path, FileMode.Create) ) {
                using ( var writer = new BinaryWriter(fs) ) {
                    CopyDataToByteArray<IconEntry.IconDir>(writer, iconDir);

                    foreach(var t in iconEntries) {
                        t.WriteIconDirEntryTo(writer);
                    }

                    foreach(var t in iconEntries) {
                        t.WriteDataTo(writer);
                    }
                }
            }

            return true;
        }
    }
}

```
</div></details>

<details><summary>ソースコード - EasyDrawIcon.cs</summary><div>

```csharp:EasyDrawIcon.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using IconUtility;



class EasyDraw:Form
{
    PictureBox pct;
    const int MAX_SIZE = 64;
    const int CANVAS_SIZE = 512;
//    const int ZOOM = 2;

    Font curFont;
    int curFontStyle;
    Pen curPen;
    Brush curBrush;
    PointF curPoint;
    GraphicsPath curPath;
    Graphics curG;

    const string DefaultFontName = "メイリオ";  // フォントを変える場合はここを変更

    EasyDraw()
    {
        curPen = Pens.Black;
        curBrush = Brushes.White;
        curPath = new GraphicsPath();
        curFont = new Font(DefaultFontName, 30.0f);  // フォントサイズを変える場合はここを変更

        if (curFont.Name != DefaultFontName) {
            MessageBox.Show("フォント \""+DefaultFontName+"\"の取得に失敗しました。\r\n"
                          + "代わりにフォント\"" + curFont.Name + "\"が使用されます。\r\n");
        }
        MessageBox.Show("アイコンに文字を使う場合は、フォントの著作権に注意してください。");

        curFontStyle = (int)FontStyle.Regular; // Bold , Italic , Strikeout , Underline の bitOR を設定可能

        pct = new PictureBox();
        pct.Image = new Bitmap(CANVAS_SIZE, CANVAS_SIZE);
        pct.Dock = DockStyle.Fill;
        Controls.Add(pct);

        ClientSize = new Size(CANVAS_SIZE, CANVAS_SIZE);

        Load+=(sender,e)=>{Draw();};
    }

    void moveto(float x, float y)
    {
        curPoint.X = x;
        curPoint.Y = y;
    }

    void rmoveto(float rx, float ry)
    {
        curPoint.X += rx;
        curPoint.Y += ry;
    }

    void lineto(float x, float y)
    {
        curPath.AddLine(curPoint.X, curPoint.Y, x, y);
        curPoint.X = x;
        curPoint.Y = y;
    }

    void rlineto(float rx, float ry)
    {
        curPath.AddLine(curPoint.X, curPoint.Y, curPoint.X+rx, curPoint.Y+ry);
        curPoint.X += rx;
        curPoint.Y += ry;
    }

    void charpath(string s)
    {
        var sf = new StringFormat();
        sf.Alignment     = StringAlignment.Center;    // 横方向の中央
        sf.LineAlignment = StringAlignment.Center;    // 縦方向の中央
        FontFamily ff = curFont.FontFamily;
        curPath.AddString(s, ff, curFontStyle, curFont.Size, curPoint, sf);
    }

    void newpath()
    {
        curPath.Reset();
        curPath.StartFigure();
    }

    void closepath()
    {
        curPath.CloseFigure();
    }

    void stroke()
    {
        if ( curG != null ) {
            curG.DrawPath(curPen, curPath);
        }
    }
    void fill()
    {
        if ( curG != null ) {
            curPath.FillMode = FillMode.Winding;
            curG.FillPath(curBrush, curPath);
        }
    }
    void eofill()
    {
        if ( curG != null ) {
            curPath.FillMode = FillMode.Alternate;
            curG.FillPath(curBrush, curPath);
        }
    }


    float cos(float degree)
    {
        return (float)Math.Cos((degree/180.0)*Math.PI);
    }
    float sin(float degree)
    {
        return (float)Math.Sin((degree/180.0)*Math.PI);
    }


    void Draw()
    {
        Bitmap bmp = CreateTransparentBitmap(MAX_SIZE, MAX_SIZE);
        curG = Graphics.FromImage(bmp);
        curG.SmoothingMode = SmoothingMode.AntiAlias;

        try {
            // ---- ここの中を変更すればよい
            curBrush = new LinearGradientBrush(new Point(0, 0), new Point(64, 64), Color.Red, Color.Yellow);

            newpath();
            for ( int deg=0 ; deg<360 ; deg+=60 ) {
                float x = 32+31*cos(deg);
                float y = 32+31*sin(deg);
                if ( deg == 0 ) {
                    moveto(x,y);
                }
                else {
                    lineto(x,y);
                }
            }
            closepath();
            fill();

            curBrush = new LinearGradientBrush(new Point(0, 0), new Point(64, 64), Color.Blue, Color.Black);
            newpath();
            moveto(32,36); // (32,32)が中央だが、文字列描画位置がずれるので微調整
            charpath("C#");
            closepath();
            fill();

            // ----
        }
        finally {
            curG.Dispose();
            curG = null;
        }

        int zoom = 2;
        Graphics g = Graphics.FromImage(pct.Image);
        g.FillRectangle(Brushes.White, 0, 0, pct.Image.Width, pct.Image.Height);
        g.DrawImage(bmp, 0, 0, bmp.Width*zoom, bmp.Height*zoom);
        g.Dispose();


        Icons icons;
        icons = new Icons();
        icons.AddIcon(bmp);
        icons.SaveToFile("Output.ico");

    }

    // 透過色で初期化
    Bitmap CreateTransparentBitmap(int width, int height)
    {
        Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
        BitmapData bd = bmp.LockBits(new Rectangle(0,0,width,height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);

        try {
            unsafe {
                // 書き込み
                byte* ptr = (byte*)bd.Scan0;
                for ( int y=0 ; y<height ; y++ ) {
                    for ( int x=0 ; x<width ; x++ ) {
                        ptr[y*bd.Stride + 4*x    ] = 0;// B
                        ptr[y*bd.Stride + 4*x + 1] = 0;// G
                        ptr[y*bd.Stride + 4*x + 2] = 0;// R
                        ptr[y*bd.Stride + 4*x + 3] = 0;// alpha = 0 (透過)
                    }
                }
            }
        }
        finally {
            bmp.UnlockBits(bd);
        }

        return bmp;
    }


    [STAThread]
    static void Main()
    {
        Application.Run(new EasyDraw());
    }
}

```

</div></details>


# メソッド名について

PostScript言語をまねてみた。


# コンパイル方法

`csc /unsafe EasyDrawIcon.cs IconUtility.cs`


# アイコンのキャッシュを更新する方法

同じファイル名のアイコンをつくると、アイコンの表示が更新されない。

コマンドプロンプトなどで
<b>Windows 10</b>では「`ie4uinit.exe -show`」、
<b>それ以外</b>のバージョンでは「`ie4uinit.exe -ClearIconCache`」
を実行すればよい。

参考サイト： https://news.mynavi.jp/article/20180131-windows_icon/