Creating Custom Cursors in C#. Part 2: An InterOp-based Cursor.
In Part 1 , I presented C# code for creating an Icon-based custom “I-Beam” mouse cursor that sizes itself according to a document’s default font size. The Icon-based cursor was implemented in Managed Code, but disappears on a black background, because it’s actually an icon.
Now we explore an InterOp-based solution that properly inverts the screen background.
First we show how our cursor creation function is called from within your Form-derived class. The cursor creation function takes the document’s Font.Height property as an input parameter, creates the cursor, and returns a handle for the caller to use in a Cursor constructor:
1 2 |
IntPtr iCursHandle=InterOpCursorCreate(fon.Height); panel1.Cursor = new Cursor(iCursHandle); |
The next code block shows the minimum required using references, an InterOP declaration, and the cursor creation function. Comments explain how it works, but some concepts will be elaborated later:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
using System; using System.Drawing; using System.Windows.Forms; using System.Runtime.InteropServices; //InterOp declaration -- put in your Form-Derived Class: [DllImport("user32.dll")] public static extern IntPtr CreateCursor( IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvANDPlane, byte[] pvXORPlane ); // Cursor creation function -- put in your Form-Derived Class: public static IntPtr InterOpCursorCreate(int iFontHeightInPixels) { int i; // Cursor height and width scale from the font height: int iHeight = Convert.ToInt32(0.9F*iFontHeightInPixels); int iWidth=1+2*Convert.ToInt32(0.15F*iFontHeightInPixels); // This helps simplify the code by keeping the cursor's // centerline in the 1st byte of each mask row: if (iWidth > 15) iWidth = 15; // We must honor the driver's capabilities: int iAllowableWidth = SystemInformation.CursorSize.Width; int iAllowableHeight = SystemInformation.CursorSize.Height; // Limit the dimensions: if (iHeight > iAllowableHeight) iHeight = iAllowableHeight; if (iWidth > iAllowableWidth) { iWidth = iAllowableWidth; if ((iWidth % 2) != 0) iWidth--; // Enforce odd width } int iHalfWidth = iWidth / 2; // Create byte buffer for masks (one bit for each pixel): int iArrayLen = iAllowableWidth * iAllowableHeight / 8; byte[] byANDmask = new byte[iArrayLen]; byte[] byXORmask = new byte[iArrayLen]; // Clear all XOR mask bits initially: for (i = 0; i < iArrayLen; i++) { // Set all the AND mask bits so there will be no opacity: byANDmask[i] = 0xFF; // Clear all the XOR mask bits initially: byXORmask[i] = 0x00; } // Calculate number of bytes in one mask row: int iNumBytesPerRow = iAllowableWidth / 8; // Selectively set the XOR mask bits. // First, do cursor's center column, while avoiding serifs. // In center column, there's only one pixel "on" per row: byte by = (byte)(1 << (7 - iHalfWidth)); for (i = iHeight - 2; i > 0; i--) byXORmask[iNumBytesPerRow * i] = by; // Trim the serif pattern to length: uint uiSerif = 0xFFFE; // Widest serif has width = 15 for (i = iWidth; i < 15; i++) uiSerif <<= 1; // Calculate where to put notch in the serifs: uint uiNotchMask = 0x0100; for (i = iHalfWidth; i < 7; i++) uiNotchMask <<= 1; // Punch hole in serif pattern, for notch: uiSerif ^= uiNotchMask; // Extract left and right bytes of serif pattern: uint uiSerifLeft = (uiSerif & 0xFF00) >> 8; uint uiSerifRight = uiSerif & 0x00FF; // Calculate bottom serif's postion in XOR mask array: int iBottomSerifOffset = iNumBytesPerRow * (iHeight - 1); // Now, set both halves of top serif in the XOR mask: byXORmask[0] = (byte)uiSerifLeft; byXORmask[1] = (byte)uiSerifRight; // Ditto for bottom serif: byXORmask[iBottomSerifOffset] = (byte)uiSerifLeft; byXORmask[iBottomSerifOffset + 1] = (byte)uiSerifRight; // Create cursor via InterOp: IntPtr cursorHandle = CreateCursor( Process.GetCurrentProcess().Handle, // app. instance iHalfWidth, // hot spot X pos iHeight / 2, // hot spot Y pos iAllowableWidth, // cursor width acceptable to driver iAllowableHeight, // cursor height acceptable to driver byANDmask, // AND mask byXORmask // XOR mask ); // The caller needs the new handle: return cursorHandle; } |
How a Real Cursor Works:
Two masks govern the appearance and behavior of a cursor: an “AND” mask, and an “XOR” mask.
The AND mask sets the cursor’s opaque parts, where the screen does not “show through”.
The XOR mask determines where the cursor inverts the screen.
The mask names derive from the underlying Raster Operations that give the cursor its characteristic behavior. Screen pixels are RGB (Red/Green/Blue) values where each color component is a number in the range 0 to 255 inclusive. The cursor is rendered in two steps. First, the AND mask is combined with the screen pixels in a logical AND operation. Where the AND mask has a one-bit, this phase of rendering lets the screen pixel show. A zero bit in the AND mask hides the corresponding screen pixel. Our cursor does not hide any screen pixels, hence its bits are all ones.
The cursor’s inverting behavior comes from the XOR mask. Where this mask has a one-bit, the corresponding screen pixel is inverted. Black becomes white, and vice-versa. Colors invert according to the respective bit values of the RGB components of the screen pixel. It is the XOR mask that our cursor creation function carefully creates according to the needed size and shape.
Driver Compatibility:
The operating system uses one and only one cursor size — usually 32 x 32 bits. The actual dimensions must be obtained from the SystemInformation.CursorSize property. Smaller cursors simply use ones in the unused AND mask bits, and zeroes in the unused XOR mask bits.
In the InterOp CreateCursor() API call, specifying a width or height different from the SystemInformation.CursorSize values may appear to work, but certain problems arise that make it evident that the display driver really does not like the non-standard values. For example, when screen-capture programs such as IrfanView and CorelCAPTURE are used, non-standard cursor sizes do not render properly in the captured images. In other words, “What You Get Ain’t What You Saw” (WYGAWYS).
Mask “Bit-Packing”:
In the Icon-based code in Part 1, the logic for setting the pixels was easy to understand, because a pixel’s coordinates in the cursor’s bitmap were directly accessible. But, now we must deal with mask bits that are packed in a one-dimensional array of bytes. Hence, not only must we compute which bits in a byte to set, via relatively obscure logical operations, but we must also determine which byte of the mask array contains the bit of interest. For these reasons, the code uses some cryptic operators like &, |, ^, <<, and >> (and, or, “exclusive or”, left-shift, and right-shift).
A Simpler Example:
Instead of walking through the code’s XOR mask creation logic, a simpler example will illustrate the task at hand. We know what kind of bit-pattern we want in the XOR mask, and from this pattern we must generate a linear sequence of bytes. Suppose we wanted a square-shaped cursor of maximal size — say 32 bits wide by 32 bits tall, empty (transparent) inside, and having a one-pixel line width. The desired XOR mask bit pattern for this “inverting square” cursor would simply look like this:

