using System.Runtime.InteropServices; using System.Text; namespace DeskCore; /// Ein Icon auf dem echten Windows-Desktop (Bildschirm-Pixel). public record DesktopIcon(string Path, int X, int Y, int IconSize, int CellWidth); /// /// Liest Positionen und Größe der echten Desktop-Icons über die offizielle Shell-Schnittstelle (IFolderView) /// aus – ohne etwas in den Explorer einzuschleusen. Nur auf dem UI-Thread (STA) aufrufen. /// public static class DesktopIcons { /// Alle Desktop-Icons mit Position – oder leer, wenn der Desktop gerade nicht erreichbar ist. public static List Read() { var result = new List(); try { var view = DesktopFolderView(out var listView); if (view is null || listView == IntPtr.Zero) return result; var view2 = view as IFolderView2; int iconSize = 48; if (view2 is not null && view2.GetViewModeAndIconSize(out _, out var size) == 0) iconSize = size; view.GetSpacing(out var spacing); var origin = new POINT(); ClientToScreen(listView, ref origin); view.ItemCount(SVGIO_ALLVIEW, out int count); for (int i = 0; i < count; i++) { if (view.Item(i, out var pidl) != 0 || pidl == IntPtr.Zero) continue; try { if (view.GetItemPosition(pidl, out var pos) != 0) continue; string path = ""; if (view2 is not null && view2.GetItem(i, typeof(IShellItem).GUID, out var obj) == 0 && obj is IShellItem item) { if (item.GetDisplayName(SIGDN_FILESYSPATH, out var p) == 0) { path = Marshal.PtrToStringUni(p) ?? ""; Marshal.FreeCoTaskMem(p); } Marshal.ReleaseComObject(item); } result.Add(new DesktopIcon(path, origin.X + pos.X, origin.Y + pos.Y, iconSize, spacing.X)); } finally { Marshal.FreeCoTaskMem(pidl); } } } catch { } return result; } /// /// Rechteck des Icon-Bildes in Bildschirm-Pixeln. Die Position aus IFolderView ist bereits die linke obere /// Ecke des Bildes (gemessen: Eintrag beginnt (Abstand − Bildgröße) / 2 weiter links, 3 px weiter oben). /// public static (int L, int T, int Size) IconRect(DesktopIcon i) => (i.X, i.Y + IconTopOffset, i.IconSize); /// Feinkorrektur nach unten (Windows zeichnet das Bild mit kleinem Rand). public static int IconTopOffset { get; set; } = 3; /// Gesamte Fläche eines Eintrags (Bild + Beschriftung), z. B. für Klick-Tests. public static (int L, int T, int W, int H) CellRect(DesktopIcon i) { int pad = Math.Max(0, (i.CellWidth - i.IconSize) / 2); return (i.X - pad, i.Y, i.CellWidth, i.IconSize + 44); } /// Zuletzt gelesene Icons (für schnelle Prüfungen aus dem Maus-Hook, ohne COM-Aufruf). public static volatile IReadOnlyList Cached = []; /// Zwischenspeicher auffrischen (UI-Thread). Gibt true zurück, wenn sich etwas geändert hat. public static bool RefreshCache() { var fresh = Read(); bool changed = fresh.Count != Cached.Count || fresh.Where((x, n) => x != Cached[n]).Any(); Cached = fresh; return changed; } public static bool HitsIcon(int x, int y) => Cached.Any(i => { var (l, t, w, h) = CellRect(i); return x >= l && x < l + w && y >= t && y < t + h; }); // ---------- Desktop-Ansicht finden ---------- private static IFolderView? DesktopFolderView(out IntPtr listView) { listView = IntPtr.Zero; var shellWindows = (IShellWindows)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ShellWindows)!)!; try { object loc = CSIDL_DESKTOP, empty = new object(); var disp = shellWindows.FindWindowSW(ref loc, ref empty, SWC_DESKTOP, out _, SWFO_NEEDDISPATCH); if (disp is not IServiceProvider sp) return null; var sidBrowser = SID_STopLevelBrowser; var iidBrowser = typeof(IShellBrowser).GUID; if (sp.QueryService(ref sidBrowser, ref iidBrowser, out var browserObj) != 0) return null; var browser = (IShellBrowser)browserObj; if (browser.QueryActiveShellView(out var shellView) != 0 || shellView is null) return null; shellView.GetWindow(out var defView); listView = FindWindowEx(defView, IntPtr.Zero, "SysListView32", null); return shellView as IFolderView; } finally { Marshal.ReleaseComObject(shellWindows); } } // ---------- COM-Schnittstellen (nur die benötigten Methoden in vtable-Reihenfolge) ---------- private const int CSIDL_DESKTOP = 0, SWC_DESKTOP = 8, SWFO_NEEDDISPATCH = 1, SVGIO_ALLVIEW = 2; private const uint SIGDN_FILESYSPATH = 0x80058000; private static readonly Guid CLSID_ShellWindows = new("9BA05972-F6A8-11CF-A442-00A0C90A8F39"); private static readonly Guid SID_STopLevelBrowser = new("4C96BE40-915C-11CF-99D3-00AA004AE837"); [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; } [ComImport, Guid("85CB6900-4D95-11CF-960C-0080C7F4EE85"), InterfaceType(ComInterfaceType.InterfaceIsDual)] private interface IShellWindows { int Count { get; } [return: MarshalAs(UnmanagedType.IDispatch)] object Item(object index); [return: MarshalAs(UnmanagedType.IUnknown)] object _NewEnum(); void Register([MarshalAs(UnmanagedType.IDispatch)] object pid, int hwnd, int swClass, out int plCookie); void RegisterPending(int lThreadId, ref object pvarloc, ref object pvarlocRoot, int swClass, out int plCookie); void Revoke(int lCookie); void OnNavigate(int lCookie, ref object pvarLoc); void OnActivated(int lCookie, short fActive); [return: MarshalAs(UnmanagedType.IDispatch)] object FindWindowSW(ref object pvarLoc, ref object pvarLocRoot, int swClass, out int pHWND, int swfwOptions); } [ComImport, Guid("6D5140C1-7436-11CE-8034-00AA006009FA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IServiceProvider { [PreserveSig] int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out object ppvObject); } [ComImport, Guid("000214E2-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IShellBrowser { void GetWindow(out IntPtr phwnd); void ContextSensitiveHelp(bool fEnterMode); void InsertMenusSB(IntPtr hmenuShared, IntPtr lpMenuWidths); void SetMenuSB(IntPtr hmenuShared, IntPtr holemenuRes, IntPtr hwndActiveObject); void RemoveMenusSB(IntPtr hmenuShared); void SetStatusTextSB(IntPtr pszStatusText); void EnableModelessSB(bool fEnable); void TranslateAcceleratorSB(IntPtr pmsg, ushort wID); void BrowseObject(IntPtr pidl, uint wFlags); void GetViewStateStream(uint grfMode, out IntPtr ppStrm); void GetControlWindow(uint id, out IntPtr phwnd); void SendControlMsg(uint id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret); [PreserveSig] int QueryActiveShellView(out IShellView ppshv); } [ComImport, Guid("000214E3-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IShellView { void GetWindow(out IntPtr phwnd); } [ComImport, Guid("CDE725B0-CCC9-4519-917E-325D72FAB4CE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IFolderView { [PreserveSig] int GetCurrentViewMode(out uint pViewMode); [PreserveSig] int SetCurrentViewMode(uint ViewMode); [PreserveSig] int GetFolder(ref Guid riid, out IntPtr ppv); [PreserveSig] int Item(int iItemIndex, out IntPtr ppidl); [PreserveSig] int ItemCount(uint uFlags, out int pcItems); [PreserveSig] int Items(uint uFlags, ref Guid riid, out IntPtr ppv); [PreserveSig] int GetSelectionMarkedItem(out int piItem); [PreserveSig] int GetFocusedItem(out int piItem); [PreserveSig] int GetItemPosition(IntPtr pidl, out POINT ppt); [PreserveSig] int GetSpacing(out POINT ppt); } [ComImport, Guid("1AF3A467-214F-4298-908E-06B03E0B39F9"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IFolderView2 { // IFolderView [PreserveSig] int GetCurrentViewMode(out uint pViewMode); [PreserveSig] int SetCurrentViewMode(uint ViewMode); [PreserveSig] int GetFolder(ref Guid riid, out IntPtr ppv); [PreserveSig] int Item(int iItemIndex, out IntPtr ppidl); [PreserveSig] int ItemCount(uint uFlags, out int pcItems); [PreserveSig] int Items(uint uFlags, ref Guid riid, out IntPtr ppv); [PreserveSig] int GetSelectionMarkedItem(out int piItem); [PreserveSig] int GetFocusedItem(out int piItem); [PreserveSig] int GetItemPosition(IntPtr pidl, out POINT ppt); [PreserveSig] int GetSpacing(out POINT ppt); [PreserveSig] int GetDefaultSpacing(out POINT ppt); [PreserveSig] int GetAutoArrange(); [PreserveSig] int SelectItem(int iItem, uint dwFlags); [PreserveSig] int SelectAndPositionItems(uint cidl, IntPtr apidl, IntPtr apt, uint dwFlags); // IFolderView2 [PreserveSig] int SetGroupBy(IntPtr key, bool fAscending); [PreserveSig] int GetGroupBy(IntPtr pkey, out bool pfAscending); [PreserveSig] int SetViewProperty(IntPtr pidl, IntPtr propkey, IntPtr propvar); [PreserveSig] int GetViewProperty(IntPtr pidl, IntPtr propkey, IntPtr ppropvar); [PreserveSig] int SetTileViewProperties(IntPtr pidl, IntPtr pszPropList); [PreserveSig] int SetExtendedTileViewProperties(IntPtr pidl, IntPtr pszPropList); [PreserveSig] int SetText(int iType, IntPtr pwszText); [PreserveSig] int SetCurrentFolderFlags(uint dwMask, uint dwFlags); [PreserveSig] int GetCurrentFolderFlags(out uint pdwFlags); [PreserveSig] int GetSortColumnCount(out int pcColumns); [PreserveSig] int SetSortColumns(IntPtr rgSortColumns, int cColumns); [PreserveSig] int GetSortColumns(IntPtr rgSortColumns, int cColumns); [PreserveSig] int GetItem(int iItem, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, [MarshalAs(UnmanagedType.Interface)] out object ppv); [PreserveSig] int GetVisibleItem(int iStart, bool fPrevious, out int piItem); [PreserveSig] int GetSelectedItem(int iStart, out int piItem); [PreserveSig] int GetSelection(bool fNoneImpliesFolder, out IntPtr ppsia); [PreserveSig] int GetSelectionState(IntPtr pidl, out uint pdwFlags); [PreserveSig] int InvokeVerbOnSelection(IntPtr pszVerb); [PreserveSig] int SetViewModeAndIconSize(int uViewMode, int iImageSize); [PreserveSig] int GetViewModeAndIconSize(out int puViewMode, out int piImageSize); } [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IShellItem { [PreserveSig] int BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv); [PreserveSig] int GetParent(out IShellItem ppsi); [PreserveSig] int GetDisplayName(uint sigdnName, out IntPtr ppszName); } [DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string? cls, string? title); }