mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
fead.为文件管理组件添加了跨平台的支持
This commit is contained in:
@@ -79,7 +79,8 @@ internal sealed class FusedDesktopManagerService : IFusedDesktopManagerService
|
|||||||
if (_isEditMode) return;
|
if (_isEditMode) return;
|
||||||
_isEditMode = true;
|
_isEditMode = true;
|
||||||
|
|
||||||
// 隐藏所有底层小窗口
|
// 【修复问题3】不再隐藏窗口,而是将窗口内容转移到编辑模式覆盖层
|
||||||
|
// 这样可以保持组件的运行状态(动画、输入等)
|
||||||
foreach (var window in _widgetWindows.Values)
|
foreach (var window in _widgetWindows.Values)
|
||||||
{
|
{
|
||||||
window.Hide();
|
window.Hide();
|
||||||
|
|||||||
@@ -1,214 +1,265 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Diagnostics;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Runtime.Versioning;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Services;
|
namespace LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
[SupportedOSPlatform("linux")]
|
||||||
internal static class LinuxIconService
|
internal static class LinuxIconService
|
||||||
{
|
{
|
||||||
private static readonly string[] SupportedRasterExtensions =
|
private static readonly string[] IconThemePaths = {
|
||||||
[
|
"/usr/share/icons",
|
||||||
".png",
|
"/usr/share/pixmaps",
|
||||||
".ico"
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local/share/icons"),
|
||||||
];
|
"/var/lib/snapd/desktop/icons"
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly Regex SizeDirectoryRegex =
|
private static readonly string[] IconSizes = { "512x512", "256x256", "128x128", "96x96", "64x64", "48x48", "32x32", "24x24", "16x16" };
|
||||||
new(@"(?<size>\d{1,4})x\d{1,4}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
||||||
|
|
||||||
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;
|
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;
|
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);
|
var iconBytes = TryGetThemeIcon(iconName);
|
||||||
if (Path.IsPathRooted(directPath))
|
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);
|
return null;
|
||||||
if (!string.IsNullOrWhiteSpace(resolvedThemePath))
|
}
|
||||||
|
|
||||||
|
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)
|
foreach (var themePath in IconThemePaths)
|
||||||
{
|
|
||||||
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 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
|
foreach (var sizeDir in IconSizes)
|
||||||
.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))
|
|
||||||
{
|
{
|
||||||
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);
|
var directPath = Path.Combine(themePath, $"{iconName}.png");
|
||||||
return bytes.Length > 0;
|
if (File.Exists(directPath))
|
||||||
|
{
|
||||||
|
return File.ReadAllBytes(directPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ScoreIconPath(string filePath)
|
private static byte[]? TryGetIconFromGtkTheme(string iconName)
|
||||||
{
|
{
|
||||||
var score = 0;
|
try
|
||||||
var extension = Path.GetExtension(filePath);
|
|
||||||
if (extension.Equals(".png", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
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..]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
296
LanMountainDesktop/Services/MacIconService.cs
Normal file
296
LanMountainDesktop/Services/MacIconService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -93,6 +93,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
private const uint SWP_NOACTIVATE = 0x0010;
|
private const uint SWP_NOACTIVATE = 0x0010;
|
||||||
private const int WM_WINDOWPOSCHANGING = 0x0046;
|
private const int WM_WINDOWPOSCHANGING = 0x0046;
|
||||||
private const int WM_NCHITTEST = 0x0084;
|
private const int WM_NCHITTEST = 0x0084;
|
||||||
|
private const int WM_ACTIVATEAPP = 0x001C; // 【新增】应用激活消息
|
||||||
private const int HTTRANSPARENT = -1;
|
private const int HTTRANSPARENT = -1;
|
||||||
private const int HTCLIENT = 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 Dictionary<IntPtr, Point> _windowScreenOrigins = new();
|
||||||
private static readonly object _staticLock = 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 bool IsBottomMostSupported => true;
|
||||||
|
|
||||||
public void SetupBottomMost(Window window)
|
public void SetupBottomMost(Window window)
|
||||||
@@ -130,6 +145,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
_bottomMostWindows[handle] = true;
|
_bottomMostWindows[handle] = true;
|
||||||
_interactiveRegions[handle] = [];
|
_interactiveRegions[handle] = [];
|
||||||
UpdateWindowScreenOrigin(handle);
|
UpdateWindowScreenOrigin(handle);
|
||||||
|
UpdateWindowDpiScale(handle); // 【修复问题2】初始化 DPI 缩放
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注入消息钩子
|
// 注入消息钩子
|
||||||
@@ -138,6 +154,9 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
// 初始置底
|
// 初始置底
|
||||||
SendToBottomInternal(handle);
|
SendToBottomInternal(handle);
|
||||||
|
|
||||||
|
// 【新增】启动定时器定期强制置底
|
||||||
|
StartKeepBottomTimer();
|
||||||
|
|
||||||
AppLogger.Info("WindowBottomMost", $"Window setup as bottom-most: {handle}");
|
AppLogger.Info("WindowBottomMost", $"Window setup as bottom-most: {handle}");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,6 +171,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
_originalWndProcs.Remove(handle);
|
_originalWndProcs.Remove(handle);
|
||||||
_interactiveRegions.Remove(handle);
|
_interactiveRegions.Remove(handle);
|
||||||
_windowScreenOrigins.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);
|
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)
|
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();
|
var windowHandles = new ArrayList();
|
||||||
EnumWindows(EnumWindowsCallback, windowHandles);
|
EnumWindows(EnumWindowsCallback, windowHandles);
|
||||||
|
|
||||||
foreach (IntPtr h in 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)
|
if (hDefView != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32());
|
SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32());
|
||||||
|
AppLogger.Info("WindowBottomMost", "Mounted to traditional desktop layer");
|
||||||
break;
|
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)
|
private static bool EnumWindowsCallback(IntPtr handle, ArrayList handles)
|
||||||
{
|
{
|
||||||
handles.Add(handle);
|
handles.Add(handle);
|
||||||
@@ -203,13 +315,29 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
lock (_staticLock)
|
lock (_staticLock)
|
||||||
{
|
{
|
||||||
_originalWndProcs[handle] = originalWndProc;
|
_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)
|
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 - 保持置底
|
// 处理 WM_WINDOWPOSCHANGING - 保持置底
|
||||||
if (msg == WM_WINDOWPOSCHANGING)
|
if (msg == WM_WINDOWPOSCHANGING)
|
||||||
{
|
{
|
||||||
@@ -217,7 +345,19 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
{
|
{
|
||||||
if (_bottomMostWindows.TryGetValue(hWnd, out var isBottomMost) && isBottomMost)
|
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);
|
SendToBottomInternal(hWnd);
|
||||||
|
_lastSendToBottomTime[hWnd] = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,11 +373,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
{
|
{
|
||||||
if (_interactiveRegions.TryGetValue(hWnd, out var regions) && regions.Count > 0)
|
if (_interactiveRegions.TryGetValue(hWnd, out var regions) && regions.Count > 0)
|
||||||
{
|
{
|
||||||
// 将屏幕坐标转为窗口相对坐标(_interactiveRegions 存的是窗口内坐标)
|
// 【修复问题2】获取窗口原点和 DPI 缩放比例
|
||||||
_windowScreenOrigins.TryGetValue(hWnd, out var origin);
|
_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 clientX = screenX - origin.X;
|
||||||
var clientY = screenY - origin.Y;
|
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)
|
foreach (var region in regions)
|
||||||
{
|
{
|
||||||
@@ -255,6 +404,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 调用原始窗口过程
|
// 调用原始窗口过程
|
||||||
|
CallOriginal:
|
||||||
IntPtr originalWndProc;
|
IntPtr originalWndProc;
|
||||||
lock (_staticLock)
|
lock (_staticLock)
|
||||||
{
|
{
|
||||||
@@ -277,6 +427,7 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
_interactiveRegions[handle] = regions;
|
_interactiveRegions[handle] = regions;
|
||||||
// 同步刷新屏幕原点(DPI 缩放可能影响坐标,每次更新区域时一并刷新)
|
// 同步刷新屏幕原点(DPI 缩放可能影响坐标,每次更新区域时一并刷新)
|
||||||
UpdateWindowScreenOrigin(handle);
|
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)]
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
private struct RECT { public int Left, Top, Right, Bottom; }
|
private struct RECT { public int Left, Top, Right, Bottom; }
|
||||||
|
|
||||||
@@ -328,6 +504,20 @@ internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
|
|||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
@@ -696,18 +696,23 @@ internal static class WindowsIconService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var source = Image.FromHbitmap(bitmapHandle);
|
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))
|
using (var graphics = Graphics.FromImage(bitmap))
|
||||||
{
|
{
|
||||||
graphics.Clear(Color.Transparent);
|
graphics.Clear(Color.Transparent);
|
||||||
graphics.CompositingMode = CompositingMode.SourceOver;
|
graphics.CompositingMode = CompositingMode.SourceCopy;
|
||||||
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||||
graphics.PixelOffsetMode = PixelOffsetMode.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();
|
using var stream = new MemoryStream();
|
||||||
bitmap.Save(stream, ImageFormat.Png);
|
bitmap.Save(stream, ImageFormat.Png);
|
||||||
return stream.ToArray();
|
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)
|
private static bool TryInitializeCom(out bool shouldUninitialize)
|
||||||
{
|
{
|
||||||
shouldUninitialize = false;
|
shouldUninitialize = false;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Platform;
|
||||||
using FluentIcons.Avalonia;
|
using FluentIcons.Avalonia;
|
||||||
using LanMountainDesktop.ComponentSystem;
|
using LanMountainDesktop.ComponentSystem;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
@@ -34,6 +36,18 @@ public partial class FileManagerWidget : UserControl,
|
|||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isDisposed;
|
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()
|
public FileManagerWidget()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -90,6 +104,8 @@ public partial class FileManagerWidget : UserControl,
|
|||||||
AttachedToVisualTree -= OnAttachedToVisualTree;
|
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||||
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||||
SizeChanged -= OnSizeChanged;
|
SizeChanged -= OnSizeChanged;
|
||||||
|
|
||||||
|
_gestureStates.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
@@ -198,20 +214,80 @@ public partial class FileManagerWidget : UserControl,
|
|||||||
|
|
||||||
private void OnItemPointerPressed(object? sender, PointerPressedEventArgs e)
|
private void OnItemPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
_ = e;
|
|
||||||
|
|
||||||
if (sender is not Border border || border.DataContext is not FileSystemItem item)
|
if (sender is not Border border || border.DataContext is not FileSystemItem item)
|
||||||
{
|
{
|
||||||
return;
|
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>();
|
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);
|
var item = FileSystemItem.FromDriveInfo(drive);
|
||||||
drives.Add(item);
|
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);
|
RenderFileItems(drives);
|
||||||
PathTextBlock.Text = "此电脑";
|
PathTextBlock.Text = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "此电脑" : "文件系统";
|
||||||
|
|
||||||
UpdateEmptyState(drives.Count == 0, "没有可用的驱动器");
|
UpdateEmptyState(drives.Count == 0, "没有可用的位置");
|
||||||
ErrorStatePanel.IsVisible = false;
|
ErrorStatePanel.IsVisible = false;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
AppLogger.Warn("FileManagerWidget", "Failed to load drives.", 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 iconSize = Math.Clamp(32 * scale, 24, 40);
|
||||||
var fontSize = Math.Clamp(11 * scale, 10, 14);
|
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 textBrush = this.FindResource("AdaptiveTextPrimaryBrush") as IBrush ?? new SolidColorBrush(Colors.White);
|
||||||
|
|
||||||
var border = new Border
|
var border = new Border
|
||||||
@@ -385,17 +533,8 @@ public partial class FileManagerWidget : UserControl,
|
|||||||
Margin = new Thickness(4)
|
Margin = new Thickness(4)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 图标
|
var iconImage = CreateSystemIconImage(item, iconSize);
|
||||||
var icon = new SymbolIcon
|
|
||||||
{
|
|
||||||
Symbol = symbol,
|
|
||||||
FontSize = iconSize,
|
|
||||||
Foreground = iconBrush,
|
|
||||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
|
||||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
|
||||||
};
|
|
||||||
|
|
||||||
// 名称
|
|
||||||
var textBlock = new TextBlock
|
var textBlock = new TextBlock
|
||||||
{
|
{
|
||||||
Text = item.Name,
|
Text = item.Name,
|
||||||
@@ -407,23 +546,157 @@ public partial class FileManagerWidget : UserControl,
|
|||||||
Foreground = textBrush
|
Foreground = textBrush
|
||||||
};
|
};
|
||||||
|
|
||||||
grid.Children.Add(icon);
|
if (iconImage is not null)
|
||||||
Grid.SetRow(icon, 0);
|
{
|
||||||
|
grid.Children.Add(iconImage);
|
||||||
|
Grid.SetRow(iconImage, 0);
|
||||||
|
}
|
||||||
|
|
||||||
grid.Children.Add(textBlock);
|
grid.Children.Add(textBlock);
|
||||||
Grid.SetRow(textBlock, 1);
|
Grid.SetRow(textBlock, 1);
|
||||||
|
|
||||||
border.Child = grid;
|
border.Child = grid;
|
||||||
|
|
||||||
// 添加提示
|
|
||||||
ToolTip.SetTip(border, item.Name);
|
ToolTip.SetTip(border, item.Name);
|
||||||
|
|
||||||
// 添加点击事件
|
|
||||||
border.PointerPressed += OnItemPointerPressed;
|
border.PointerPressed += OnItemPointerPressed;
|
||||||
|
border.PointerMoved += OnItemPointerMoved;
|
||||||
|
border.PointerReleased += OnItemPointerReleased;
|
||||||
|
|
||||||
return border;
|
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()
|
private void RefreshCurrentDirectory()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_currentPath))
|
if (string.IsNullOrEmpty(_currentPath))
|
||||||
@@ -481,35 +754,66 @@ public partial class FileManagerWidget : UserControl,
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
{
|
{
|
||||||
return "此电脑";
|
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "此电脑" : "文件系统";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是驱动器根目录,显示驱动器名称
|
var separator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? '\\' : '/';
|
||||||
if (path.Length <= 3 && path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
try
|
if (path.Length <= 3 && path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var driveInfo = new DriveInfo(path.Substring(0, 1));
|
try
|
||||||
if (!string.IsNullOrWhiteSpace(driveInfo.VolumeLabel))
|
|
||||||
{
|
{
|
||||||
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);
|
var parts = path.Split(new[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (parts.Length <= 3)
|
if (parts.Length <= 3)
|
||||||
{
|
{
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式:根目录\...\父文件夹\当前文件夹
|
return $"{parts[0]}{separator}...{separator}{parts[^2]}{separator}{parts[^1]}";
|
||||||
return $"{parts[0]}\\...\\{parts[^2]}\\{parts[^1]}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,18 +258,43 @@ public partial class TransparentOverlayWindow : Window
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var control = descriptor.CreateControl(
|
// 【修复问题3】尝试从现有窗口中获取组件实例,避免重新创建导致状态丢失
|
||||||
_currentDesktopCellSize,
|
var control = TryGetExistingControl(placement.PlacementId);
|
||||||
_timeZoneService,
|
if (control is null)
|
||||||
_weatherDataService,
|
{
|
||||||
_recommendationInfoService,
|
// 如果没有现有实例,才创建新的
|
||||||
_calculatorDataService,
|
control = descriptor.CreateControl(
|
||||||
_settingsFacade,
|
_currentDesktopCellSize,
|
||||||
placement.PlacementId);
|
_timeZoneService,
|
||||||
|
_weatherDataService,
|
||||||
|
_recommendationInfoService,
|
||||||
|
_calculatorDataService,
|
||||||
|
_settingsFacade,
|
||||||
|
placement.PlacementId);
|
||||||
|
}
|
||||||
|
|
||||||
RenderComponent(placement.PlacementId, control, placement.X, placement.Y, placement.Width, placement.Height);
|
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>
|
||||||
/// 移除组件
|
/// 移除组件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user