A complete rewrite of the Python pygame cursor overlay in C# to eliminate graphics complications. This is a transparent fullscreen overlay that renders the osu! cursor and trail on top of all windows.
- Transparent Overlay - Pure black background is fully transparent; clicks pass through
- Real osu! Skins - Load cursor.png and cursortrail.png from any osu! skin folder
- 144 FPS - Dedicated render thread + Stopwatch spin-wait for accurate timing
- System Tray - Pause/resume overlay, open config, or exit from context menu
- Global Hotkey - Ctrl+Shift+Q to exit (configurable in config.ini)
- System Cursor Hidden - All 12 Windows cursor types replaced; restored on exit
- Alpha-Blended Trail - Fades from oldest (transparent) to newest (opaque); scales smallest to largest
- No External Dependencies - Uses only .NET 8 WinForms and System.Drawing
- Direct Win32 P/Invoke - SetWindowPos, SetLayeredWindowAttributes, RegisterHotKey, CreateCursor, GetCursorPos
- Double-Buffered Rendering - Background thread renders to Bitmap, then blits to HWND via
Graphics.FromHwnd - Zero Per-Frame GC - Trail bitmaps and alpha ImageAttributes pre-allocated
cd OsuCursorOverlay_CSharp
dotnet build -c ReleaseOutput: bin/Release/net8.0-windows/OsuCursorOverlay.exe
OsuCursorOverlay.exe- A skin selector dialog appears - choose any osu! skin with a cursor.png file
- The overlay starts in the background; tray icon appears in system tray
- Right-click tray icon:
- Pausar - Hide overlay, restore system cursor
- Reanudar - Show overlay, hide system cursor
- Abrir Config - Open config.ini in default text editor
- Salir - Exit and restore system cursor
config.ini in the application directory:
[cursor]
scale = 1.0 # Size multiplier for cursor PNG
trail_length = 15 # Max trail points
max_trail_alpha = 150 # Alpha of newest trail point (0-255)
min_trail_scale = 0.3 # Scale of oldest trail point (0.0-1.0)
trail_spacing = 3.0 # Min pixel distance between trail points
[system]
target_fps = 144 # Render frame rate
hide_system_cursor = true # Hide Windows cursor when overlay is active
exit_hotkey = ctrl+shift+q # Global hotkey to exit (ctrl, shift, alt, win + letter)Created on first run with defaults if missing.
OsuCursorOverlay_CSharp/
├── OsuCursorOverlay.csproj Project file (.NET 8 Windows)
├── Program.cs Entry point: STAThread, skin selector, overlay startup
├── Config.cs INI parser + AppSettings class
├── SkinSelector.cs WinForms dialog + SkinDiscovery (replaces Python msvcrt UI)
├── NativeMethods.cs P/Invoke declarations for Win32 APIs
├── CursorManager.cs Hide/restore all 12 system cursor types
├── SkinAssets.cs Load PNG skins with black color-key transparency
├── TrailRenderer.cs Trail queue + GDI+ rendering with alpha blending
├── OverlayForm.cs Main overlay form: transparency, tray, hotkey, render thread
├── README.md This file
└── assets/
└── tray_icon.ico (generated programmatically if missing)
Calls Application.SetHighDpiMode(HighDpiMode.PerMonitorV2) before EnableVisualStyles() to ensure GetCursorPos physical pixel coordinates align with window bounds on 125%+ displays.
- WinForms sets
TransparencyKey = Color.Black(auto-appliesLWA_COLORKEY) - P/Invoke additionally ORs
WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATEto prevent focus stealing and enable click-through - Each frame,
Graphics.Clear(Color.Black)fills the back-buffer; pure black becomes transparent
- Thread: Background thread calls
Graphics.FromHwnd(Handle)directly (cross-thread safe for overlays) - Timing:
timeBeginPeriod(1)+Stopwatchspin-wait (notThread.Sleepalone, which has 15ms resolution) - Caching: Trail scaled bitmaps cached per slot; 256 alpha
ImageAttributespre-allocated to avoid per-frame GC
Uses ColorMatrix with ImageAttributes to set per-bitmap alpha without recreating the bitmap:
var cm = new ColorMatrix { Matrix33 = alpha / 255f };
var ia = new ImageAttributes();
ia.SetColorMatrix(cm);
g.DrawImage(scaled, destRect, 0, 0, w, h, GraphicsUnit.Pixel, ia);Checks three paths in order:
%LOCALAPPDATA%\osu!\SkinsC:\Program Files\osu!\SkinsC:\Program Files (x86)\osu!\Skins
Valid skin = subfolder containing cursor.png.
| Feature | Python | C# |
|---|---|---|
| Rendering | pygame (SDL) | GDI+ (Win32) |
| Transparency | pygame.NOFRAME + SetLayeredWindowAttributes |
WinForms TransparencyKey + manual P/Invoke |
| 144 FPS | clock.tick(144) (can stutter) |
timeBeginPeriod(1) + spin-wait (accurate) |
| Skin Selector | Console arrow keys (msvcrt) | WinForms dialog |
| Hotkey | keyboard package |
Win32 RegisterHotKey |
| System Cursor | SetSystemCursor via ctypes |
P/Invoke CreateCursor / SetSystemCursor |
| Trail Rendering | pygame.Surface.set_alpha() + per-frame scale |
ImageAttributes + cached bitmaps |
| Dependencies | 4 (pygame, pystray, keyboard, Pillow) | 0 (all .NET 8) |
Overlay not transparent?
- Verify screen background is pure black (RGB 0,0,0)
- Check
config.inivalues are reasonable
Cursor not hidden?
- Ensure
hide_system_cursor = truein config.ini - Overlay may need to be running with sufficient permissions
Trail not rendering?
- Verify osu! skin folder contains
cursortrail.png(optional) - Falls back to procedural white circle if missing
Flicker at high FPS?
- Check system CPU usage; render thread may be competing with other processes
- Try lowering
target_fpsin config.ini temporarily to debug
Requirements:
- .NET 8 SDK (or later)
- Windows 10+ (WinForms is Windows-only)
dotnet build -c Release
dotnet publish -c Release -o distBinary is self-contained; no runtime installation needed.
Status: Production-ready. Built 2026-04-17.