123 lines
7.3 KiB
PowerShell
123 lines
7.3 KiB
PowerShell
# Scoped interactive cursor probe. No theme, driver, display or Sunshine changes.
|
|
$ErrorActionPreference='Stop'
|
|
$folder='C:\ProgramData\VDI\cursor-poc-20261103'
|
|
Set-Content "$folder\pid.txt" $PID
|
|
try {
|
|
Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @'
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows.Forms;
|
|
public class VDICursorShape {
|
|
public string name; public int width,height,hotX,hotY,invertedPixels;
|
|
public bool monochrome; public uint[] argb;
|
|
}
|
|
public class VDICursorProbe : Form {
|
|
[StructLayout(LayoutKind.Sequential)] struct POINT { public int x,y; }
|
|
[StructLayout(LayoutKind.Sequential)] struct CI { public int size,flags; public IntPtr cursor; public POINT point; }
|
|
[StructLayout(LayoutKind.Sequential)] struct II { public bool icon; public int x,y; public IntPtr mask,color; }
|
|
[DllImport("user32.dll")] static extern bool GetCursorInfo(ref CI ci);
|
|
[DllImport("user32.dll")] static extern bool GetIconInfo(IntPtr h,out II info);
|
|
[DllImport("user32.dll")] static extern bool DrawIconEx(IntPtr dc,int x,int y,IntPtr icon,int w,int h,int step,IntPtr brush,int flags);
|
|
[DllImport("gdi32.dll")] static extern bool DeleteObject(IntPtr h);
|
|
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
|
public static readonly string[] Names={"arrow","ibeam","sizewe","wait"};
|
|
static readonly Cursor[] Shapes={Cursors.Default,Cursors.IBeam,Cursors.SizeWE,Cursors.WaitCursor};
|
|
static Bitmap Draw(IntPtr handle,int w,int h,Color background) {
|
|
Bitmap result=new Bitmap(w,h,System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
using(Graphics g=Graphics.FromImage(result)) {
|
|
g.Clear(background); IntPtr dc=g.GetHdc();
|
|
try { if(!DrawIconEx(dc,0,0,handle,w,h,0,IntPtr.Zero,3)) throw new Exception("DrawIconEx failed"); }
|
|
finally { g.ReleaseHdc(dc); }
|
|
}
|
|
return result;
|
|
}
|
|
public static VDICursorShape Export(string name,IntPtr handle) {
|
|
II info; if(!GetIconInfo(handle,out info)) throw new Exception("GetIconInfo failed");
|
|
try {
|
|
int w,h;
|
|
using(Bitmap mask=Bitmap.FromHbitmap(info.mask)) { w=mask.Width; h=mask.Height/(info.color==IntPtr.Zero?2:1); }
|
|
if(w<1 || h<1 || w>128 || h>128) throw new Exception("Unsupported cursor dimensions");
|
|
VDICursorShape s=new VDICursorShape {name=name,width=w,height=h,hotX=info.x,hotY=info.y,monochrome=info.color==IntPtr.Zero,argb=new uint[w*h]};
|
|
using(Bitmap black=Draw(handle,w,h,Color.Black))
|
|
using(Bitmap white=Draw(handle,w,h,Color.White)) {
|
|
for(int y=0;y<h;y++) for(int x=0;x<w;x++) {
|
|
Color b=black.GetPixel(x,y), z=white.GetPixel(x,y);
|
|
int dr=z.R-b.R,dg=z.G-b.G,db=z.B-b.B;
|
|
// XOR/inverted cursors need destination-dependent composition.
|
|
// Preserve the unsupported count; never silently call them alpha cursors.
|
|
if(dr<0 || dg<0 || db<0 || Math.Abs(dr-dg)>2 || Math.Abs(dr-db)>2) { ++s.invertedPixels; continue; }
|
|
int a=255-dr;
|
|
if(a==0) continue;
|
|
uint r=(uint)Math.Min(255,(b.R*255+a/2)/a),g=(uint)Math.Min(255,(b.G*255+a/2)/a),bl=(uint)Math.Min(255,(b.B*255+a/2)/a);
|
|
s.argb[y*w+x]=((uint)a<<24)|(r<<16)|(g<<8)|bl;
|
|
}
|
|
}
|
|
return s;
|
|
} finally { if(info.mask!=IntPtr.Zero) DeleteObject(info.mask); if(info.color!=IntPtr.Zero) DeleteObject(info.color); }
|
|
}
|
|
public static VDICursorShape[] ExportAll() {
|
|
List<VDICursorShape> result=new List<VDICursorShape>();
|
|
for(int i=0;i<Names.Length;i++) result.Add(Export(Names[i],Shapes[i].Handle));
|
|
return result.ToArray();
|
|
}
|
|
readonly Timer timer=new Timer(); readonly string folder; readonly DateTime started=DateTime.UtcNow;
|
|
bool hidden; string shape="arrow",lastObserved=""; int clicks;
|
|
public VDICursorProbe(string path) {
|
|
folder=path; Text="VDI Independent Cursor Probe"; FormBorderStyle=FormBorderStyle.None;
|
|
Bounds=Screen.PrimaryScreen.Bounds; TopMost=true; DoubleBuffered=true;
|
|
File.WriteAllText(Path.Combine(folder,"clicks.csv"),"utc,x,y,shape\n");
|
|
File.WriteAllText(Path.Combine(folder,"states.csv"),"utc,shape,flags,x,y\n");
|
|
timer.Interval=100;
|
|
timer.Tick+=delegate {
|
|
if((DateTime.UtcNow-started).TotalMinutes>8) { Close(); return; }
|
|
CI ci=new CI(); ci.size=Marshal.SizeOf(typeof(CI));
|
|
if(!GetCursorInfo(ref ci)) return;
|
|
string observed="unknown";
|
|
for(int i=0;i<Shapes.Length;i++) if(ci.cursor==Shapes[i].Handle) observed=Names[i];
|
|
string observedKey=observed+","+ci.flags;
|
|
if(observedKey!=lastObserved) {
|
|
File.AppendAllText(Path.Combine(folder,"states.csv"),DateTime.UtcNow.ToString("o")+","+observedKey+","+ci.point.x+","+ci.point.y+"\n");
|
|
lastObserved=observedKey;
|
|
}
|
|
string json="{\"version\":1,\"shape\":\""+observed+"\",\"visible\":"+(((ci.flags&1)!=0)?"true":"false")+",\"x\":"+ci.point.x+",\"y\":"+ci.point.y+",\"utc\":\""+DateTime.UtcNow.ToString("o")+"\"}";
|
|
string target=Path.Combine(folder,"state.json"),temp=target+".new";
|
|
File.WriteAllText(temp,json);
|
|
if(File.Exists(target)) File.Replace(temp,target,null); else File.Move(temp,target);
|
|
Invalidate(new Rectangle(0,0,Width,140));
|
|
};
|
|
timer.Start();
|
|
}
|
|
protected override void OnMouseMove(MouseEventArgs e) {
|
|
base.OnMouseMove(e);
|
|
bool nextHidden=e.Y>Height*3/4;
|
|
if(nextHidden!=hidden) { if(nextHidden) Cursor.Hide(); else Cursor.Show(); hidden=nextHidden; }
|
|
int index=Math.Min(3,e.X*4/Math.Max(1,Width)); shape=Names[index]; Cursor=Shapes[index];
|
|
}
|
|
protected override void OnMouseDown(MouseEventArgs e) {
|
|
base.OnMouseDown(e); ++clicks;
|
|
File.AppendAllText(Path.Combine(folder,"clicks.csv"),DateTime.UtcNow.ToString("o")+","+e.X+","+e.Y+","+shape+"\n"); Invalidate();
|
|
}
|
|
protected override void OnPaint(PaintEventArgs e) {
|
|
e.Graphics.Clear(Color.FromArgb(24,32,44));
|
|
using(Font f=new Font("Consolas",23)) for(int i=0;i<4;i++) {
|
|
e.Graphics.DrawRectangle(Pens.SlateGray,i*Width/4,150,Width/4-2,Height*3/4-150);
|
|
e.Graphics.DrawString(Names[i],f,Brushes.White,i*Width/4+40,200);
|
|
}
|
|
using(Font f=new Font("Consolas",24)) {
|
|
e.Graphics.DrawString("Independent cursor probe | clicks: "+clicks+" | "+DateTime.UtcNow.ToString("HH:mm:ss.fff"),f,Brushes.White,40,40);
|
|
e.Graphics.DrawString("BOTTOM AREA: cursor hidden",f,Brushes.Gold,40,Height*3/4+60);
|
|
}
|
|
}
|
|
protected override void OnFormClosed(FormClosedEventArgs e) {
|
|
timer.Stop(); timer.Dispose(); if(hidden) { Cursor.Show(); hidden=false; } base.OnFormClosed(e);
|
|
}
|
|
}
|
|
'@
|
|
[VDICursorProbe]::SetProcessDPIAware() | Out-Null
|
|
@{version=1; shapes=[VDICursorProbe]::ExportAll()} | ConvertTo-Json -Depth 6 -Compress | Set-Content -Encoding utf8 "$folder\shapes.json"
|
|
[System.Windows.Forms.Application]::Run((New-Object VDICursorProbe($folder)))
|
|
} catch { $_ | Out-String | Set-Content "$folder\error.txt"; throw }
|