none
Save Custom Cursor?

    Question

  • I have programmatically created myself a custom cursor using an external .bmp.  Does anyone know of a way to then save this cursor as a cursor file with the extension .cur therefore making it useful as a cursor in other projects?

     

    Thanks!!!

    Friday, June 06, 2008 7:09 PM

Answers

  • Hmya, the dreaded Bitmap.GetHicon(). I decided to roll up my sleeves and find a permanent solution for these cursor problems. There are other problems with the Cursor class too, it can't load animated cursors nor cursors that have more than 16 colors.

    Add a new class to your project and paste the code shown below. Use it as follows:

    To load a custom cursor from file:
      Cursor cur = CustomCursor.Load(@"c:\windows\cursors\handwait.ani");

    To convert an image to a custom cursor:
       Cursor cur = CustomCursor.FromImage(image, new Point(16, 16), new Point(0, 0))
    where:
      1st arg = Image or Bitmap to convert to a cursor
      2nd arg = location of pixel that is the cursor's hotspot
      3rd arg = location of pixel that has the background color

    To convert an image to a custom cursor and save it to disk:
      CursorStream stream = CustomCursor.Create(image, hotSpot, backGround)
      CustomCursor.Save(stream, @"c:\temp\custom.cur");
      // optional:
      Cursor cur = CustomCursor.FromStream(stream);

    Hope it works for you.

    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Reflection;

    public static class CustomCursor {

      public static Cursor Load(string path) {
        // Load cursor from <path>
        IntPtr handle = LoadImage(IntPtr.Zero, path, IMAGE_CURSOR, 0, 0, LR_LOADFROMFILE);
        if (handle == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
        Cursor retval = new Cursor(handle);
        // Fix handle ownership problem
        typeof(Cursor).GetField("ownHandle", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(retval, true);
        return retval;
      }

      public static Cursor FromImage(Image bmp, Point hotSpot, Point backGround) {
        // Create cursor from image <bmp>
        return FromStream(Create(bmp, hotSpot, backGround));
      }

      public static Cursor FromStream(CursorStream stream) {
        // Create cursor from stream
        string path = Path.GetTempFileName();
        Save(stream, path);
        Cursor retval = Load(path);
        File.Delete(path);
        return retval;
      }

      public static void Save(CursorStream stream, string path) {
        // Saves cursor to <path>
        using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
          fs.Write(stream.GetBuffer(), 0, (int)stream.Length);
      }

      public static CursorStream Create(Image bmp, Point hotSpot, Point backGround) {
        // Convert image <bmp> to a 256 color cursor stored in a stream
        if (bmp.Width > 256 || bmp.Height > 256) throw new ArgumentException("Image too large");
        if (hotSpot.X < 0 || hotSpot.X >= bmp.Width ||
            hotSpot.Y < 0 || hotSpot.Y >= bmp.Height) throw new ArgumentException("Invalid hot-spot");
        if (backGround.X < 0 || backGround.X >= bmp.Width ||
            backGround.Y < 0 || backGround.Y >= bmp.Height) throw new ArgumentException("Invalid background");

        // Encode to GIF to get 8bpp image
        CursorStream cvt = new CursorStream();
        bmp.Save(cvt, ImageFormat.Gif);
        cvt.Seek(0, SeekOrigin.Begin);
        // Then to BMP to get the color table etc.
        using (Image bmp8 = Image.FromStream(cvt)) {
          cvt = new CursorStream();
          bmp8.Save(cvt, ImageFormat.Bmp);
        }
        // Alrighty, we've got:
        // offset 0x0000:  BITMAPFILEHEADER
        // offset 0x000E:  BITMAPINFOHEADER, 40 bytes
        // offset 0x0036:  color table, 256 x 4 bytes
        // offset 0x0436:  bitmap bits, stride x height bytes

        // Write the .cur file header
        CursorStream ret = new CursorStream();
        BinaryWriter bw = new BinaryWriter(ret);
        bw.Write((short)0);   // Reserved, must be zero
        bw.Write((short)2);   // Type, 2 = cursor
        bw.Write((short)1);   // Number of images
        // Write the .cur image directory
        byte width = bmp.Width == 256 ? (byte)0 : (byte)bmp.Width;
        byte height = bmp.Height == 256 ? (byte)0 : (byte)bmp.Height;
        int stride = 4 * ((bmp.Width + 3) / 4);
        int bitStride = 4 * ((width / 8 + 3) / 4);
        bw.Write(width);
        bw.Write(height);
        bw.Write((byte)0);    // 0 = 256 colors
        bw.Write((byte)0);    // Reserved
        bw.Write((short)hotSpot.X);
        bw.Write((short)hotSpot.Y);
        bw.Write(stride * height + 4 * 256 + height * bitStride + 40);      // Size of image
        bw.Write(0x16);       // Offset to image
        // Write BITMAPINFOHEADER, we need double the height
        cvt.Seek(0x0e, SeekOrigin.Begin);
        BinaryReader br = new BinaryReader(cvt);
        int tmpi;
        byte[] tmpb;
        tmpi = br.ReadInt32(); bw.Write(tmpi);  // biSize
        tmpi = br.ReadInt32(); bw.Write(tmpi);  // biWidth
        tmpi = br.ReadInt32(); bw.Write(2 * tmpi);  // biHeight
        tmpb = br.ReadBytes(20); bw.Write(tmpb);  // rest of header
        int colors = br.ReadInt32(); bw.Write(256);
        tmpi = br.ReadInt32(); bw.Write(tmpi);
        // Write color table
        tmpb = br.ReadBytes(4 * colors);
        bw.Write(tmpb);
        for (int filler = colors; filler < 256; ++filler) bw.Write((int)0);

        // Find out what color the transparency got converted to
        cvt.Seek(0x36 + 4 * colors + backGround.Y * stride + backGround.X, SeekOrigin.Begin);
        byte transparent = br.ReadByte();
       
        // We'll need the color index of black for the transparency
        // Fairly sure that GIF always generates black at index 0, but not 100%
        int black;
        cvt.Seek(0x36, SeekOrigin.Begin);
        for (black = 0; black < colors; ++black) {
          int color = br.ReadInt32();
          if ((color & 0xffffff) == 0) break;
        }
        if (black >= colors) throw new ArgumentException("Converted image is missing black");

        // Convert the bitmap bytes.  We're building the AND mask as we convert each scan
        cvt.Seek(0x36 + 4 * colors, SeekOrigin.Begin);
        byte[] andMask = new byte[bitStride * height];
        int andIndex;
        for (int y = 0; y < height; ++y) {
          andIndex = bitStride * y;
          int bit = 0;
          for (int x = 0; x < stride; ++x) {
            byte pixel = br.ReadByte();
            bw.Write(pixel == transparent ? (byte)black : pixel);
            if (pixel != transparent) andMask[andIndex] <<= 1;
            else andMask[andIndex] = (byte)((andMask[andIndex] << 1) | 1);
            if (++bit % 8 == 0) andIndex++;
          }
          for (; bit % 8 != 0; ++bit)
            andMask[andIndex] <<= 1;
        }
        // Write the AND mask
        bw.Write(andMask, 0, andMask.Length);

        // Done!
        ret.Seek(0, SeekOrigin.Begin);
        return ret;
      }

      // P/Invoke declarations
      private const int LR_LOADFROMFILE = 0x10;
      private const int IMAGE_CURSOR = 2;
      [DllImport("user32.dll", SetLastError = true)]
      private static extern IntPtr LoadImage(IntPtr hInst, string path, int type, int width, int height, int flags);
      [DllImport("user32.dll")]
      private static extern bool DestroyCursor(IntPtr handle);
    }

    // Type-safe stream handling
    public class CursorStream : MemoryStream { }


    Saturday, June 07, 2008 6:42 PM
    Moderator

All replies

  • Neither Win32 nor the .NET framework have any method to save a Cursor.  GDI+ advertizes the Icon format but that doesn't actually work.  It is a tricky format, it requires three bitmaps and a hot-spot.

    However, a .cur is pretty close to a .ico file.  And the Cursor class constructor is willing to load a file that actually contains an icon.  Try this for example:

    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Windows.Forms;
    using System.IO;
    using System.Runtime.InteropServices;

    namespace WindowsFormsApplication1 {
      public partial class Form1 : Form {
        public Form1() {
          InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e) {
          Bitmap bmp = Properties.Resources.Image1;
          bmp.MakeTransparent(Color.White);
          IntPtr hIcon = bmp.GetHicon();
          Icon ico = Icon.FromHandle(hIcon);
          Cursor cur = new Cursor(hIcon);
          using (FileStream fs = new FileStream(@"c:\temp\test.cur", FileMode.Create, FileAccess.Write))
            ico.Save(fs);
          cur.Dispose();
          ico.Dispose();
          DestroyIcon(hIcon);

          // Test it
          cur = new Cursor(@"c:\temp\test.cur");
          this.Cursor = cur;
        }
        [DllImport("user32.dll")]
        extern static bool DestroyIcon(IntPtr handle);
      }
    }

    I added a small bitmap to the resources, named Image1.
    Friday, June 06, 2008 8:36 PM
    Moderator
  • This solution sort of works.  The colors from my original cursor to my new saved then reopened cursor is quite different.  Also, when I try to add my new cursor to a project then try to open the cursor file, VS won't open the file.

     

    Thanks for the post!!!

     

    Friday, June 06, 2008 9:36 PM
  • Just add it as an .ico then, that's what it really is.  Color shifts are par for the course.  I don't know what you are using to create your icon/cursor but Bitmap.GetHicon() does a bummer job on the bitmap, producing an icon with only 16 colors.  Unless you use the primary ones, it is not going to look good.  Post the code you use to create the icon/cursor and I'll have a look.
    Friday, June 06, 2008 10:46 PM
    Moderator
  • Here's the code I'm using to create the cursor:

     

    Code Snippet

    namespace CreateCursor

    {

    public struct IconInfo

    {

    public bool fIcon;

    public int xHotspot;

    public int yHotspot;

    public IntPtr hbmMask;

    public IntPtr hbmColor;

    }

    public partial class Form1 : Form

    {

    Cursor myCursor;

     

    public Form1()

    {

    InitializeComponent();

    }

     

    private void btnCreate_Click(object sender, EventArgs e)

    {

    Stream s = this.GetType().Assembly.GetManifestResourceStream("CreateCursor.Faux_Cursor_Small.gif");

    Bitmap bitmap = new Bitmap(s);

    s.Close();

    myCursor = CreateCursor(bitmap, 15, 15);

    this.Cursor = myCursor;

    bitmap.Dispose();

    }

     

    [DllImport("user32.dll")]

    public static extern IntPtr CreateIconIndirect(

    ref IconInfo icon);

     

    [DllImport("user32.dll")]

    [return: MarshalAs(UnmanagedType.Bool)]

    public static extern bool GetIconInfo(IntPtr hIcon,

    ref IconInfo pIconInfo);

     

    public static Cursor CreateCursor(Bitmap bmp,

    int xHotSpot, int yHotSpot)

    {

    IconInfo tmp = new IconInfo();

    GetIconInfo(bmp.GetHicon(), ref tmp);

    tmp.xHotspot = xHotSpot;

    tmp.yHotspot = yHotSpot;

    tmp.fIcon = false;

    return new Cursor(CreateIconIndirect(ref tmp));

    }

    }

    }

     

     

     

    Friday, June 06, 2008 11:56 PM
  • Hmya, the dreaded Bitmap.GetHicon(). I decided to roll up my sleeves and find a permanent solution for these cursor problems. There are other problems with the Cursor class too, it can't load animated cursors nor cursors that have more than 16 colors.

    Add a new class to your project and paste the code shown below. Use it as follows:

    To load a custom cursor from file:
      Cursor cur = CustomCursor.Load(@"c:\windows\cursors\handwait.ani");

    To convert an image to a custom cursor:
       Cursor cur = CustomCursor.FromImage(image, new Point(16, 16), new Point(0, 0))
    where:
      1st arg = Image or Bitmap to convert to a cursor
      2nd arg = location of pixel that is the cursor's hotspot
      3rd arg = location of pixel that has the background color

    To convert an image to a custom cursor and save it to disk:
      CursorStream stream = CustomCursor.Create(image, hotSpot, backGround)
      CustomCursor.Save(stream, @"c:\temp\custom.cur");
      // optional:
      Cursor cur = CustomCursor.FromStream(stream);

    Hope it works for you.

    using System;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.ComponentModel;
    using System.Windows.Forms;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Reflection;

    public static class CustomCursor {

      public static Cursor Load(string path) {
        // Load cursor from <path>
        IntPtr handle = LoadImage(IntPtr.Zero, path, IMAGE_CURSOR, 0, 0, LR_LOADFROMFILE);
        if (handle == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
        Cursor retval = new Cursor(handle);
        // Fix handle ownership problem
        typeof(Cursor).GetField("ownHandle", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(retval, true);
        return retval;
      }

      public static Cursor FromImage(Image bmp, Point hotSpot, Point backGround) {
        // Create cursor from image <bmp>
        return FromStream(Create(bmp, hotSpot, backGround));
      }

      public static Cursor FromStream(CursorStream stream) {
        // Create cursor from stream
        string path = Path.GetTempFileName();
        Save(stream, path);
        Cursor retval = Load(path);
        File.Delete(path);
        return retval;
      }

      public static void Save(CursorStream stream, string path) {
        // Saves cursor to <path>
        using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
          fs.Write(stream.GetBuffer(), 0, (int)stream.Length);
      }

      public static CursorStream Create(Image bmp, Point hotSpot, Point backGround) {
        // Convert image <bmp> to a 256 color cursor stored in a stream
        if (bmp.Width > 256 || bmp.Height > 256) throw new ArgumentException("Image too large");
        if (hotSpot.X < 0 || hotSpot.X >= bmp.Width ||
            hotSpot.Y < 0 || hotSpot.Y >= bmp.Height) throw new ArgumentException("Invalid hot-spot");
        if (backGround.X < 0 || backGround.X >= bmp.Width ||
            backGround.Y < 0 || backGround.Y >= bmp.Height) throw new ArgumentException("Invalid background");

        // Encode to GIF to get 8bpp image
        CursorStream cvt = new CursorStream();
        bmp.Save(cvt, ImageFormat.Gif);
        cvt.Seek(0, SeekOrigin.Begin);
        // Then to BMP to get the color table etc.
        using (Image bmp8 = Image.FromStream(cvt)) {
          cvt = new CursorStream();
          bmp8.Save(cvt, ImageFormat.Bmp);
        }
        // Alrighty, we've got:
        // offset 0x0000:  BITMAPFILEHEADER
        // offset 0x000E:  BITMAPINFOHEADER, 40 bytes
        // offset 0x0036:  color table, 256 x 4 bytes
        // offset 0x0436:  bitmap bits, stride x height bytes

        // Write the .cur file header
        CursorStream ret = new CursorStream();
        BinaryWriter bw = new BinaryWriter(ret);
        bw.Write((short)0);   // Reserved, must be zero
        bw.Write((short)2);   // Type, 2 = cursor
        bw.Write((short)1);   // Number of images
        // Write the .cur image directory
        byte width = bmp.Width == 256 ? (byte)0 : (byte)bmp.Width;
        byte height = bmp.Height == 256 ? (byte)0 : (byte)bmp.Height;
        int stride = 4 * ((bmp.Width + 3) / 4);
        int bitStride = 4 * ((width / 8 + 3) / 4);
        bw.Write(width);
        bw.Write(height);
        bw.Write((byte)0);    // 0 = 256 colors
        bw.Write((byte)0);    // Reserved
        bw.Write((short)hotSpot.X);
        bw.Write((short)hotSpot.Y);
        bw.Write(stride * height + 4 * 256 + height * bitStride + 40);      // Size of image
        bw.Write(0x16);       // Offset to image
        // Write BITMAPINFOHEADER, we need double the height
        cvt.Seek(0x0e, SeekOrigin.Begin);
        BinaryReader br = new BinaryReader(cvt);
        int tmpi;
        byte[] tmpb;
        tmpi = br.ReadInt32(); bw.Write(tmpi);  // biSize
        tmpi = br.ReadInt32(); bw.Write(tmpi);  // biWidth
        tmpi = br.ReadInt32(); bw.Write(2 * tmpi);  // biHeight
        tmpb = br.ReadBytes(20); bw.Write(tmpb);  // rest of header
        int colors = br.ReadInt32(); bw.Write(256);
        tmpi = br.ReadInt32(); bw.Write(tmpi);
        // Write color table
        tmpb = br.ReadBytes(4 * colors);
        bw.Write(tmpb);
        for (int filler = colors; filler < 256; ++filler) bw.Write((int)0);

        // Find out what color the transparency got converted to
        cvt.Seek(0x36 + 4 * colors + backGround.Y * stride + backGround.X, SeekOrigin.Begin);
        byte transparent = br.ReadByte();
       
        // We'll need the color index of black for the transparency
        // Fairly sure that GIF always generates black at index 0, but not 100%
        int black;
        cvt.Seek(0x36, SeekOrigin.Begin);
        for (black = 0; black < colors; ++black) {
          int color = br.ReadInt32();
          if ((color & 0xffffff) == 0) break;
        }
        if (black >= colors) throw new ArgumentException("Converted image is missing black");

        // Convert the bitmap bytes.  We're building the AND mask as we convert each scan
        cvt.Seek(0x36 + 4 * colors, SeekOrigin.Begin);
        byte[] andMask = new byte[bitStride * height];
        int andIndex;
        for (int y = 0; y < height; ++y) {
          andIndex = bitStride * y;
          int bit = 0;
          for (int x = 0; x < stride; ++x) {
            byte pixel = br.ReadByte();
            bw.Write(pixel == transparent ? (byte)black : pixel);
            if (pixel != transparent) andMask[andIndex] <<= 1;
            else andMask[andIndex] = (byte)((andMask[andIndex] << 1) | 1);
            if (++bit % 8 == 0) andIndex++;
          }
          for (; bit % 8 != 0; ++bit)
            andMask[andIndex] <<= 1;
        }
        // Write the AND mask
        bw.Write(andMask, 0, andMask.Length);

        // Done!
        ret.Seek(0, SeekOrigin.Begin);
        return ret;
      }

      // P/Invoke declarations
      private const int LR_LOADFROMFILE = 0x10;
      private const int IMAGE_CURSOR = 2;
      [DllImport("user32.dll", SetLastError = true)]
      private static extern IntPtr LoadImage(IntPtr hInst, string path, int type, int width, int height, int flags);
      [DllImport("user32.dll")]
      private static extern bool DestroyCursor(IntPtr handle);
    }

    // Type-safe stream handling
    public class CursorStream : MemoryStream { }


    Saturday, June 07, 2008 6:42 PM
    Moderator
  • Wow, thanks so much for writing all that.  I actually get an EndOfStreamException in the Create function when building the AND mask, specifically at this point:

    byte pixel = br.ReadByte();

    My height is 31 (original .bmp and .gif, tried both, are 31x31) and the stride is 32.  Before I start the nested for loops for building the AND mask, the br position is 1078 with a length of 1302 which is only 224 bytes but but I'm trying to read 31x32 bytes, or 961 bytes, which explains why I get the EndOfStreamException.  What you've written is out of the realm of anything I've ever written before and I tried to find definitions of the file formats for .bmp and .cur but I'm haivng a hard time translating that to your code, so I'm having a hard time debugging this problem.

     

    Just for reference, this is what I'm doing to convert an image to a cursor, called from a button click on my form, perhaps my method of creating the Image to pass to your function for creating the cursor is the problem?:

     

    //Create Image Element

    Stream s = this.GetType().Assembly.GetManifestResourceStream("CreateCursor.Faux_Cursor_Small.bmp");

    Image myImage = new Bitmap(s);

     

    //Create cursor

    Cursor myCursor = CustomCursor.FromImage(myImage, new Point(15, 15), new Point(0, 0));

     

    Thank you very much.  You have been extremely helpful and I greatly appreciate all that you've done (and taught me).

    Monday, June 09, 2008 9:36 PM
  • Something must have gone wrong with the Image to GIF to Bitmap conversion.  I can't phantom what that might be. The length is indeed too short, maybe the color table isn't long enough.  I'm not checking for that and assume 256 colors.  Possibly, the source image has a limited number of colors, please post the .bmp on a file sharing service so we can try to repro the problem ourselves.  Or you can email it to me, address is in my profile (|Monkeytail| = @). 

    You can find the .ico/.cur file format documented in this Wikipedia article.
    Monday, June 09, 2008 11:21 PM
    Moderator
  • Yes, that's what the problem was.  You are using an 8bpp image with a color table with 64 colors.  Not quite sure what you're doing, you could never create such an image in .NET code.  It should just be easier to create the cursor with VS.

    I updated the code in my previous post to work with a small color table.
    Tuesday, June 10, 2008 4:55 AM
    Moderator
  • Ok, thanks.  I created the cursor in CorelDraw then just exported it as a .BMP.  Perhaps I should check what I'm doing creating my graphic.

     

    Just for my reference, would you mind sharing with me what you did to determine that my image was 8bpp and a color table of 64 colors?

     

    THANK YOU!!!!!

     

     

     

    Tuesday, June 10, 2008 3:41 PM
  • I looked at the BITMAPINFOHEADER to find out.  In spite of the work, I'd have to recommend you do *not* use my code.  You should just create a .cur file with the IDE.  Project + Add New Item, select "Cursor File".  That brings up the cursor editor.  Image + New Image type, click Custom.  Enter 31 x 31, 256 Colors.  OK + OK.  Start + Run, "mspaint", OK.  Open your image.  Type Ctrl+A, Ctrl+C to copy the image.  Switch back to the IDE, type Ctrl+V.  The way I got it, the transparency was already set.  Odd.  If necessary, flood-fill the background of the image with the teal screen color.  File + Save As and pick a good place to store the .cur file.  You can now use it in any other project.  If it doesn't load properly, I didn't check, you can always use my CustomCursor.Load() method.

    Fare well.

    Tuesday, June 10, 2008 11:01 PM
    Moderator