Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SetSystemCursor() for multiple cursors behavior

Tags:

c#

winforms

I am trying to change multiple cursors to Cross cursor. This is the code I am using for that matter:

[DllImport("user32.dll")]
static extern bool SetSystemCursor(IntPtr hcur, uint id);
[DllImport("user32.dll")]
static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern Int32 SystemParametersInfo(UInt32 uiAction, 
    UInt32 uiParam, String pvParam, UInt32 fWinIni);

//Normal cursor
private static uint OCR_NORMAL = 32512;
//The text selection (I-beam) cursor.
private static uint OCR_IBEAM = 32513;
//The cross-shaped cursor.
private static uint OCR_CROSS = 32515;

Then I use these two functions I made:

static public void ChangeCursors() {
    SetSystemCursor(LoadCursor(IntPtr.Zero, (int)OCR_CROSS), OCR_NORMAL);
    SetSystemCursor(LoadCursor(IntPtr.Zero, (int)OCR_CROSS), OCR_IBEAM);
}

static public void RevertCursors() {
    SystemParametersInfo(0x0057, 0, null, 0);
}

If I just use SetSystemCursor(LoadCursor(IntPtr.Zero, (int)OCR_CROSS), OCR_NORMAL);, everything works fine. The Normal cursor gets replaced by Cross cursor.

My problem is when I try to change multiple cursors to Cross cursor. If I call ChangeCursors(), the expected result would be Normal cursor AND I-beam cursor gets replaced by Cross cursor. But the result is something really weird.

When the software starts depending on the current state of the cursor when the program started, the following strange things happen:

  • If cursor was Normal when the software started, it changes to Cross (that's good). Also, I-beam is replaced with Normal (that's bad, it should be Cross).
  • If cursor was I-beam when the software started, it stays I-beam(that's bad, because it should be Cross). Then, by hovering over to where previously the cursor should be Normal it is now Cross (that's good). Then, if I hover over to where the cursor was I-beam 1 second ago, it magically changes to Normal (that's weird) and stays that way.

So, my question is, how can I change 2 or more Cursors to Cross cursor, using SetSystemCursor() ?

like image 278
dimitris93 Avatar asked Apr 21 '15 19:04

dimitris93


2 Answers

Dont get confused about the weird behaviour. It's just the cursors getting swapped each time when you Assign.

At first

Normal  ==  Normal
IBeam   ==  IBeam
Cross   ==  Cross

You Assign Normal = Cross

Normal  ==  Cross
IBeam   ==  IBeam
Cross   ==  Normal

And now assign IBeam = Cross (Which is Normal now)

Normal  ==  Cross
IBeam   ==  Normal
Cross   ==  IBeam

So for not letting it get swapped, you have to keep copies of all the cursors. I'll give you an example having Normal and IBeam changed to CROSS.

Program.cs

static class Program
{
    [DllImport("user32.dll")]
    static extern bool SetSystemCursor(IntPtr hcur, uint id);

    [DllImport("user32.dll")]
    static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName);

    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    private static extern Int32 SystemParametersInfo(UInt32 uiAction, UInt32
    uiParam, String pvParam, UInt32 fWinIni);

    [DllImport("user32.dll")]
    public static extern IntPtr CopyIcon(IntPtr pcur);

    private static uint CROSS = 32515;
    private static uint NORMAL = 32512;
    private static uint IBEAM = 32513;
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        uint[] Cursors = {NORMAL, IBEAM};

        for (int i = 0; i < Cursors.Length; i++)
            SetSystemCursor(CopyIcon(LoadCursor(IntPtr.Zero, (int)CROSS)), Cursors[i]);

        Application.Run(new Form1());
        SystemParametersInfo(0x0057, 0, null, 0);
    }
}
like image 56
Abdul Saleem Avatar answered Oct 11 '22 15:10

Abdul Saleem


SetSystemCursor replaces the system cursor given by the second argument (OCR_CROSS in this example) with the cursor in the first argument. So, you set the OCR_CROSS cursor first to Normal, then to IBeam, so in effect the cross-cursor is set to look like an IBeam.

The documentation also specifically says

The system destroys hcur [the first argument] by calling the DestroyCursor function. Therefore, hcur cannot be a cursor loaded using the LoadCursor function. To specify a cursor loaded from a resource, copy the cursor using the CopyCursor function, then pass the copy to SetSystemCursor.

Your code does this, so things could go wrong here, or at the least leak handles.


In-depth look

The SetSystemCursor is far more invasive then you might think. It actually swaps the global cursor-data of the specified cursor in the second argument by the cursor object in the first argument.

This has consequences. Say that you replaced IDC_WAIT by IDC_CROSS and then IDC_ARROW by IDC_WAIT (code in C):

HCURSOR hCursor = LoadCursor(0, MAKEINTRESOURCE(IDC_CROSS));
HCURSOR hCopyCursor = CopyCursor(hCursor);
SetSystemCursor(hCopyCursor, (DWORD)IDC_WAIT); // Replace Wait by Cross
HCURSOR hCursor2 = LoadCursor(0, MAKEINTRESOURCE(IDC_WAIT)); // Load whatever is in Wait and put it in Arrow
HCURSOR hCopyCursor2 = CopyCursor(hCursor2);
SetSystemCursor(hCopyCursor2, (DWORD)IDC_ARROW); // Replace Arrow by Wait.

Question: Is the Arrow cursor of the system a Cross or a Wait cursor?

Answer: It's a Cross cursor.

The reason this happens is because you actually swap the underlying cursor data, not just some reference, so LoadCursor reads the replaced cursor data.

This also shows why it gets very confusing when you don't make a copy of the cursor first: then the two cursors are getting swapped globally.

like image 45
MicroVirus Avatar answered Oct 11 '22 15:10

MicroVirus