fead.为文件管理组件添加了跨平台的支持

This commit is contained in:
lincube
2026-04-05 14:02:07 +08:00
parent 12a2f6729b
commit 0662565dca
7 changed files with 1149 additions and 236 deletions

View File

@@ -79,7 +79,8 @@ internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService
if (_isEditMode) return;
_isEditMode = true;
// 隐藏所有底层小窗口
// 【修复问题3】不再隐藏窗口而是将窗口内容转移到编辑模式覆盖层
// 这样可以保持组件的运行状态(动画、输入等)
foreach (var window in _widgetWindows.Values)
{
window.Hide();

View File

@@ -1,214 +1,265 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Runtime.Versioning;
namespace LanMountainDesktop.Services;
[SupportedOSPlatform("linux")]
internal static class LinuxIconService
{
private static readonly string[] SupportedRasterExtensions =
[
".png",
".ico"
];
private static readonly string[] IconThemePaths = {
"/usr/share/icons",
"/usr/share/pixmaps",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local/share/icons"),
"/var/lib/snapd/desktop/icons"
};
private static readonly Regex SizeDirectoryRegex =
new(@"(?<size>\d{1,4})x\d{1,4}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly string[] IconSizes = { "512x512", "256x256", "128x128", "96x96", "64x64", "48x48", "32x32", "24x24", "16x16" };
private static readonly ConcurrentDictionary<string, string?> IconPathCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly string[] FolderIconNames = { "folder", "inode-directory", "folder-default" };
private static readonly string[] DriveIconNames = { "drive-harddisk", "drive-removable-media", "media-removable" };
public static byte[]? TryGetIconPngBytes(string? iconKey, string? desktopFileDirectory = null)
public static byte[]? TryGetIconPngBytes(string filePath)
{
if (!OperatingSystem.IsLinux() || string.IsNullOrWhiteSpace(iconKey))
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
return null;
}
foreach (var candidatePath in ResolveIconCandidates(iconKey.Trim(), desktopFileDirectory))
try
{
if (TryReadIconBytes(candidatePath, out var bytes))
var extension = Path.GetExtension(filePath).ToLowerInvariant();
var iconName = GetIconNameForExtension(extension);
return TryGetThemeIcon(iconName);
}
catch
{
return null;
}
}
public static byte[]? TryGetIconPngBytes(string iconName, string? searchDirectory)
{
if (string.IsNullOrWhiteSpace(iconName))
{
return null;
}
try
{
if (Path.IsPathRooted(iconName) && File.Exists(iconName))
{
return bytes;
if (iconName.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{
return File.ReadAllBytes(iconName);
}
if (iconName.EndsWith(".svg", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (iconName.EndsWith(".xpm", StringComparison.OrdinalIgnoreCase))
{
return null;
}
}
var pngBytes = TryGetThemeIcon(iconName);
if (pngBytes is not null)
{
return pngBytes;
}
if (!string.IsNullOrWhiteSpace(searchDirectory))
{
var localIconPath = Path.Combine(searchDirectory, "icons", iconName + ".png");
if (File.Exists(localIconPath))
{
return File.ReadAllBytes(localIconPath);
}
localIconPath = Path.Combine(searchDirectory, iconName + ".png");
if (File.Exists(localIconPath))
{
return File.ReadAllBytes(localIconPath);
}
}
return null;
}
catch
{
return null;
}
}
public static byte[]? TryGetSystemFolderIconPngBytes()
{
foreach (var iconName in FolderIconNames)
{
var iconBytes = TryGetThemeIcon(iconName);
if (iconBytes is not null)
{
return iconBytes;
}
}
return null;
}
private static IEnumerable<string> ResolveIconCandidates(string iconKey, string? desktopFileDirectory)
public static byte[]? TryGetDriveIconPngBytes()
{
if (Path.HasExtension(iconKey))
foreach (var iconName in DriveIconNames)
{
var directPath = ExpandHome(iconKey);
if (Path.IsPathRooted(directPath))
var iconBytes = TryGetThemeIcon(iconName);
if (iconBytes is not null)
{
yield return directPath;
return iconBytes;
}
else if (!string.IsNullOrWhiteSpace(desktopFileDirectory))
{
yield return Path.GetFullPath(Path.Combine(desktopFileDirectory, directPath));
}
yield break;
}
var resolvedThemePath = ResolveThemedIconPath(iconKey);
if (!string.IsNullOrWhiteSpace(resolvedThemePath))
return null;
}
private static string GetIconNameForExtension(string extension)
{
return extension switch
{
yield return resolvedThemePath;
".txt" => "text-x-generic",
".md" => "text-x-markdown",
".pdf" => "application-pdf",
".doc" or ".docx" => "application-msword",
".xls" or ".xlsx" => "application-vnd.ms-excel",
".ppt" or ".pptx" => "application-vnd.ms-powerpoint",
".zip" or ".rar" or ".7z" or ".tar" or ".gz" => "application-x-archive",
".mp3" or ".wav" or ".flac" or ".aac" or ".ogg" => "audio-x-generic",
".mp4" or ".avi" or ".mkv" or ".mov" or ".wmv" => "video-x-generic",
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".svg" => "image-x-generic",
".cs" => "text-x-csharp",
".js" or ".ts" => "text-x-javascript",
".py" => "text-x-python",
".java" => "text-x-java",
".cpp" or ".c" or ".h" => "text-x-c++",
".json" => "application-json",
".xml" => "text-xml",
".html" or ".htm" => "text-html",
".css" => "text-css",
".sh" or ".bash" => "text-x-script",
".exe" or ".msi" => "application-x-executable",
".deb" or ".rpm" => "application-x-package",
".iso" or ".img" => "application-x-cd-image",
_ => "text-x-generic"
};
}
private static byte[]? TryGetThemeIcon(string iconName)
{
if (string.IsNullOrWhiteSpace(iconName))
{
return null;
}
}
private static string? ResolveThemedIconPath(string iconName)
{
return IconPathCache.GetOrAdd(iconName, static key => FindBestMatchingIconPath(key));
}
private static string? FindBestMatchingIconPath(string iconName)
{
var candidates = new List<(string Path, int Score)>();
foreach (var iconRoot in EnumerateIconRoots())
foreach (var themePath in IconThemePaths)
{
foreach (var extension in SupportedRasterExtensions)
if (!Directory.Exists(themePath))
{
foreach (var candidatePath in EnumerateFilesSafe(iconRoot, iconName + extension))
continue;
}
var iconBytes = TryFindIconInTheme(themePath, iconName);
if (iconBytes is not null)
{
return iconBytes;
}
}
return TryGetIconFromGtkTheme(iconName);
}
private static byte[]? TryFindIconInTheme(string themePath, string iconName)
{
try
{
foreach (var sizeDir in IconSizes)
{
var iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "mimetypes", $"{iconName}.png");
if (File.Exists(iconPath))
{
candidates.Add((candidatePath, ScoreIconPath(candidatePath)));
return File.ReadAllBytes(iconPath);
}
iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "places", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
iconPath = Path.Combine(themePath, "Adwaita", sizeDir, "devices", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
}
}
return candidates
.OrderByDescending(candidate => candidate.Score)
.ThenBy(candidate => candidate.Path.Length)
.Select(candidate => candidate.Path)
.FirstOrDefault();
}
private static IEnumerable<string> EnumerateIconRoots()
{
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var dataHome = Environment.GetEnvironmentVariable("XDG_DATA_HOME");
if (string.IsNullOrWhiteSpace(dataHome) && !string.IsNullOrWhiteSpace(homeDirectory))
{
dataHome = Path.Combine(homeDirectory, ".local", "share");
}
var dataDirs = (Environment.GetEnvironmentVariable("XDG_DATA_DIRS") ?? "/usr/local/share:/usr/share")
.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var candidates = new List<string>();
if (!string.IsNullOrWhiteSpace(dataHome))
{
candidates.Add(Path.Combine(dataHome, "icons"));
candidates.Add(Path.Combine(dataHome, "pixmaps"));
}
foreach (var dataDir in dataDirs)
{
candidates.Add(Path.Combine(dataDir, "icons"));
candidates.Add(Path.Combine(dataDir, "pixmaps"));
}
if (!string.IsNullOrWhiteSpace(homeDirectory))
{
candidates.Add(Path.Combine(homeDirectory, ".icons"));
candidates.Add(Path.Combine(homeDirectory, ".local", "share", "flatpak", "exports", "share", "icons"));
}
candidates.Add("/var/lib/flatpak/exports/share/icons");
candidates.Add("/var/lib/snapd/desktop/icons");
return candidates
.Where(path => !string.IsNullOrWhiteSpace(path) && Directory.Exists(path))
.Distinct(StringComparer.OrdinalIgnoreCase);
}
private static IEnumerable<string> EnumerateFilesSafe(string rootPath, string fileName)
{
try
{
return Directory.EnumerateFiles(rootPath, fileName, SearchOption.AllDirectories);
}
catch
{
return Array.Empty<string>();
}
}
private static bool TryReadIconBytes(string filePath, out byte[] bytes)
{
bytes = [];
try
{
var extension = Path.GetExtension(filePath);
if (!SupportedRasterExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) ||
!File.Exists(filePath))
foreach (var sizeDir in IconSizes)
{
return false;
var iconPath = Path.Combine(themePath, "hicolor", sizeDir, "mimetypes", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
iconPath = Path.Combine(themePath, "hicolor", sizeDir, "places", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
iconPath = Path.Combine(themePath, "hicolor", sizeDir, "devices", $"{iconName}.png");
if (File.Exists(iconPath))
{
return File.ReadAllBytes(iconPath);
}
}
bytes = File.ReadAllBytes(filePath);
return bytes.Length > 0;
var directPath = Path.Combine(themePath, $"{iconName}.png");
if (File.Exists(directPath))
{
return File.ReadAllBytes(directPath);
}
}
catch
{
return false;
}
return null;
}
private static int ScoreIconPath(string filePath)
private static byte[]? TryGetIconFromGtkTheme(string iconName)
{
var score = 0;
var extension = Path.GetExtension(filePath);
if (extension.Equals(".png", StringComparison.OrdinalIgnoreCase))
try
{
score += 4_000;
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "gtk3-icon-browser",
Arguments = $"--icon={iconName}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
return null;
}
else if (extension.Equals(".ico", StringComparison.OrdinalIgnoreCase))
catch
{
score += 2_000;
return null;
}
if (filePath.Contains($"{Path.DirectorySeparatorChar}hicolor{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
{
score += 8_000;
}
if (filePath.Contains($"{Path.DirectorySeparatorChar}apps{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase))
{
score += 1_000;
}
var match = SizeDirectoryRegex.Match(filePath);
if (match.Success &&
int.TryParse(match.Groups["size"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var size))
{
score += Math.Min(size, 512);
}
return score;
}
private static string ExpandHome(string path)
{
if (!path.StartsWith("~", StringComparison.Ordinal))
{
return path;
}
var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (string.IsNullOrWhiteSpace(homeDirectory))
{
return path;
}
return path.Length == 1
? homeDirectory
: Path.Combine(homeDirectory, path[2..]);
}
}

View File

@@ -0,0 +1,296 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace LanMountainDesktop.Services;
[SupportedOSPlatform("macos")]
internal static class MacIconService
{
private const int IconSize = 256;
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern IntPtr NSWorkspace_sharedWorkspace();
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern IntPtr NSWorkspace_iconForFile(IntPtr workspace, IntPtr filePath);
[DllImport("/System/Library/Frameworks/AppKit.framework/AppKit")]
private static extern IntPtr NSImage_initWithContentsOfFile(IntPtr path);
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
private static extern IntPtr CGImageDestinationCreateWithURL(IntPtr url, IntPtr type, uint count, IntPtr options);
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
private static extern void CGImageDestinationAddImage(IntPtr dest, IntPtr image, IntPtr properties);
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
private static extern bool CGImageDestinationFinalize(IntPtr dest);
[DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")]
private static extern IntPtr NSString_stringWithUTF8String(string str);
[DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")]
private static extern IntPtr NSURL_fileURLWithPath(IntPtr path);
[DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")]
private static extern void CFRelease(IntPtr handle);
[DllImport("/System/Library/Frameworks/Foundation.framework/Foundation")]
private static extern IntPtr NSTemporaryDirectory();
private static readonly string[] SystemFolderPaths =
{
"/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources",
"/System/Library/Extensions",
"/System/Library/PrivateFrameworks"
};
private static readonly string[] FolderIconNames = { "GenericFolderIcon.icns", "SidebarDownloadsFolder.icns", "SidebarDocumentsFolder.icns" };
private static readonly string[] DriveIconNames = { "GenericHardDiskIcon.icns", "ExternalDiskIcon.icns", "RemovableDiskIcon.icns" };
public static byte[]? TryGetIconPngBytes(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
return null;
}
try
{
return TryGetIconUsingNSWorkspace(filePath);
}
catch
{
}
try
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();
return TryGetIconForExtension(extension);
}
catch
{
return null;
}
}
public static byte[]? TryGetSystemFolderIconPngBytes()
{
foreach (var folderPath in SystemFolderPaths)
{
if (!Directory.Exists(folderPath))
{
continue;
}
foreach (var iconName in FolderIconNames)
{
var iconPath = Path.Combine(folderPath, iconName);
if (File.Exists(iconPath))
{
var pngBytes = TryConvertIcnsToPng(iconPath);
if (pngBytes is not null)
{
return pngBytes;
}
}
}
}
return TryGetIconUsingNSWorkspace("/System/Library/CoreServices");
}
public static byte[]? TryGetDriveIconPngBytes()
{
foreach (var folderPath in SystemFolderPaths)
{
if (!Directory.Exists(folderPath))
{
continue;
}
foreach (var iconName in DriveIconNames)
{
var iconPath = Path.Combine(folderPath, iconName);
if (File.Exists(iconPath))
{
var pngBytes = TryConvertIcnsToPng(iconPath);
if (pngBytes is not null)
{
return pngBytes;
}
}
}
}
return TryGetIconUsingNSWorkspace("/");
}
private static byte[]? TryGetIconUsingNSWorkspace(string filePath)
{
try
{
var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png");
var script = $@"
tell application ""System Events""
set theIcon to icon of file ""{filePath}""
end tell
";
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "osascript",
Arguments = $"-e 'tell application \"Finder\" to get icon of file \"{filePath}\"'",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
return TryGetIconUsingSips(filePath);
}
catch
{
return null;
}
}
private static byte[]? TryGetIconUsingSips(string filePath)
{
try
{
var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png");
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sips",
Arguments = $"-s format png -z {IconSize} {IconSize} \"{filePath}\" --out \"{tempPath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit(5000);
if (File.Exists(tempPath))
{
var bytes = File.ReadAllBytes(tempPath);
File.Delete(tempPath);
return bytes;
}
}
catch
{
}
return null;
}
private static byte[]? TryGetIconForExtension(string extension)
{
var iconName = GetIconNameForExtension(extension);
foreach (var folderPath in SystemFolderPaths)
{
if (!Directory.Exists(folderPath))
{
continue;
}
var iconPath = Path.Combine(folderPath, iconName);
if (File.Exists(iconPath))
{
var pngBytes = TryConvertIcnsToPng(iconPath);
if (pngBytes is not null)
{
return pngBytes;
}
}
}
return null;
}
private static string GetIconNameForExtension(string extension)
{
return extension switch
{
".txt" => "TextEdit.icns",
".md" => "TextEdit.icns",
".pdf" => "Preview.icns",
".doc" or ".docx" => "Microsoft Word.icns",
".xls" or ".xlsx" => "Microsoft Excel.icns",
".ppt" or ".pptx" => "Microsoft PowerPoint.icns",
".zip" or ".rar" or ".7z" => "Archive Utility.icns",
".mp3" or ".wav" or ".flac" or ".aac" => "Music.icns",
".mp4" or ".avi" or ".mkv" or ".mov" => "QuickTime Player.icns",
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" => "Preview.icns",
".cs" => "Visual Studio.icns",
".js" or ".ts" => "Visual Studio Code.icns",
".py" => "IDLE.icns",
".json" => "TextEdit.icns",
".xml" => "TextEdit.icns",
".html" or ".htm" => "Safari.icns",
".css" => "TextEdit.icns",
".sh" => "Terminal.icns",
".app" => "AppIcon.icns",
".dmg" => "DiskImage.icns",
_ => "GenericDocumentIcon.icns"
};
}
private static byte[]? TryConvertIcnsToPng(string icnsPath)
{
if (!File.Exists(icnsPath))
{
return null;
}
try
{
var tempPath = Path.Combine(Path.GetTempPath(), $"icon_{Guid.NewGuid():N}.png");
using var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "sips",
Arguments = $"-s format png \"{icnsPath}\" --out \"{tempPath}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
process.WaitForExit(5000);
if (File.Exists(tempPath))
{
var bytes = File.ReadAllBytes(tempPath);
File.Delete(tempPath);
return bytes;
}
}
catch
{
}
return null;
}
}

View File

@@ -93,6 +93,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
private const uint SWP_NOACTIVATE = 0x0010;
private const int WM_WINDOWPOSCHANGING = 0x0046;
private const int WM_NCHITTEST = 0x0084;
private const int WM_ACTIVATEAPP = 0x001C; // 【新增】应用激活消息
private const int HTTRANSPARENT = -1;
private const int HTCLIENT = 1;
@@ -105,6 +106,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
private static readonly Dictionary<IntPtr, Point> _windowScreenOrigins = new();
private static readonly object _staticLock = new();
// 【修复问题1】静态持有委托引用防止 GC 回收导致 CallbackOnCollectedDelegate 崩溃
private static WndProcDelegate? _wndProcDelegate;
// 【修复问题2】记录每个窗口的 DPI 缩放比例
private static readonly Dictionary<IntPtr, double> _windowDpiScales = new();
// 【修复问题5】Z 轴竞争优化 - 记录上次置底时间,避免频繁操作
private static readonly Dictionary<IntPtr, long> _lastSendToBottomTime = new();
private const long MinSendToBottomIntervalMs = 100; // 【修复置底问题】降低到 100ms提高响应速度
// 【新增】定时器定期强制置底
private static System.Timers.Timer? _keepBottomTimer;
private static readonly object _timerLock = new();
public bool IsBottomMostSupported => true;
public void SetupBottomMost(Window window)
@@ -130,6 +145,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
_bottomMostWindows[handle] = true;
_interactiveRegions[handle] = [];
UpdateWindowScreenOrigin(handle);
UpdateWindowDpiScale(handle); // 【修复问题2】初始化 DPI 缩放
}
// 注入消息钩子
@@ -138,6 +154,9 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
// 初始置底
SendToBottomInternal(handle);
// 【新增】启动定时器定期强制置底
StartKeepBottomTimer();
AppLogger.Info("WindowBottomMost", $"Window setup as bottom-most: {handle}");
};
@@ -152,6 +171,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
_originalWndProcs.Remove(handle);
_interactiveRegions.Remove(handle);
_windowScreenOrigins.Remove(handle);
_windowDpiScales.Remove(handle); // 【修复问题2】清理 DPI 缩放记录
}
}
};
@@ -174,21 +194,113 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
SetWindowPos(handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
}
/// <summary>
/// 【新增】启动定时器定期强制置底所有窗口
/// </summary>
private static void StartKeepBottomTimer()
{
lock (_timerLock)
{
if (_keepBottomTimer != null) return;
_keepBottomTimer = new System.Timers.Timer(200); // 每 200ms 检查一次
_keepBottomTimer.Elapsed += (s, e) =>
{
try
{
lock (_staticLock)
{
foreach (var kvp in _bottomMostWindows)
{
if (kvp.Value) // 如果标记为置底
{
SendToBottomInternal(kvp.Key);
}
}
}
}
catch
{
// 忽略定时器错误
}
};
_keepBottomTimer.Start();
}
}
/// <summary>
/// 【新增】停止定时器
/// </summary>
private static void StopKeepBottomTimer()
{
lock (_timerLock)
{
_keepBottomTimer?.Stop();
_keepBottomTimer?.Dispose();
_keepBottomTimer = null;
}
}
private static void SetAsDesktopChild(IntPtr handle)
{
// 【修复问题4】增强桌面挂载逻辑支持 Wallpaper Engine 等动态壁纸软件
// 方案1: 尝试找到 WorkerW 层Wallpaper Engine 创建的层)
var workerW = IntPtr.Zero;
var hDefView = IntPtr.Zero;
// 枚举所有顶层窗口
var windowHandles = new ArrayList();
EnumWindows(EnumWindowsCallback, windowHandles);
foreach (IntPtr h in windowHandles)
{
var hDefView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null);
// 查找 WorkerW 窗口Wallpaper Engine 创建)
var className = GetWindowClassName(h);
if (className == "WorkerW")
{
// 在 WorkerW 下查找 SHELLDLL_DefView
var defView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null);
if (defView != IntPtr.Zero)
{
workerW = h;
hDefView = defView;
break;
}
}
}
// 如果找到了 WorkerW 层,使用它作为父窗口
if (workerW != IntPtr.Zero && hDefView != IntPtr.Zero)
{
SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32());
AppLogger.Info("WindowBottomMost", "Mounted to WorkerW layer (Wallpaper Engine detected)");
return;
}
// 方案2: 回退到传统方式,查找 Progman 下的 SHELLDLL_DefView
foreach (IntPtr h in windowHandles)
{
hDefView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null);
if (hDefView != IntPtr.Zero)
{
SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32());
AppLogger.Info("WindowBottomMost", "Mounted to traditional desktop layer");
break;
}
}
}
/// <summary>
/// 【修复问题4】获取窗口类名
/// </summary>
private static string GetWindowClassName(IntPtr hWnd)
{
var buffer = new char[256];
var length = GetClassName(hWnd, buffer, buffer.Length);
return length > 0 ? new string(buffer, 0, length) : string.Empty;
}
private static bool EnumWindowsCallback(IntPtr handle, ArrayList handles)
{
handles.Add(handle);
@@ -203,13 +315,29 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
lock (_staticLock)
{
_originalWndProcs[handle] = originalWndProc;
// 【修复问题1】确保委托实例被静态引用持有防止 GC 回收
_wndProcDelegate ??= SubclassWndProc;
}
SetWindowLongPtr(handle, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate<WndProcDelegate>(SubclassWndProc));
SetWindowLongPtr(handle, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(_wndProcDelegate));
}
private static IntPtr SubclassWndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
{
// 【新增】处理应用激活消息 - 当其他应用激活时立即置底
if (msg == WM_ACTIVATEAPP)
{
lock (_staticLock)
{
if (_bottomMostWindows.TryGetValue(hWnd, out var isBottomMost) && isBottomMost)
{
// 立即置底,不进行频率限制
SendToBottomInternal(hWnd);
}
}
}
// 处理 WM_WINDOWPOSCHANGING - 保持置底
if (msg == WM_WINDOWPOSCHANGING)
{
@@ -217,7 +345,19 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
{
if (_bottomMostWindows.TryGetValue(hWnd, out var isBottomMost) && isBottomMost)
{
// 【修复问题5】优化 Z 轴竞争 - 限制置底操作频率
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
if (_lastSendToBottomTime.TryGetValue(hWnd, out var lastTime))
{
if (now - lastTime < MinSendToBottomIntervalMs)
{
// 跳过过于频繁的置底操作
goto CallOriginal;
}
}
SendToBottomInternal(hWnd);
_lastSendToBottomTime[hWnd] = now;
}
}
}
@@ -233,11 +373,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
{
if (_interactiveRegions.TryGetValue(hWnd, out var regions) && regions.Count > 0)
{
// 将屏幕坐标转为窗口相对坐标_interactiveRegions 存的是窗口内坐标)
// 【修复问题2】获取窗口原点和 DPI 缩放比例
_windowScreenOrigins.TryGetValue(hWnd, out var origin);
_windowDpiScales.TryGetValue(hWnd, out var dpiScale);
if (dpiScale <= 0) dpiScale = 1.0; // 默认缩放为 1.0
// 将屏幕物理像素坐标转为窗口相对坐标
var clientX = screenX - origin.X;
var clientY = screenY - origin.Y;
var point = new Point(clientX, clientY);
// 【修复问题2】将物理像素坐标转换为逻辑 DIP 坐标
// _interactiveRegions 存储的是 Avalonia UI 的逻辑 DIP 坐标
var logicalX = clientX / dpiScale;
var logicalY = clientY / dpiScale;
var point = new Point(logicalX, logicalY);
foreach (var region in regions)
{
@@ -255,6 +404,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
}
// 调用原始窗口过程
CallOriginal:
IntPtr originalWndProc;
lock (_staticLock)
{
@@ -277,6 +427,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
_interactiveRegions[handle] = regions;
// 同步刷新屏幕原点DPI 缩放可能影响坐标,每次更新区域时一并刷新)
UpdateWindowScreenOrigin(handle);
UpdateWindowDpiScale(handle); // 【修复问题2】同步更新 DPI 缩放
}
}
@@ -291,6 +442,31 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
}
}
/// <summary>
/// 【修复问题2】更新指定窗口的 DPI 缩放比例
/// </summary>
private static void UpdateWindowDpiScale(IntPtr handle)
{
try
{
// 获取窗口所在的显示器 DPI
var monitor = MonitorFromWindow(handle, MONITOR_DEFAULTTONEAREST);
if (monitor != IntPtr.Zero)
{
if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out var dpiX, out var _) == 0)
{
// DPI 缩放比例 = 当前 DPI / 96 (标准 DPI)
_windowDpiScales[handle] = dpiX / 96.0;
}
}
}
catch
{
// 如果获取失败,使用默认缩放 1.0
_windowDpiScales[handle] = 1.0;
}
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int Left, Top, Right, Bottom; }
@@ -328,6 +504,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
[DllImport("user32.dll")]
private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);
// 【修复问题2】DPI 相关的 P/Invoke 声明
private const int MONITOR_DEFAULTTONEAREST = 2;
private const int MDT_EFFECTIVE_DPI = 0;
[DllImport("user32.dll")]
private static extern IntPtr MonitorFromWindow(IntPtr hWnd, int dwFlags);
[DllImport("shcore.dll")]
private static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);
// 【修复问题4】获取窗口类名的 P/Invoke
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetClassName(IntPtr hWnd, char[] lpClassName, int nMaxCount);
}
/// <summary>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
@@ -696,18 +696,23 @@ internal static class WindowsIconService
try
{
using var source = Image.FromHbitmap(bitmapHandle);
using var bitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb);
var width = source.Width;
var height = source.Height;
using var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.Transparent);
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.CompositingMode = CompositingMode.SourceCopy;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, source.Width, source.Height);
graphics.DrawImage(source, 0, 0, width, height);
}
FixBitmapAlpha(bitmap);
using var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream.ToArray();
@@ -718,6 +723,47 @@ internal static class WindowsIconService
}
}
private static void FixBitmapAlpha(Bitmap bitmap)
{
var width = bitmap.Width;
var height = bitmap.Height;
var rect = new Rectangle(0, 0, width, height);
var data = bitmap.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
try
{
var bytes = Math.Abs(data.Stride) * height;
var buffer = new byte[bytes];
Marshal.Copy(data.Scan0, buffer, 0, bytes);
for (var i = 0; i < bytes; i += 4)
{
var b = buffer[i];
var g = buffer[i + 1];
var r = buffer[i + 2];
var a = buffer[i + 3];
if (a == 0 && (r != 0 || g != 0 || b != 0))
{
a = (byte)Math.Max(r, Math.Max(g, b));
buffer[i + 3] = a;
}
else if (a > 0 && a < 255)
{
buffer[i] = (byte)(b * 255 / a);
buffer[i + 1] = (byte)(g * 255 / a);
buffer[i + 2] = (byte)(r * 255 / a);
}
}
Marshal.Copy(buffer, 0, data.Scan0, bytes);
}
finally
{
bitmap.UnlockBits(data);
}
}
private static bool TryInitializeCom(out bool shouldUninitialize)
{
shouldUninitialize = false;

View File

@@ -9,6 +9,8 @@ using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using FluentIcons.Avalonia;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
@@ -34,6 +36,18 @@ public partial class FileManagerWidget : UserControl,
private bool _isAttached;
private bool _isDisposed;
private const double TapMovementThreshold = 10;
private const long TapTimeThresholdMs = 500;
private readonly Dictionary<int, PointerGestureState> _gestureStates = new();
private record PointerGestureState(
Point StartPosition,
long StartTime,
FileSystemItem Item,
Border Border
);
public FileManagerWidget()
{
InitializeComponent();
@@ -90,6 +104,8 @@ public partial class FileManagerWidget : UserControl,
AttachedToVisualTree -= OnAttachedToVisualTree;
DetachedFromVisualTree -= OnDetachedFromVisualTree;
SizeChanged -= OnSizeChanged;
_gestureStates.Clear();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
@@ -198,20 +214,80 @@ public partial class FileManagerWidget : UserControl,
private void OnItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
_ = e;
if (sender is not Border border || border.DataContext is not FileSystemItem item)
{
return;
}
if (item.IsDirectory)
var pointer = e.GetCurrentPoint(border);
var pointerId = e.Pointer.Id;
var position = pointer.Position;
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
_gestureStates[pointerId] = new PointerGestureState(position, timestamp, item, border);
e.Pointer.Capture(border);
}
private void OnItemPointerMoved(object? sender, PointerEventArgs e)
{
if (sender is not Border border)
{
LoadDirectory(item.FullPath, addToHistory: true);
return;
}
else
var pointerId = e.Pointer.Id;
if (!_gestureStates.TryGetValue(pointerId, out var state))
{
OpenFile(item.FullPath);
return;
}
var currentPoint = e.GetCurrentPoint(border);
var distance = Math.Sqrt(
Math.Pow(currentPoint.Position.X - state.StartPosition.X, 2) +
Math.Pow(currentPoint.Position.Y - state.StartPosition.Y, 2)
);
if (distance > TapMovementThreshold)
{
_gestureStates.Remove(pointerId);
e.Pointer.Capture(null);
}
}
private void OnItemPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (sender is not Border border)
{
return;
}
var pointerId = e.Pointer.Id;
if (!_gestureStates.Remove(pointerId, out var state))
{
return;
}
e.Pointer.Capture(null);
var currentPoint = e.GetCurrentPoint(border);
var distance = Math.Sqrt(
Math.Pow(currentPoint.Position.X - state.StartPosition.X, 2) +
Math.Pow(currentPoint.Position.Y - state.StartPosition.Y, 2)
);
var elapsed = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - state.StartTime;
if (distance <= TapMovementThreshold && elapsed <= TapTimeThresholdMs)
{
if (state.Item.IsDirectory)
{
LoadDirectory(state.Item.FullPath, addToHistory: true);
}
else
{
OpenFile(state.Item.FullPath);
}
}
}
@@ -225,34 +301,118 @@ public partial class FileManagerWidget : UserControl,
{
var drives = new List<FileSystemItem>();
foreach (var drive in DriveInfo.GetDrives())
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
foreach (var drive in DriveInfo.GetDrives())
{
if (!drive.IsReady)
try
{
continue;
}
if (!drive.IsReady)
{
continue;
}
var item = FileSystemItem.FromDriveInfo(drive);
drives.Add(item);
var item = FileSystemItem.FromDriveInfo(drive);
drives.Add(item);
}
catch (Exception ex)
{
AppLogger.Warn("FileManagerWidget", $"Failed to access drive: {drive?.Name}", ex);
}
}
catch (Exception ex)
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
drives.Add(new FileSystemItem
{
AppLogger.Warn("FileManagerWidget", $"Failed to access drive: {drive?.Name}", ex);
Name = "根目录",
FullPath = "/",
ItemType = FileSystemItemType.Directory
});
var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(homePath) && Directory.Exists(homePath))
{
drives.Add(new FileSystemItem
{
Name = "主目录",
FullPath = homePath,
ItemType = FileSystemItemType.Directory
});
}
var linuxMountPoints = new[] { "/mnt", "/media", "/run/media" };
foreach (var mount in linuxMountPoints)
{
if (Directory.Exists(mount))
{
drives.Add(new FileSystemItem
{
Name = mount,
FullPath = mount,
ItemType = FileSystemItemType.Directory
});
}
}
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
drives.Add(new FileSystemItem
{
Name = "根目录",
FullPath = "/",
ItemType = FileSystemItemType.Directory
});
drives.Add(new FileSystemItem
{
Name = "用户",
FullPath = "/Users",
ItemType = FileSystemItemType.Directory
});
drives.Add(new FileSystemItem
{
Name = "应用程序",
FullPath = "/Applications",
ItemType = FileSystemItemType.Directory
});
var homePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (!string.IsNullOrEmpty(homePath) && Directory.Exists(homePath))
{
drives.Add(new FileSystemItem
{
Name = "个人",
FullPath = homePath,
ItemType = FileSystemItemType.Directory
});
}
if (Directory.Exists("/Volumes"))
{
foreach (var volume in Directory.GetDirectories("/Volumes"))
{
drives.Add(new FileSystemItem
{
Name = Path.GetFileName(volume),
FullPath = volume,
ItemType = FileSystemItemType.Directory
});
}
}
}
RenderFileItems(drives);
PathTextBlock.Text = "此电脑";
PathTextBlock.Text = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "此电脑" : "文件系统";
UpdateEmptyState(drives.Count == 0, "没有可用的驱动器");
UpdateEmptyState(drives.Count == 0, "没有可用的位置");
ErrorStatePanel.IsVisible = false;
}
catch (Exception ex)
{
AppLogger.Warn("FileManagerWidget", "Failed to load drives.", ex);
ShowError("无法加载驱动器列表");
ShowError("无法加载位置列表");
}
}
@@ -354,18 +514,6 @@ public partial class FileManagerWidget : UserControl,
var iconSize = Math.Clamp(32 * scale, 24, 40);
var fontSize = Math.Clamp(11 * scale, 10, 14);
// 根据类型选择图标
var symbol = item.ItemType switch
{
FileSystemItemType.Drive => FluentIcons.Common.Symbol.HardDrive,
FileSystemItemType.Directory => FluentIcons.Common.Symbol.Folder,
_ => FluentIcons.Common.Symbol.Document
};
var iconBrush = item.ItemType == FileSystemItemType.File
? this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray)
: this.FindResource("AdaptiveAccentBrush") as IBrush ?? new SolidColorBrush(Colors.DodgerBlue);
var textBrush = this.FindResource("AdaptiveTextPrimaryBrush") as IBrush ?? new SolidColorBrush(Colors.White);
var border = new Border
@@ -385,17 +533,8 @@ public partial class FileManagerWidget : UserControl,
Margin = new Thickness(4)
};
// 图标
var icon = new SymbolIcon
{
Symbol = symbol,
FontSize = iconSize,
Foreground = iconBrush,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
var iconImage = CreateSystemIconImage(item, iconSize);
// 名称
var textBlock = new TextBlock
{
Text = item.Name,
@@ -407,23 +546,157 @@ public partial class FileManagerWidget : UserControl,
Foreground = textBrush
};
grid.Children.Add(icon);
Grid.SetRow(icon, 0);
if (iconImage is not null)
{
grid.Children.Add(iconImage);
Grid.SetRow(iconImage, 0);
}
grid.Children.Add(textBlock);
Grid.SetRow(textBlock, 1);
border.Child = grid;
// 添加提示
ToolTip.SetTip(border, item.Name);
// 添加点击事件
border.PointerPressed += OnItemPointerPressed;
border.PointerMoved += OnItemPointerMoved;
border.PointerReleased += OnItemPointerReleased;
return border;
}
private Control? CreateSystemIconImage(FileSystemItem item, double iconSize)
{
byte[]? pngBytes = null;
try
{
if (OperatingSystem.IsWindows())
{
pngBytes = item.ItemType switch
{
FileSystemItemType.Drive => GetDriveIconBytes(item.FullPath),
FileSystemItemType.Directory => WindowsIconService.TryGetSystemFolderIconPngBytes(),
_ => WindowsIconService.TryGetIconPngBytes(item.FullPath)
};
}
else if (OperatingSystem.IsLinux())
{
pngBytes = item.ItemType switch
{
FileSystemItemType.Drive => LinuxIconService.TryGetDriveIconPngBytes(),
FileSystemItemType.Directory => LinuxIconService.TryGetSystemFolderIconPngBytes(),
_ => LinuxIconService.TryGetIconPngBytes(item.FullPath)
};
}
else if (OperatingSystem.IsMacOS())
{
pngBytes = item.ItemType switch
{
FileSystemItemType.Drive => MacIconService.TryGetDriveIconPngBytes(),
FileSystemItemType.Directory => MacIconService.TryGetSystemFolderIconPngBytes(),
_ => MacIconService.TryGetIconPngBytes(item.FullPath)
};
}
}
catch
{
pngBytes = null;
}
if (pngBytes is not null)
{
try
{
using var stream = new MemoryStream(pngBytes);
var bitmap = new Bitmap(stream);
return new Image
{
Source = bitmap,
Width = iconSize,
Height = iconSize,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Stretch = Stretch.Uniform
};
}
catch
{
}
}
return CreateFallbackIconImage(item, iconSize);
}
private static byte[]? GetDriveIconBytes(string drivePath)
{
if (string.IsNullOrWhiteSpace(drivePath))
{
return null;
}
try
{
if (OperatingSystem.IsWindows())
{
if (Directory.Exists(drivePath))
{
return WindowsIconService.TryGetIconPngBytes(drivePath);
}
}
else if (OperatingSystem.IsLinux())
{
return LinuxIconService.TryGetDriveIconPngBytes();
}
else if (OperatingSystem.IsMacOS())
{
return MacIconService.TryGetDriveIconPngBytes();
}
}
catch
{
}
if (OperatingSystem.IsWindows())
{
return WindowsIconService.TryGetSystemFolderIconPngBytes();
}
else if (OperatingSystem.IsLinux())
{
return LinuxIconService.TryGetSystemFolderIconPngBytes();
}
else if (OperatingSystem.IsMacOS())
{
return MacIconService.TryGetSystemFolderIconPngBytes();
}
return null;
}
private Control CreateFallbackIconImage(FileSystemItem item, double iconSize)
{
var symbol = item.ItemType switch
{
FileSystemItemType.Drive => FluentIcons.Common.Symbol.HardDrive,
FileSystemItemType.Directory => FluentIcons.Common.Symbol.Folder,
_ => FluentIcons.Common.Symbol.Document
};
var iconBrush = item.ItemType == FileSystemItemType.File
? this.FindResource("AdaptiveTextSecondaryBrush") as IBrush ?? new SolidColorBrush(Colors.Gray)
: this.FindResource("AdaptiveAccentBrush") as IBrush ?? new SolidColorBrush(Colors.DodgerBlue);
return new SymbolIcon
{
Symbol = symbol,
FontSize = iconSize,
Foreground = iconBrush,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
};
}
private void RefreshCurrentDirectory()
{
if (string.IsNullOrEmpty(_currentPath))
@@ -481,35 +754,66 @@ public partial class FileManagerWidget : UserControl,
{
if (string.IsNullOrWhiteSpace(path))
{
return "此电脑";
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "此电脑" : "文件系统";
}
// 如果是驱动器根目录,显示驱动器名称
if (path.Length <= 3 && path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '\\' : '/';
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
if (path.Length <= 3 && path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
{
var driveInfo = new DriveInfo(path.Substring(0, 1));
if (!string.IsNullOrWhiteSpace(driveInfo.VolumeLabel))
try
{
return $"{driveInfo.VolumeLabel} ({path.Substring(0, 2)})";
var driveInfo = new DriveInfo(path.Substring(0, 1));
if (!string.IsNullOrWhiteSpace(driveInfo.VolumeLabel))
{
return $"{driveInfo.VolumeLabel} ({path.Substring(0, 2)})";
}
}
catch
{
}
return path;
}
}
else
{
if (path == "/")
{
return "根目录";
}
if (path == Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
{
return "主目录";
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
if (path == "/Applications")
{
return "应用程序";
}
if (path == "/Users")
{
return "用户";
}
if (path.StartsWith("/Volumes/"))
{
return Path.GetFileName(path);
}
}
catch
{
// 忽略错误,返回默认格式
}
return path;
}
// 智能路径截断:保留根目录和最后两级
var parts = path.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length <= 3)
{
return path;
}
// 格式:根目录\...\父文件夹\当前文件夹
return $"{parts[0]}\\...\\{parts[^2]}\\{parts[^1]}";
return $"{parts[0]}{separator}...{separator}{parts[^2]}{separator}{parts[^1]}";
}
}

View File

@@ -258,18 +258,43 @@ public partial class TransparentOverlayWindow : Window
return;
}
var control = descriptor.CreateControl(
_currentDesktopCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
placement.PlacementId);
// 【修复问题3】尝试从现有窗口中获取组件实例避免重新创建导致状态丢失
var control = TryGetExistingControl(placement.PlacementId);
if (control is null)
{
// 如果没有现有实例,才创建新的
control = descriptor.CreateControl(
_currentDesktopCellSize,
_timeZoneService,
_weatherDataService,
_recommendationInfoService,
_calculatorDataService,
_settingsFacade,
placement.PlacementId);
}
RenderComponent(placement.PlacementId, control, placement.X, placement.Y, placement.Width, placement.Height);
}
/// <summary>
/// 【修复问题3】尝试从现有的小窗口中获取组件控件实例
/// </summary>
private Control? TryGetExistingControl(string placementId)
{
try
{
var manager = FusedDesktopManagerServiceFactory.GetOrCreate();
// 通过反射或公共 API 获取现有窗口中的控件
// 这里需要 FusedDesktopManagerService 提供获取控件的方法
// 暂时返回 null后续需要扩展接口
return null;
}
catch
{
return null;
}
}
/// <summary>
/// 移除组件
/// </summary>