I’ve found a workaround where I hide the default cursor and draw my own in OnMouseMove, but this means the whole viewport needs to be redrawn each time the mouse is moved. Are there more efficient ways of doing this, maybe?
I’m developing a custom sculpting plugin and find the default arrow a bit too obtrusive. Here, you can see the workaround I came up with, using a custom conduit updated on OnMouseMove. Ideally I’d like to have the option to just change the default cursor outside of the Get() command, though.
@stevebaer, @dale, I’m reviving this topic as I still haven’t found any solution.
From my investigation, it seems that Rhino is constantly enforcing it’s native cursor on each MouseMove in a callback. Setting a custom cursor in this override mostly works, but there is a flicker when the default one shows for a split second to be immediately replaced by its custom counterpart.
As mentioned previously _DragMode sets custom cursors. Is there a way to do this via the c++ SDK? I tried listening to the message loop and intercepting cursor changes but the flicker remains.
/// <summary>
/// Manages custom cursor behavior for Rhino viewport windows by intercepting and handling
/// Windows cursor messages through window procedure (WndProc) hooking.
/// </summary>
public class ViewportCursorOverride
{
[DllImport("user32.dll")]
private static extern IntPtr SetCursor(IntPtr hCursor);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, int msg, IntPtr wParam,IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex,WndProcDelegate newProc);
/// <summary>
/// Windows message sent when the cursor needs to be set.
/// </summary>
private const int WM_SETCURSOR = 0x0020;
/// <summary>
/// Index used to modify a window's procedure in the Windows API.
/// </summary>
private const int GWL_WNDPROC = -4;
/// <summary>
/// Delegate for the window procedure callback that processes window messages.
/// </summary>
private delegate IntPtr WndProcDelegate(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
/// <summary>
/// Static instance of the WndProc delegate to prevent garbage collection.
/// </summary>
private static readonly WndProcDelegate _wndProcDelegate = CustomWndProc;
/// <summary>
/// Stores the original window procedures for each hooked window handle.
/// </summary>
private static readonly Dictionary<IntPtr, IntPtr> _oldWndProcs = new Dictionary<IntPtr, IntPtr>();
/// <summary>
/// Handle to the cursor that will be used for the viewport.
/// </summary>
private static IntPtr _cursorHandle;
/// <summary>
/// Custom window procedure that handles cursor-related window messages.
/// </summary>
/// <param name="hWnd">Handle to the window.</param>
/// <param name="msg">Message identifier.</param>
/// <param name="wParam">Additional message-specific information.</param>
/// <param name="lParam">Additional message-specific information.</param>
/// <returns>Result of processing the message.</returns>
private static IntPtr CustomWndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
{
if (msg == WM_SETCURSOR)
{
SetCursor(_cursorHandle);
return IntPtr.Zero;
}
if (_oldWndProcs.TryGetValue(hWnd, out var oldWndProc))
return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
return IntPtr.Zero;
}
/// <summary>
/// Hooks the window procedure for all active Rhino viewport windows to enable custom cursor behavior.
/// This should be called when custom cursor behavior needs to be enabled.
/// </summary>
/// <param name="cursorHandle">Handle to the cursor that should be displayed in the viewport.</param>
public static void HookAllViews(IntPtr cursorHandle)
{
_cursorHandle = cursorHandle;
var doc = RhinoDoc.ActiveDoc;
if (doc == null)
return;
foreach (var view in doc.Views)
{
IntPtr hWnd = view.Handle;
if (_oldWndProcs.ContainsKey(hWnd))
return;
IntPtr oldWndProc = SetWindowLongPtr(hWnd, GWL_WNDPROC, _wndProcDelegate);
if (oldWndProc != IntPtr.Zero)
_oldWndProcs[hWnd] = oldWndProc;
}
}
/// <summary>
/// Restores the original window procedure for all previously hooked viewport windows.
/// This should be called when custom cursor behavior needs to be disabled.
/// </summary>
public static void UnhookAllViews()
{
var doc = RhinoDoc.ActiveDoc;
if (doc == null)
return;
foreach (var kvp in _oldWndProcs)
{
var viewHandle = kvp.Key;
var oldWndProc = kvp.Value;
if (viewHandle != IntPtr.Zero && oldWndProc != IntPtr.Zero)
{
SetWindowLongPtr(viewHandle, GWL_WNDPROC,
Marshal.GetDelegateForFunctionPointer<WndProcDelegate>(oldWndProc));
}
}
_oldWndProcs.Clear();
}
}
Thank you for sharing! I’ve been looking to do the same and arrived here as well but didn’t finish the hooking code yet. This is helpful, thanks again!