Files
DeskCore/DesktopIcons.cs
2026-10-04 14:48:53 +00:00

235 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
using System.Text;
namespace DeskCore;
/// <summary>Ein Icon auf dem echten Windows-Desktop (Bildschirm-Pixel).</summary>
public record DesktopIcon(string Path, int X, int Y, int IconSize, int CellWidth);
/// <summary>
/// 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.
/// </summary>
public static class DesktopIcons
{
/// <summary>Alle Desktop-Icons mit Position – oder leer, wenn der Desktop gerade nicht erreichbar ist.</summary>
public static List<DesktopIcon> Read()
{
var result = new List<DesktopIcon>();
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;
}
/// <summary>
/// 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).
/// </summary>
public static (int L, int T, int Size) IconRect(DesktopIcon i) => (i.X, i.Y + IconTopOffset, i.IconSize);
/// <summary>Feinkorrektur nach unten (Windows zeichnet das Bild mit kleinem Rand).</summary>
public static int IconTopOffset { get; set; } = 3;
/// <summary>Gesamte Fläche eines Eintrags (Bild + Beschriftung), z. B. für Klick-Tests.</summary>
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);
}
/// <summary>Zuletzt gelesene Icons (für schnelle Prüfungen aus dem Maus-Hook, ohne COM-Aufruf).</summary>
public static volatile IReadOnlyList<DesktopIcon> Cached = [];
/// <summary>Zwischenspeicher auffrischen (UI-Thread). Gibt true zurück, wenn sich etwas geändert hat.</summary>
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;
}
/// <param name="margin">Zusätzlicher Rand in Pixeln, damit Klicks knapp neben Bild oder Beschriftung auch zählen.</param>
public static bool HitsIcon(int x, int y, int margin = 0) => Cached.Any(i =>
{
var (l, t, w, h) = CellRect(i);
return x >= l - margin && x < l + w + margin && y >= t - margin && y < t + h + margin;
});
// ---------- 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);
}