Ryan Griggs
Ryan Griggs

Reputation: 2758

How to hide the mouse cursor for all windows in C#?

TLDR: How to programmatically hide the mouse cursor for entire desktop no matter the application that has focus?

Even though I have "Hide pointer while typing" enabled in Windows Mouse Options, it doesn't work for most windows. It only seems to work on windows that use the standard Windows text box control (such as notepad.exe). Everywhere else, the cursor stays right on top of whatever I'm typing.

I want to programmatically hide the mouse cursor across the entire desktop no matter what application has focus, when I execute a specific action (for example, a hotkey is pressed). Then, when the mouse is moved, I want the cursor to reappear.

I can handle the hotkey etc. I just need to know how to hide the cursor.

What I have tried:

Currently my workaround is to listen for the Spacebar to be pressed (implies I'm typing more than a few characters), then move the mouse cursor to the top corner of the desktop, out of the way. However, this is not an ideal solution as it requires the mouse to be moved a lot to return to the original position, as well as messing up mouse position in apps such as Sketchup when entering dimensions containing spaces. I would prefer that the cursor simply become invisible in its current location, then reappear at the same location when moving the mouse, as it is supposed to work with the "Hide pointer when typing" option.

I'm not tied to C# but it's just easy to compile into an executable for Windows, which I can launch on Windows startup.

Thank you for any help!

Upvotes: 1

Views: 1780

Answers (3)

Oliver Henriksson
Oliver Henriksson

Reputation: 94

This is a simpler AHK solution to contrast with Evgeny's answer. By using the SystemCursor function from the DllCall docs the script can be massively simplified. Note though that this is definetly not as fully featured, it very simply hides the cursor when any keyboard key is pressed, and reveals it when the mouse is used, and no more.

InstallKeybdHook
InstallMouseHook
CoordMode("mouse", "screen")

OnExit (*) => SystemCursor("Show")  ; Ensure the cursor is made visible when the script exits

SetTimer(Main, 10)

Main() {

   if(A_TimeIdleKeyboard < 25)
      ;A keyboard key has been pressed, hide cursor
      SystemCursor("Hide")

   else if(A_TimeIdleMouse < 25)
      ;Mouse have been used, show cursor
      SystemCursor("Show")
}    

SystemCursor(cmd) { ; cmd = "Show|Hide|Toggle|Reload"

   static visible := true, c := Map()
   static sys_cursors := [32512, 32513, 32514, 32515, 32516, 32642, 32643, 32644, 32645, 32646, 32648, 32649, 32650]

   if (cmd = "Reload" or !c.Count) { ; Reload when requested or at first call.
   
      for i, id in sys_cursors {          
         h_cursor := DllCall("LoadCursor", "Ptr", 0, "Ptr", id)
         h_default := DllCall("CopyImage", "Ptr", h_cursor, "UInt", 2, "Int", 0, "Int", 0, "UInt", 0)
         h_blank := DllCall("CreateCursor", "Ptr", 0, "Int", 0, "Int", 0, "Int", 32, "Int", 32, "Ptr", Buffer(32 * 4, 0xFF), "Ptr", Buffer(32 * 4, 0))
         c[id] := { default: h_default, blank: h_blank }
      }
   }

   switch cmd {       
      case "Show": visible := true
      case "Hide": visible := false
      case "Toggle": visible := !visible
      default: return
   }

   for id, handles in c {       
      h_cursor := DllCall("CopyImage", "Ptr", visible ? handles.default : handles.blank, "UInt", 2, "Int", 0, "Int", 0, "UInt", 0)
      DllCall("SetSystemCursor", "Ptr", h_cursor, "UInt", id)
   }
}

Upvotes: 0

johnnyjob
johnnyjob

Reputation: 390

While writing a similar utility I found that playing with Cursor.Hide (by moving your app' window right under the cursor) doesn't work well with some 3rd party apps (e.g. Microsoft Office). It turns out that this technique produces additional OnHover events which break internal logic of MS Office, causing some popups/menus to stop working properly.

Also, moving the cursor to the corner of a screen is not any better because it causes similar issues.

So I ended up with the following logic:

  1. Detect OnMouseUp event globally. In the most cases you want to hide the cursor after you click on some text.
  2. Detect if the cursor is I-Beam. It's a safety check that prevents hiding cursor every time you click somewhere.
  3. Replace the cursor image with completely transparent bitmap globally.
  4. Restore the cursor in OnMouseMove event handler.

Here's a working app: https://github.com/johnnyjob/single-cursor

Upvotes: 1

Evgeny
Evgeny

Reputation: 113

Here is the AutoHotkey solution to this, though since this just calls Windows APIs (for the hide/show part, otherwise the script is a bit more complicated in using UIA framework to track editable fields not to hide a pointer when you are not typing and also tracking mouse movements and other keybinding-related machinery) you can just as well translate it to C# (or just launch the AHK script on Windows start)

It uses the same logic that Jimi suggested (iterating through system cursors and replacing their pointers with blank images), although has a tweak to fix an issue when your pointer has a scale size > 1 (it becomes blurry if you simply try to replace it). However, this doesn't work for custom app mouse pointers, only the ones you see in the system settings (IArrow, Hand, etc.)

Alternatively, it uses a conceptually cleaner method of attaching an empty gui element to an app's window and sending a hide pointer ShowCursor command

Full standalone script source with the libraries that it depends on

The function that hides the pointer

    sys🖰Pointer(OnOff := On) {
      global is🖰PointerHidden
      static C := win32Constant.Misc ; various win32 API constants
    
      static hCur,AndMask,XorMask
      , isInit  := false, toShow := 1, toHide := 2
      , lrDef   := C.lrShared | C.lrDefColor                ; lrDefSz
      , lcDef   := C.lrShared | C.lrDefColor | C.lrCcSrc    ; lrDefSz
    
      if ( (OnOff = Off)
        or (OnOff = Toggle and (not is🖰PointerHidden
                             or not isInit)) ) { ; hide on first init call as well
        ; dbgTT(dbgMin:=0, Text:='toHide', Time:=1,id:=6,X:=0,Y:=150)
        changeTo := toHide  ; use hCur_blank cursors
      } else {
        ; dbgTT(dbgMin:=0, Text:='toShow', Time:=1,id:=8,X:=0,Y:=250)
        changeTo := toShow  ; use hCur_saved cursors
      }
    
      static curSID     := [ ;system_cursors learn.microsoft.com/en-us/windows/win32/menurc/about-cursors
          Arrow         := 32512    ; IDC_ARROW     MAKEINTRESOURCE(32512)   Normal select
        , Ibeam         := 32513    ; IDC_IBEAM     MAKEINTRESOURCE(32513)   Text select
        , Wait          := 32514    ; IDC_WAIT      MAKEINTRESOURCE(32514)   Busy
        , Cross         := 32515    ; IDC_CROSS     MAKEINTRESOURCE(32515)   Precision select
        , UpArrow       := 32516    ; IDC_UPARROW   MAKEINTRESOURCE(32516)   Alternate select
        , Size⤡         := 32642    ; IDC_SIZENWSE  MAKEINTRESOURCE(32642)   Diagonal resize 1
        , Size⤢         := 32643    ; IDC_SIZENESW  MAKEINTRESOURCE(32643)   Diagonal resize 2
        , Size↔         := 32644    ; IDC_SIZEWE    MAKEINTRESOURCE(32644)   Horizontal resize
        , Size↕         := 32645    ; IDC_SIZENS    MAKEINTRESOURCE(32645)   Vertical resize
        , Size⤨         := 32646    ; IDC_SIZEALL   MAKEINTRESOURCE(32646)   Move
        , No            := 32648    ; IDC_NO        MAKEINTRESOURCE(32648)   Unavailable
        , Hand          := 32649    ; IDC_HAND      MAKEINTRESOURCE(32649)   Link select
        ; ↓ not in      OCR_NORMAL  so can't be a restore target? learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setsystemcursor. Or just a doc omission?
        , AppStarting   := 32650    ; IDC_APPSTARTING   MAKEINTRESOURCE(32650)   Working in background
        , Help          := 32651    ; IDC_HELP          MAKEINTRESOURCE(32651)   Help select
        , Pin           := 32671    ; IDC_PIN           MAKEINTRESOURCE(32671)   Location select
        , Person        := 32672    ; IDC_PERSON        MAKEINTRESOURCE(32672)   Person select
        ;, _handwrite   := 32631    ;                   MAKEINTRESOURCE(32631)   Handwriting
      ]
       , hCursors := Array()
      hCursors.Capacity := curSID.Length
    
      static sys := helperSystem
      ; Get mouse pointer actual size (https://stackoverflow.com/a/65534381)
        ; GetIconInfo will return a bitmap sized for the primary display only
          ; If your main display is 150%, but the cursor is on a 100% secondary monitor, you'll get an incorrect 48x48 bitmap instead of 32x32
        ; 1 get monitor DPI         via GetDpiForMonitor
        ; 2 get proper icon size    via GetSystemMetricsForDpi
        ; 3 scale by the "cursor magnification" settings from accessibility
      dpi🖥️        := sys.getDPI🖥️(), dpi🖥️x:=dpi🖥️[1], dpi🖥️y:=dpi🖥️[2]                                          ; 1) monitor dpi
      sysCurMagniF  := RegRead('HKEY_CURRENT_USER\SOFTWARE\Microsoft\Accessibility','CursorSize',1)                 ; 2) pointer size @ Settings/Ease of Access/Mouse pointer
      dpi🖰Pointer  := sys.getDPI🖰Pointer(dpi🖥️x), width🖰Pointer:=dpi🖰Pointer[1], height🖰Pointer:=dpi🖰Pointer[2]  ;  3) get dpi-scaled system metric for mouse cursor size learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetricsfordpi
      cx            := sysCurMagniF * width🖰Pointer, cy := sysCurMagniF * height🖰Pointer ; LoadImageW
      cxc           := 0, cyc := 0 ; copy
      ; cxc         := cx, cyc := cy ; copy
    
      if    (OnOff = Init or isInit = false) {  ; init when requested or at first call
        dbgTT(dbgMin:=3, Text:='init', Time:=2,id:=5,X:=0,Y:=450)
        hCur    := Buffer( 4*A_PtrSize, 1)      ;
        AndMask := Buffer(32*A_PtrSize, 0xFF)   ;
        XorMask := Buffer(32*A_PtrSize, 0)      ;
        loop curSID.Length {
          hCur := DllCall('LoadImageW' ; ↓ LoadImage ret HANDLE to the newly loaded image; NULL on error, use GetLastError
            , 'Ptr',0                       ; opt HINSTANCE hInst   handle to the module DLL/EXE that contains image to be loaded
            ,'uint',curSID[A_Index]         ; LPCWSTR   name        if ↑Null and fuLoad≠lrLOADFROMFILE, predefined image to load
            ,'uint',C.imgCursor             ; uint      type        type of image to be loaded
            , 'Int',cx, 'Int',cy            ; int       cx|xy       icon/cursor's width|height  px
              ; 0 & fuLoad=lrDefSz                                  use SM_CXICON/CURSOR (/Y) system metric value to set W/H
              ; 0 & not    lrDefSz                                  use actual resource height
            ,'uint',lrDef                   ; uint      fuLoad
            , 'Ptr')                        ; learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadimagew
          hCur_saved := DllCall("CopyImage" ; create a new image (icon/cursor/bitmap) and copy its attributes to the new one. Stretch the bits to fit the desired size if needed, learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-copyimage
            ,"Ptr" ,hCur                    ; HANDLE    h
            ,"uint",C.imgCursor             ; uint      type
            , "int",cxc, "int",cyc          ; int       cx|cy   0=returned image will have same width|height as original hImage
            ,"uint",lcDef                   ; uint      flags
            , 'Ptr')
          hCur_blank := DllCall("CreateCursor" ; create a monochrome cursor
            , "Ptr",0           ;opt HINSTANCE  hInst,
            , "int", 0,"int", 0 ;int        xHotSpot / yHotSpot
            , "int",32,"int",32 ;int        nWidth   / nHeight
            , 'Ptr',AndMask     ;const VOID *pvANDPlane array of bytes with bit values for the AND mask of the cursor, as in a device-dependent monochrome bitmap
            , 'Ptr',XorMask     ;const VOID *pvXORPlane array of bytes with bit values for the XOR mask of the cursor, as in a device-dependent monochrome bitmap
            , 'Ptr')
          hCursors.Push([hCur_saved, hCur_blank]) ; toShow=1 toHide=2
        }
        ; isInit    := true ; move to the end to allow hiding cursor on 1st toggle call
      }
      ; MouseGetPos(&🖰x, &🖰y) ; Get the current mouse position, and store its coordinates
      dbgOut := "changeTo=" changeTo
        . "`nsysCurMagniF=" sysCurMagniF
        . "`nmonDPIx|y`t= " dpi🖥️x " | " dpi🖥️y ;"`n" 🖰x " " 🖰y
        . "`ncurW|H_dpi`t= " width🖰Pointer " | " height🖰Pointer
        . "`ncX|Y`t= " cx " | " cy
    
      loop curSID.Length {
        hCur := DllCall("CopyImage", "Ptr", hCursors[A_Index][changeTo]
          ,"uint",C.imgCursor, "int",cxc,"int",cyc, "uint",lcDef)
        DllCall("SetSystemCursor" ; replace the contents of the system cursor specified by id with the contents of the cursor handled by hcur
          , "Ptr",hCur              ; cursor handle, destroyed via DestroyCursor, so can't be LoadCursor, use CopyCursor
          ,"uint",curSID[A_Index]   ; system cursor to replace
          )
        ; dbgOut .= "`nhCur=" hCur
        }
      is🖰PointerHidden := (changeTo = toHide) ? true : false
      dbgOut .= "`nis🖰PointerHidden=" is🖰PointerHidden
      dbgOut .= "`nOnOff=" OnOff
      if changeTo = toShow {
        restore🖰Pointers()
        sys🖰Btn(On)
      } else if changeTo = toHide {
        sys🖰Btn(Off)
      }
      dbgTT(dbgMin:=4, Text:=dbgOut, Time:=3,id:=3,X:=0,Y:=750)
      isInit    := true
    }