But, those are bits. The code needs to specify the XOR mask as a sequence of bytes, as follows:

That is the kind of processing that the code shown above does for the I-Beam cursor, with the following result:
Result:
Here is the result, showing the I-Beam cursor on a black background:
![]()
Magnified, the cursor shows its “inversion magic” over the text, as well:
![]()
One small problem remains, which shall be left as an exercise for the reader. (In school, I dreaded hearing this). Since the cursor uses “inversion”, black becomes white, and vice-versa, but gray becomes gray, due to the idempotence of gray under the inversion operation. Hence, when the cursor is over a screen area of color having RGB=(128, 128, 128), the inversion creates a cursor of substanially the same shade of gray, RGB= (127, 127, 127), and our precious cursor disappears. This is undesirable, but incredibly, this is more or less standard behavior. Go figure.
Web Resources:
Kudos to Mike Chaliy, whose article got me on the right track:
http://csharparticles.blogspot.com/2005/03/custom-drawing-cursors.html
Other pertinent background:
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=63447&SiteID=1
http://www.rw-designer.com/forum/net-apps/152
http://blog.paranoidferret.com/index.php/2008/01/30/csharp-tutorial-how-to-use-custom-cursors/
http://www.csharpfriends.com/Forums/ShowPost.aspx?PostID=45050
http://www.devnewsgroups.net/group/microsoft.public.dotnet.framework.windowsforms/topic25109.aspx
Article link: http://www.hsys.com/CustomCursorArticlePart2.htm