Help us understand the problem. What is going on with this article?

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

やれること

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

image.png

注意事項

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

ソースコード

以前のコードをちょっとリファクタリングして、アイコンデータ作成部分をIconUtility.csとして分離した。

ソースコード - IconUtility.cs
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;
        }
    }
}

ソースコード - EasyDrawIcon.cs
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());
    }
}

メソッド名について

PostScript言語をまねてみた。

コンパイル方法

csc /unsafe EasyDrawIcon.cs IconUtility.cs

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

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

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

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away