And the function that restores the pointer

    restore🖰Pointers() {
      static C := win32Constant.Misc ; various win32 API constants
      DllCall("SystemParametersInfo"
       ,'uint',C.curReload  ; uint      uiAction    system-wide parameter to be retrieved or set
       ,'uint',0            ; uint      uiParam     parameter whose usage and format depends on the system parameter being queried or set., see uiAction. Must be 0 if not otherwise indicated
       ,'uint',0            ;io PVOID   pvParam     parameter whose usage and format depends on the system parameter being queried or set, see uiAction. Must be NULL if not otherwise indicated
       ,'uint',0            ; uint      fWinIni     If a system parameter is being set, specifies whether the user profile is to be updated, and if so, whether the WM_SETTINGCHANGE message is to be broadcast to all top-level windows to notify them of the change
       )
    }

And the alternative approach

app🖰Pointer(OnOff := '') { ; create our own gui element, make the target app its owner, then show a pointer there so it's redirected from the app to our invisible element
  static guiBlankChild := Gui()
   , guiOwner := 0
   , displayCounter := 0 ; track thread pointer counter, pointer is shown only if >=0, no way to get current value

  is🖰vis := is🖰PointerVisible() ; check if pointer is visible otherwise ShowCursor can stack hiding it requiring multiple calls to unstack
  MouseGetPos(,,&winID,)

  if    OnOff = Off                         ; hide if explicit command to hide is given
    or (OnOff = Toggle and is🖰vis = 0)     ; or   if explicti command to toggle is given and it's not hidden yet
    or (OnOff = ''     and is🖰vis = 0) {   ; or no command and it hasn't been hidden yet
    guiBlankChild.Opt("+Owner" . winID) ; make the GUI owned by winID
    guiOwner := winID
    if is🖰vis {
      if displayCounter < -1 { ;;; likely an issue with being unable to hide the pointer
      } else {
        displayCounter := DllCall("ShowCursor", "int",0)
      }
    }
  } else {
    if not is🖰vis {
      if not winID = guiOwner {
        guiBlankChild.Opt("+Owner" . winID) ; make the GUI owned by winID
        guiOwner := winID
        displayCounter := DllCall("ShowCursor", "int",1)
        guiBlankChild.Opt("-Owner")
      } else {
        displayCounter := DllCall("ShowCursor", "int",1)
        guiBlankChild.Opt("-Owner")
      }
    }
  }
}

P.S. "Hide pointer while typing" is indeed deficient as it relies on apps polling its value and hiding the pointer themselves, which few apps do

P.P.S. Also found a simpler C++ GUI app MousePuff

Upvotes: 0

Related Questions