初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class AumidHelper
|
||||
{
|
||||
public const string Aumid = "PCLCommunity.PCLCE";
|
||||
|
||||
public static bool HasAumid()
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid));
|
||||
return key is not null;
|
||||
}
|
||||
|
||||
public static void RegisterAumid()
|
||||
{
|
||||
// .NET 8 在正常情况下不可能返回 null,如果炸了不应该包住而是让他炸下去
|
||||
using var key = Registry.CurrentUser.CreateSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid));
|
||||
key.SetValue("DisplayName", "Plain Craft Launcher Community Edition");
|
||||
key.SetValue("IconUri", IconHelper.GetIconPath());
|
||||
key.SetValue("IconBackgroundColor", "FFDDDD");
|
||||
}
|
||||
|
||||
public static void UnregisterAumid()
|
||||
{
|
||||
Registry.CurrentUser.DeleteSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid), false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
public static class ClipboardUtils {
|
||||
/// <summary>
|
||||
/// 将剪贴板内容设置为用于复制/粘贴操作的文件或文件夹路径列表。
|
||||
/// </summary>
|
||||
/// <param name="paths">要设置到剪贴板的文件或文件夹路径数组。</param>
|
||||
public static void SetClipboardFiles(string[] paths) {
|
||||
if (paths is null || paths.Length == 0) {
|
||||
throw new ArgumentException("Paths cannot be null or empty.", nameof(paths));
|
||||
}
|
||||
|
||||
var dataObject = new DataObject();
|
||||
dataObject.SetData(DataFormats.FileDrop, paths);
|
||||
Clipboard.SetDataObject(dataObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
public partial class DragHelper
|
||||
{
|
||||
public event EventHandler? DragDrop;
|
||||
|
||||
public string[]? DropFilePaths { get; private set; }
|
||||
public Point DropDragPoint { get; private set; }
|
||||
|
||||
public HwndSource? HwndSource { get; set; }
|
||||
|
||||
#region Public API
|
||||
|
||||
public void AddHook()
|
||||
{
|
||||
if (HwndSource is null)
|
||||
throw new InvalidOperationException("HwndSource 未设置");
|
||||
|
||||
RemoveHook();
|
||||
|
||||
HwndSource.AddHook(WndProc);
|
||||
IntPtr hwnd = HwndSource.Handle;
|
||||
|
||||
if (IsUserAnAdmin())
|
||||
RevokeDragDrop(hwnd);
|
||||
|
||||
DragAcceptFiles(hwnd, true);
|
||||
ChangeMessageFilter(hwnd);
|
||||
}
|
||||
|
||||
public void RemoveHook()
|
||||
{
|
||||
if (HwndSource is null)
|
||||
return;
|
||||
|
||||
HwndSource.RemoveHook(WndProc);
|
||||
DragAcceptFiles(HwndSource.Handle, false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region WndProc
|
||||
|
||||
private IntPtr WndProc(
|
||||
IntPtr hwnd,
|
||||
int msg,
|
||||
IntPtr wParam,
|
||||
IntPtr lParam,
|
||||
ref bool handled)
|
||||
{
|
||||
if (TryGetDropInfo(msg, wParam, out var files, out var pt))
|
||||
{
|
||||
DropFilePaths = files;
|
||||
DropDragPoint = new Point(pt.X, pt.Y);
|
||||
DragDrop?.Invoke(this, EventArgs.Empty);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Message filter (UAC)
|
||||
|
||||
private static unsafe void ChangeMessageFilter(IntPtr hwnd)
|
||||
{
|
||||
var ver = Environment.OSVersion.Version;
|
||||
if (ver < new Version(6, 0))
|
||||
return;
|
||||
|
||||
var win7OrHigher = ver >= new Version(6, 1);
|
||||
|
||||
var filter = new CHANGEFILTERSTRUCT
|
||||
{
|
||||
cbSize = (uint)sizeof(CHANGEFILTERSTRUCT)
|
||||
};
|
||||
|
||||
uint[] messages = [
|
||||
WM_DROPFILES,
|
||||
WM_COPYGLOBALDATA,
|
||||
WM_COPYDATA
|
||||
];
|
||||
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
var ok = win7OrHigher
|
||||
? ChangeWindowMessageFilterEx(hwnd, msg, MSGFLT_ALLOW, ref filter)
|
||||
: ChangeWindowMessageFilter(msg, MSGFLT_ADD);
|
||||
|
||||
if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Drop parsing
|
||||
|
||||
private static bool TryGetDropInfo(
|
||||
int msg,
|
||||
IntPtr hDrop,
|
||||
out string[]? filePaths,
|
||||
out DragPoint dropPoint)
|
||||
{
|
||||
filePaths = null;
|
||||
dropPoint = default;
|
||||
|
||||
if (msg != WM_DROPFILES)
|
||||
return false;
|
||||
|
||||
var count = DragQueryFile(hDrop, uint.MaxValue, IntPtr.Zero, 0);
|
||||
filePaths = new string[count];
|
||||
|
||||
const int maxPath = 32768, smallerMaxPath = 1024;
|
||||
|
||||
Span<char> gBuffer = stackalloc char[smallerMaxPath];
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
var len = DragQueryFile(hDrop, i, IntPtr.Zero, 0) + 1;
|
||||
if (len > maxPath) len = maxPath;
|
||||
var buffer = len <= smallerMaxPath ? gBuffer[..(int)len] : new char[len];
|
||||
_ = DragQueryFile(hDrop, i, buffer, len);
|
||||
filePaths[i] = new string(buffer[..(int)(len - 1)]);
|
||||
}
|
||||
|
||||
DragFinish(hDrop);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Win32
|
||||
|
||||
private const uint WM_COPYGLOBALDATA = 0x0049;
|
||||
private const uint WM_COPYDATA = 0x004A;
|
||||
private const uint WM_DROPFILES = 0x0233;
|
||||
|
||||
private const uint MSGFLT_ALLOW = 1;
|
||||
private const uint MSGFLT_ADD = 1;
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ChangeWindowMessageFilter(
|
||||
uint msg,
|
||||
uint flags);
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ChangeWindowMessageFilterEx(
|
||||
IntPtr hwnd,
|
||||
uint msg,
|
||||
uint action,
|
||||
ref CHANGEFILTERSTRUCT filter);
|
||||
|
||||
[LibraryImport("shell32.dll")]
|
||||
private static partial void DragAcceptFiles(
|
||||
IntPtr hwnd,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool accept);
|
||||
|
||||
[LibraryImport("shell32.dll", EntryPoint = "DragQueryFileW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial uint DragQueryFile(IntPtr hDrop, uint iFile, Span<char> lpszFile, uint cch);
|
||||
|
||||
[LibraryImport("shell32.dll", EntryPoint = "DragQueryFileW")]
|
||||
private static partial uint DragQueryFile(IntPtr hDrop, uint iFile, IntPtr lpszFile, uint cch);
|
||||
|
||||
[LibraryImport("shell32.dll")]
|
||||
private static partial void DragFinish(IntPtr hDrop);
|
||||
|
||||
[LibraryImport("ole32.dll")]
|
||||
private static partial int RevokeDragDrop(IntPtr hwnd);
|
||||
|
||||
[LibraryImport("shell32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool IsUserAnAdmin();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Structs
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct DragPoint
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct CHANGEFILTERSTRUCT
|
||||
{
|
||||
public uint cbSize;
|
||||
public uint ExtStatus;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.Exts;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static partial class EnvironmentInterop
|
||||
{
|
||||
private const string LogModule = "Environment";
|
||||
|
||||
/// <summary>
|
||||
/// 读取环境变量并使用 <see cref="StringExtension.Convert{T}"/> 将其转换为指定类型并写入目标引用。
|
||||
/// </summary>
|
||||
/// <param name="key">环境变量名</param>
|
||||
/// <param name="target">需要写入的目标引用 (不存在该环境变量或转换失败时不会写入)</param>
|
||||
/// <param name="detailLog">是否在日志中输出变量值</param>
|
||||
/// <typeparam name="TValue">目标引用的类型</typeparam>
|
||||
/// <returns>是否成功写入目标引用</returns>
|
||||
public static bool ReadVariable<TValue>(string key, ref TValue target, bool detailLog = true)
|
||||
{
|
||||
var envValue = Environment.GetEnvironmentVariable(key);
|
||||
if (envValue is null) return false;
|
||||
var valueLog = detailLog ? $" = {envValue}" : string.Empty;
|
||||
LogWrapper.Debug(LogModule, $"读取到环境变量 {key}{valueLog}");
|
||||
var value = envValue.Convert<TValue>();
|
||||
if (value is null)
|
||||
{
|
||||
LogWrapper.Warn(LogModule, $"环境变量 {key} 类型转换失败");
|
||||
return false;
|
||||
}
|
||||
target = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string? GetSecret(string key, bool readEnv = true, bool readEnvDebugOnly = false)
|
||||
{
|
||||
if (!SecretDictionary.TryGetValue(key, out var value) &&
|
||||
readEnv &&
|
||||
#if !DEBUG
|
||||
!readEnvDebugOnly &&
|
||||
#endif
|
||||
ReadVariable($"PCL_{key}", ref value, false)
|
||||
) SecretDictionary[key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前操作系统名称。
|
||||
/// </summary>
|
||||
/// <returns>返回小写的操作系统名称,如 "windows", "linux", "osx"。</returns>
|
||||
public static string GetCurrentOsName() {
|
||||
if (OperatingSystem.IsWindows())
|
||||
return "windows";
|
||||
if (OperatingSystem.IsLinux())
|
||||
return "linux";
|
||||
return OperatingSystem.IsMacOS()
|
||||
? "osx"
|
||||
: "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Management;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class HardwareInfo
|
||||
{
|
||||
private static readonly object _Lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// 系统 CPU 信息
|
||||
/// </summary>
|
||||
public static string CPUName = "Unknown";
|
||||
|
||||
/// <summary>
|
||||
/// 系统 GPU 信息
|
||||
/// </summary>
|
||||
public static IReadOnlyList<GPUInfo> GPUs { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 已安装物理内存大小,单位 MiB
|
||||
/// </summary>
|
||||
public static long SystemMemorySize = (long)KernelInterop.GetPhysicalMemoryBytes().Total / 1024 / 1024;
|
||||
|
||||
public readonly record struct GPUInfo(string Name, string DriverVersion, long Memory);
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统信息,例如 CPU 与 GPU,并存储到 CPUName 和 GPUs
|
||||
/// </summary>
|
||||
public static void GetHardwareInfo()
|
||||
{
|
||||
// CPU
|
||||
var cpuName = (string?)null;
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(@"root\CIMV2", "SELECT * FROM Win32_Processor");
|
||||
foreach (ManagementObject queryObj in searcher.Get())
|
||||
{
|
||||
cpuName = queryObj["Name"]?.ToString()?.Trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "获取 CPU 信息时出错");
|
||||
}
|
||||
|
||||
// GPU
|
||||
var gpuList = new List<GPUInfo>();
|
||||
try
|
||||
{
|
||||
using var searcher =
|
||||
new ManagementObjectSearcher(@"root\CIMV2", "SELECT * FROM Win32_VideoController");
|
||||
foreach (ManagementObject queryObj in searcher.Get())
|
||||
{
|
||||
var gpuInfo = new GPUInfo
|
||||
{
|
||||
Name = queryObj["Name"]?.ToString() ?? "",
|
||||
DriverVersion = queryObj["DriverVersion"]?.ToString() ?? "",
|
||||
Memory = queryObj["AdapterRAM"] is not null and not DBNull
|
||||
? Convert.ToInt64(queryObj["AdapterRAM"]) / (1024 * 1024)
|
||||
: 0
|
||||
};
|
||||
gpuList.Add(gpuInfo);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Warn(ex, "获取 GPU 信息时出错");
|
||||
}
|
||||
|
||||
lock (_Lock)
|
||||
{
|
||||
if (cpuName is not null)
|
||||
CPUName = cpuName;
|
||||
if (gpuList.Count > 0)
|
||||
GPUs = gpuList;
|
||||
}
|
||||
LogWrapper.Info("已获取系统硬件信息");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static partial class KernelInterop
|
||||
{
|
||||
// ReSharper disable InconsistentNaming, UnusedMember.Local
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", SetLastError = true)]
|
||||
private static partial uint _GetCurrentThreadId();
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "ExitProcess", SetLastError = false)]
|
||||
private static partial void _ExitProcess(uint statusCode);
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetNamedPipeClientProcessId", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool _GetNamedPipeClientProcessId(IntPtr pipeHandle, out uint clientProcessId);
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetLogicalProcessorInformationEx", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool _GetLogicalProcessorInformationEx(
|
||||
LOGICAL_PROCESSOR_RELATIONSHIP relationshipType,
|
||||
IntPtr buffer,
|
||||
ref uint returnLength);
|
||||
|
||||
private const int ERROR_INSUFFICIENT_BUFFER = 122;
|
||||
|
||||
private enum LOGICAL_PROCESSOR_RELATIONSHIP : uint
|
||||
{
|
||||
RelationProcessorCore = 0,
|
||||
RelationNumaNode = 1,
|
||||
RelationCache = 2,
|
||||
RelationProcessorPackage = 3,
|
||||
RelationGroup = 4,
|
||||
RelationAll = 0xffff
|
||||
}
|
||||
|
||||
private static MEMORYSTATUSEX CreateStatus() => new() { dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>() };
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct MEMORYSTATUSEX
|
||||
{
|
||||
public uint dwLength;
|
||||
public uint dwMemoryLoad;
|
||||
public ulong ullTotalPhys;
|
||||
public ulong ullAvailPhys;
|
||||
public ulong ullTotalPageFile;
|
||||
public ulong ullAvailPageFile;
|
||||
public ulong ullTotalVirtual;
|
||||
public ulong ullAvailVirtual;
|
||||
public ulong ullAvailExtendedVirtual;
|
||||
}
|
||||
|
||||
private const int ERROR_ACCESS_DENIED = 5;
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "AllocConsole")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool _AllocConsole();
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "FreeConsole")]
|
||||
private static partial void _FreeConsole();
|
||||
|
||||
[LibraryImport("kernel32.dll", EntryPoint = "GetConsoleWindow")]
|
||||
private static partial nint _GetConsoleWindow();
|
||||
|
||||
// ReSharper restore InconsistentNaming, UnusedMember.Local
|
||||
|
||||
private static void _ThrowLastWin32Error(int? errorCode = null) => throw new Win32Exception(errorCode ?? Marshal.GetLastWin32Error());
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前线程的 Win32 Thread ID。若无特殊情况请用 <see cref="Thread.ManagedThreadId"/> 而不是这个方法。
|
||||
/// </summary>
|
||||
public static uint CurrentNativeThreadId => _GetCurrentThreadId();
|
||||
|
||||
/// <summary>
|
||||
/// 直接结束当前进程。若无特殊情况请使用 <see cref="PCL.Core.App.IoC.Lifecycle.Shutdown"/>
|
||||
/// </summary>
|
||||
/// <param name="statusCode">退出状态码 (返回值)</param>
|
||||
public static void ExitProcess(int statusCode = 0) => _ExitProcess((uint)statusCode);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定命名管道当前连接的客户端进程 ID
|
||||
/// </summary>
|
||||
/// <param name="pipeHandle">命名管道句柄</param>
|
||||
/// <returns>获取到的进程 ID</returns>
|
||||
public static uint GetNamedPipeClientProcessId(IntPtr pipeHandle)
|
||||
{
|
||||
if (!_GetNamedPipeClientProcessId(pipeHandle, out var clientProcessId)) _ThrowLastWin32Error();
|
||||
return clientProcessId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取仅包含性能核(P-core)的逻辑处理器数量。
|
||||
/// 在不支持 EfficiencyClass(旧 OS 或非混合架构)时,会退回到 Environment.ProcessorCount。
|
||||
/// </summary>
|
||||
public static int GetPerformanceLogicalProcessorCount()
|
||||
{
|
||||
var cores = QueryProcessorCoreRelationships();
|
||||
if (cores.Count == 0)
|
||||
{
|
||||
// 不支持 EfficiencyClass
|
||||
return Environment.ProcessorCount;
|
||||
}
|
||||
|
||||
// 原理:性能核的 EfficiencyClass 一定比能效核大
|
||||
var maxEff = cores.Max(c => c.EfficiencyClass);
|
||||
|
||||
// 统计所有效率等级为 maxEff 的核心的掩码位数
|
||||
return cores
|
||||
.Where(c => c.EfficiencyClass == maxEff)
|
||||
.Sum(c => CountSetBits(c.Mask));
|
||||
|
||||
static int CountSetBits(ulong v)
|
||||
{
|
||||
var cnt = 0;
|
||||
while (v != 0)
|
||||
{
|
||||
cnt += (int)(v & 1);
|
||||
v >>= 1;
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仅承载 EfficiencyClass 和 Mask 的简单 CPU 核心信息
|
||||
/// </summary>
|
||||
public sealed record ProcessorCore(byte EfficiencyClass, ulong Mask);
|
||||
|
||||
/// <summary>
|
||||
/// 枚举 RelationProcessorCore 返回的所有物理核心关系信息
|
||||
/// </summary>
|
||||
// Partly generated by o4-mini-high (20250709)
|
||||
public static List<ProcessorCore> QueryProcessorCoreRelationships()
|
||||
{
|
||||
uint returnedLength = 0;
|
||||
|
||||
// 第一次调用仅为了获取所需缓冲区大小
|
||||
if (!_GetLogicalProcessorInformationEx(
|
||||
LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore,
|
||||
IntPtr.Zero,
|
||||
ref returnedLength)
|
||||
&& Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
var list = new List<ProcessorCore>();
|
||||
var buffer = Marshal.AllocHGlobal((int)returnedLength);
|
||||
try
|
||||
{
|
||||
if (!_GetLogicalProcessorInformationEx(
|
||||
LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore,
|
||||
buffer,
|
||||
ref returnedLength))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
var ptr = buffer;
|
||||
var end = IntPtr.Add(buffer, (int)returnedLength);
|
||||
|
||||
// SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX 头部:Relationship (4 字节) + Size (4 字节)
|
||||
const int headerSize = sizeof(uint) + sizeof(uint);
|
||||
// GROUP_AFFINITY 大小 = KAFFINITY (平台指针大小) + WORD Group + WORD[3] Reserved
|
||||
var groupAffinitySize = IntPtr.Size + 8;
|
||||
|
||||
while (ptr.ToInt64() < end.ToInt64())
|
||||
{
|
||||
var relationship = (uint)Marshal.ReadInt32(ptr);
|
||||
var size = (uint)Marshal.ReadInt32(ptr, sizeof(uint));
|
||||
|
||||
if (relationship == (uint)LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore)
|
||||
{
|
||||
// PROCESSOR_RELATIONSHIP 结构:
|
||||
// Flags BYTE @ offset 8
|
||||
// EfficiencyClass BYTE @ offset 9
|
||||
// Reserved[20] BYTE[20]
|
||||
// GroupCount WORD @ offset 30
|
||||
// GroupMask[ANYSIZE] GROUP_AFFINITY 从 offset 32 开始
|
||||
|
||||
var efficiencyClass = Marshal.ReadByte(ptr, headerSize + 1);
|
||||
var groupCount = (ushort)Marshal.ReadInt16(ptr, headerSize + 2 + 20);
|
||||
var maskBase = IntPtr.Add(ptr, headerSize + 2 + 20 + sizeof(ushort));
|
||||
|
||||
for (var i = 0; i < groupCount; i++)
|
||||
{
|
||||
var affinityPtr = IntPtr.Add(maskBase, i * groupAffinitySize);
|
||||
// 只读取 Mask 部分,统计位数
|
||||
var mask = (IntPtr.Size == 8 ? (ulong)Marshal.ReadInt64(affinityPtr) : (uint)Marshal.ReadInt32(affinityPtr));
|
||||
list.Add(new ProcessorCore(efficiencyClass, mask));
|
||||
}
|
||||
}
|
||||
|
||||
// 移动到下一个记录
|
||||
ptr = IntPtr.Add(ptr, (int)size);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统可用物理内存 (<c>ullAvailPhys</c>) 的字节数
|
||||
/// </summary>
|
||||
public static ulong GetAvailablePhysicalMemoryBytes()
|
||||
{
|
||||
var status = CreateStatus();
|
||||
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
|
||||
return status.ullAvailPhys;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统可用物理内存 (<c>ullAvailPhys</c>) 和总物理内存 (<c>ullTotalPhys</c>) 的字节数
|
||||
/// </summary>
|
||||
public static (ulong Total, ulong Available) GetPhysicalMemoryBytes()
|
||||
{
|
||||
var status = CreateStatus();
|
||||
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
|
||||
return (status.ullTotalPhys, status.ullAvailPhys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取以百分比表示的系统内存占用 (范围 0.0 ~ 100.0)
|
||||
/// </summary>
|
||||
public static double GetMemoryLoadPercent()
|
||||
{
|
||||
var status = CreateStatus();
|
||||
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
|
||||
return status.dwMemoryLoad;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为当前进程新建终端窗口。<br/>
|
||||
/// 若进程已拥有终端窗口,该方法将无任何作用。若有需要,可在调用前使用
|
||||
/// <see cref="GetConsoleWindow"/> 来确认进程是否存在关联的终端窗口。
|
||||
/// </summary>
|
||||
public static void AllocateConsole()
|
||||
{
|
||||
if (_AllocConsole()) return;
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
if (lastError != ERROR_ACCESS_DENIED) _ThrowLastWin32Error(lastError);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前进程的终端窗口。<br/>
|
||||
/// 若进程不存在关联的终端窗口,该方法将无任何作用。
|
||||
/// </summary>
|
||||
public static void FreeConsole() => _FreeConsole();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前进程关联的终端窗口句柄。
|
||||
/// </summary>
|
||||
/// <returns>代表终端窗口的 HWND,若当前进程无关联的终端窗口,则该值为 <see cref="nint.Zero"/></returns>
|
||||
public static nint GetConsoleWindow() => _GetConsoleWindow();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class NetworkUtils
|
||||
{
|
||||
private static readonly IPAddress[] _LocalIpAddresses = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(x => x is { OperationalStatus: OperationalStatus.Up })
|
||||
.SelectMany(x => x.GetIPProperties().UnicastAddresses)
|
||||
.Where(ua => ua.Address is { AddressFamily: AddressFamily.InterNetwork or AddressFamily.InterNetworkV6 } &&
|
||||
!ua.Address.Equals(IPAddress.Any) &&
|
||||
!ua.Address.Equals(IPAddress.IPv6Any) &&
|
||||
!ua.Address.Equals(IPAddress.Loopback) &&
|
||||
!ua.Address.Equals(IPAddress.IPv6Loopback))
|
||||
.Select(ua => ua.Address)
|
||||
.ToArray();
|
||||
|
||||
public static IPAddress[] GetAllLocalAddress() => _LocalIpAddresses;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static partial class NtInterop
|
||||
{
|
||||
[LibraryImport("ntdll.dll")]
|
||||
private static partial void RtlGetNtVersionNumbers(
|
||||
out int major,
|
||||
out int minor,
|
||||
out int build);
|
||||
|
||||
private static void _ThrowLastWin32Error(int? errorCode = null) => throw new Win32Exception(errorCode ?? Marshal.GetLastWin32Error());
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the kernel version number of the current operating system (unaffected by compatibility settings)
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Version"/> instance, used to represent the current operating system kernel version number.</returns>
|
||||
public static Version GetCurrentOsVersion()
|
||||
{
|
||||
RtlGetNtVersionNumbers(out var major, out var minor, out var build);
|
||||
build &= 0xFFFF;
|
||||
return new Version(major, minor, build);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Management;
|
||||
using System.Security;
|
||||
using System.Security.Principal;
|
||||
using Microsoft.Win32;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public class ProcessInterop {
|
||||
/// <summary>
|
||||
/// 检查当前程序是否以管理员权限运行。
|
||||
/// </summary>
|
||||
/// <returns>如果当前用户具有管理员权限,则返回 true;否则返回 false。</returns>
|
||||
public static bool IsAdmin() =>
|
||||
new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定进程 ID 的命令行参数。
|
||||
/// </summary>
|
||||
/// <param name="processId">进程 ID</param>
|
||||
/// <returns>命令行参数文本</returns>
|
||||
public static string? GetCommandLine(int processId) {
|
||||
var query = $"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {processId}";
|
||||
using var searcher = new ManagementObjectSearcher(query);
|
||||
return searcher.Get().GetEnumerator().Current["CommandLine"].ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从本地可执行文件启动新的进程。
|
||||
/// </summary>
|
||||
/// <param name="path">可执行文件路径</param>
|
||||
/// <param name="arguments">程序参数</param>
|
||||
/// <param name="runAsAdmin">指定是否以管理员身份启动该进程</param>
|
||||
/// <returns>新的进程实例</returns>
|
||||
public static Process? Start(string path, string? arguments = null, bool runAsAdmin = false) {
|
||||
var psi = new ProcessStartInfo(path);
|
||||
if (arguments is not null) psi.Arguments = arguments;
|
||||
if (runAsAdmin)
|
||||
{
|
||||
psi.UseShellExecute = true;
|
||||
psi.Verb = "runas";
|
||||
}
|
||||
if (Directory.Exists(path))
|
||||
psi.UseShellExecute = true;
|
||||
|
||||
return Process.Start(psi);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定进程的可执行文件路径
|
||||
/// </summary>
|
||||
/// <param name="process">进程实例</param>
|
||||
/// <returns>可执行文件路径,若无法获取则为 <c>null</c></returns>
|
||||
public static string? GetExecutablePath(Process process) {
|
||||
try {
|
||||
var path = process.MainModule?.FileName;
|
||||
return (path is null) ? null : Path.GetFullPath(path);
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从本地可执行文件以管理员身份启动新的进程。<see cref="Start"/> 的套壳。
|
||||
/// </summary>
|
||||
/// <param name="path">可执行文件路径</param>
|
||||
/// <param name="arguments">程序参数</param>
|
||||
/// <returns>新的进程实例</returns>
|
||||
public static Process? StartAsAdmin(string path, string? arguments = null) => Start(path, arguments, true);
|
||||
|
||||
/// <summary>
|
||||
/// 结束指定进程。
|
||||
/// </summary>
|
||||
/// <param name="process">要结束的进程实例</param>
|
||||
/// <param name="timeout">等待进程退出超时,以毫秒为单位,-1 表示无限制</param>
|
||||
/// <param name="force">指定是否强制结束,若为 <c>true</c> 将通过带 <c>/F</c> 参数的 <c>TASKKILL.EXE</c> 结束进程</param>
|
||||
/// <returns>进程返回值,若等待超时将返回 <see cref="int.MinValue"/></returns>
|
||||
public static int Kill(Process process, int timeout = 3000, bool force = false) {
|
||||
if (force) Process.Start(new ProcessStartInfo("TASKKILL.EXE", $"/PID {process.Id} /F") { UseShellExecute = false });
|
||||
else process.Kill();
|
||||
if (timeout == -1) process.WaitForExit();
|
||||
else if (timeout != 0) process.WaitForExit(timeout);
|
||||
return process.HasExited ? process.ExitCode : int.MinValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将特定程序设置为使用高性能显卡启动。
|
||||
/// </summary>
|
||||
/// <param name="executable">可执行文件路径。</param>
|
||||
/// <param name="wantHighPerformance">是否使用高性能显卡,默认为 true。</param>
|
||||
/// <exception cref="ArgumentException">当可执行文件路径无效时抛出</exception>
|
||||
/// <exception cref="UnauthorizedAccessException">当没有足够权限访问注册表时抛出</exception>
|
||||
/// <exception cref="SecurityException">当安全策略不允许访问注册表时抛出</exception>
|
||||
/// <exception cref="InvalidOperationException">当注册表操作失败时抛出</exception>
|
||||
public static void SetGpuPreference(string executable, bool wantHighPerformance = true) {
|
||||
// 参数验证
|
||||
if (string.IsNullOrWhiteSpace(executable)) {
|
||||
throw new ArgumentException("可执行文件路径不能为空或仅包含空白字符", nameof(executable));
|
||||
}
|
||||
|
||||
// 验证文件路径格式
|
||||
try {
|
||||
var fullPath = Path.GetFullPath(executable);
|
||||
if (!File.Exists(fullPath)) {
|
||||
LogWrapper.Warn("System", $"指定的可执行文件不存在: {executable}");
|
||||
}
|
||||
} catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) {
|
||||
throw new ArgumentException($"无效的可执行文件路径: {executable}", nameof(executable), ex);
|
||||
}
|
||||
|
||||
const string gpuPreferenceRegKey = @"Software\Microsoft\DirectX\UserGpuPreferences";
|
||||
const string gpuPreferenceRegValueHigh = "GpuPreference=2;";
|
||||
const string gpuPreferenceRegValueDefault = "GpuPreference=0;";
|
||||
|
||||
try {
|
||||
var isCurrentHighPerformance = _GetCurrentGpuPreference(executable, gpuPreferenceRegKey, gpuPreferenceRegValueHigh);
|
||||
|
||||
LogWrapper.Info("System", $"当前程序 ({executable}) 的显卡设置为高性能: {isCurrentHighPerformance}");
|
||||
|
||||
// 如果当前设置已经是期望的设置,则无需修改
|
||||
if (isCurrentHighPerformance == wantHighPerformance) {
|
||||
LogWrapper.Info("System", $"程序 ({executable}) 的显卡设置已经是期望的设置,无需修改");
|
||||
return;
|
||||
}
|
||||
|
||||
// 写入新设置
|
||||
_SetGpuPreferenceValue(executable, wantHighPerformance, gpuPreferenceRegKey,
|
||||
gpuPreferenceRegValueHigh, gpuPreferenceRegValueDefault);
|
||||
} catch (UnauthorizedAccessException ex) {
|
||||
var errorMsg = "没有足够的权限访问注册表。请以管理员身份运行程序或检查用户权限设置。";
|
||||
LogWrapper.Error(ex, "System", errorMsg);
|
||||
throw new UnauthorizedAccessException(errorMsg, ex);
|
||||
} catch (SecurityException ex) {
|
||||
var errorMsg = "安全策略不允许访问注册表。请联系系统管理员检查安全设置。";
|
||||
LogWrapper.Error(ex, "System", errorMsg);
|
||||
throw new SecurityException(errorMsg, ex);
|
||||
} catch (Exception ex) {
|
||||
var errorMsg = $"设置 GPU 偏好时发生未预期的错误: {ex.Message}";
|
||||
LogWrapper.Error(ex, "System", errorMsg);
|
||||
throw new InvalidOperationException(errorMsg, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前程序的GPU偏好设置
|
||||
/// </summary>
|
||||
private static bool _GetCurrentGpuPreference(string executable, string regKey, string highPerfValue) {
|
||||
try {
|
||||
using var readOnlyKey = Registry.CurrentUser.OpenSubKey(regKey, false);
|
||||
if (readOnlyKey is null) {
|
||||
LogWrapper.Info("System", "GPU 偏好注册表键不存在,将在需要时创建");
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentValue = readOnlyKey.GetValue(executable)?.ToString();
|
||||
return string.Equals(currentValue, highPerfValue, StringComparison.OrdinalIgnoreCase);
|
||||
} catch (Exception ex) {
|
||||
LogWrapper.Warn(ex, "System", $"读取当前 GPU 偏好设置时出现错误: {ex.Message}");
|
||||
return false; // 假设当前不是高性能模式
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置GPU偏好值到注册表
|
||||
/// </summary>
|
||||
private static bool _SetGpuPreferenceValue(string executable, bool wantHighPerformance,
|
||||
string regKey, string highPerfValue, string defaultValue) {
|
||||
RegistryKey? writeKey = null;
|
||||
try {
|
||||
// 尝试打开现有键进行写入
|
||||
writeKey = Registry.CurrentUser.OpenSubKey(regKey, true);
|
||||
|
||||
// 如果键不存在,创建它
|
||||
if (writeKey is null) {
|
||||
LogWrapper.Info("System", "创建 GPU 偏好注册表键");
|
||||
writeKey = Registry.CurrentUser.CreateSubKey(regKey);
|
||||
|
||||
if (writeKey is null) {
|
||||
throw new InvalidOperationException($"无法创建注册表键: {regKey}");
|
||||
}
|
||||
}
|
||||
|
||||
var valueToSet = wantHighPerformance ? highPerfValue : defaultValue;
|
||||
writeKey.SetValue(executable, valueToSet, RegistryValueKind.String);
|
||||
|
||||
LogWrapper.Info("System", $"成功设置程序 ({executable}) 的GPU偏好: {(wantHighPerformance ? "高性能" : "默认")}");
|
||||
return true;
|
||||
} catch (UnauthorizedAccessException) {
|
||||
// 重新抛出,让上层处理
|
||||
throw;
|
||||
} catch (SecurityException) {
|
||||
// 重新抛出,让上层处理
|
||||
throw;
|
||||
} catch (Exception ex) {
|
||||
var errorMsg = $"写入注册表时发生错误: {ex.Message}";
|
||||
LogWrapper.Error(ex, "System", errorMsg);
|
||||
throw new InvalidOperationException(errorMsg, ex);
|
||||
} finally {
|
||||
writeKey?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ProcessExitCode {
|
||||
/// <summary>
|
||||
/// Indicates that the process completed successfully.
|
||||
/// </summary>
|
||||
TaskDone = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a general failure of the process.
|
||||
/// </summary>
|
||||
Failed = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the process was canceled.
|
||||
/// </summary>
|
||||
Canceled = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the process failed due to insufficient permissions.
|
||||
/// </summary>
|
||||
AccessDenied = 5
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public partial class RegistryChangeMonitor : IDisposable
|
||||
{
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
private const int REG_NOTIFY_CHANGE_LAST_SET = 0x00000004;
|
||||
private const int KEY_NOTIFY = 0x0010;
|
||||
private const int KEY_QUERY_VALUE = 0x0001;
|
||||
private const int KEY_READ = (KEY_QUERY_VALUE | KEY_NOTIFY);
|
||||
private const UIntPtr HKEY_CURRENT_USER = 0x80000001;
|
||||
|
||||
[LibraryImport("advapi32.dll", EntryPoint = "RegOpenKeyExW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial int _RegOpenKeyEx(UIntPtr hKey, string subKey, uint options, int samDesired, out IntPtr phkResult);
|
||||
|
||||
[LibraryImport("advapi32.dll", EntryPoint = "RegNotifyChangeKeyValue", SetLastError = true)]
|
||||
private static partial int _RegNotifyChangeKeyValue(IntPtr hKey, [MarshalAs(UnmanagedType.Bool)] bool bWatchSubtree, int dwNotifyFilter, IntPtr hEvent, [MarshalAs(UnmanagedType.Bool)] bool fAsynchronous);
|
||||
|
||||
[LibraryImport("advapi32.dll", EntryPoint = "RegCloseKey", SetLastError = true)]
|
||||
private static partial int _RegCloseKey(IntPtr hKey);
|
||||
|
||||
// ReSharper restore InconsistentNaming
|
||||
|
||||
private readonly IntPtr _hKey;
|
||||
private readonly ManualResetEvent _stopEvent = new(false);
|
||||
private readonly ManualResetEvent _registryEvent = new(false);
|
||||
private readonly Thread _monitorThread;
|
||||
|
||||
public event EventHandler? Changed;
|
||||
|
||||
public RegistryChangeMonitor(string keyPath)
|
||||
{
|
||||
// Open registry key with proper access rights
|
||||
var result = _RegOpenKeyEx(HKEY_CURRENT_USER, keyPath, 0, KEY_READ, out _hKey);
|
||||
if (result != 0) throw new Win32Exception(result);
|
||||
|
||||
// Start monitoring thread
|
||||
_monitorThread = new Thread(_MonitorThread) { IsBackground = true };
|
||||
_monitorThread.Start();
|
||||
}
|
||||
|
||||
private void _MonitorThread()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Initial registration
|
||||
_RegisterForNotification();
|
||||
|
||||
while (!_stopEvent.WaitOne(0))
|
||||
{
|
||||
// Wait for either registry change or stop signal
|
||||
var index = WaitHandle.WaitAny(
|
||||
[_registryEvent, _stopEvent],
|
||||
TimeSpan.FromSeconds(1)); // Timeout to check for stop periodically
|
||||
|
||||
if (index == 1) break; // Stop requested
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
_registryEvent.Reset();
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
_RegisterForNotification(); // Re-register for next change
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_registryEvent.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void _RegisterForNotification()
|
||||
{
|
||||
var result = _RegNotifyChangeKeyValue(
|
||||
_hKey,
|
||||
true,
|
||||
REG_NOTIFY_CHANGE_LAST_SET,
|
||||
_registryEvent.SafeWaitHandle.DangerousGetHandle(),
|
||||
true); // Must be asynchronous to allow graceful shutdown
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
// Handle error - key might have been deleted
|
||||
_stopEvent.Set();
|
||||
throw new Win32Exception(result);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_stopEvent.Set();
|
||||
|
||||
// Give thread a chance to exit gracefully
|
||||
if (_monitorThread is {IsAlive: true})
|
||||
_monitorThread.Join(1000);
|
||||
|
||||
if (_hKey != IntPtr.Zero)
|
||||
_ = _RegCloseKey(_hKey);
|
||||
|
||||
_stopEvent.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class SystemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否为 32 位系统。
|
||||
/// </summary>
|
||||
public static readonly bool Is32BitSystem = !Environment.Is64BitOperatingSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 ARM64 架构。
|
||||
/// </summary>
|
||||
public static readonly bool IsArm64System = RuntimeInformation.OSArchitecture == Architecture.Arm64;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用 GBK 编码。
|
||||
/// </summary>
|
||||
public static readonly bool IsGBKEncoding = Encoding.Default.CodePage == 936;
|
||||
|
||||
/// <summary>
|
||||
/// 系统信息描述,例如 Microsoft Windows 11 专业工作站版 10.0.22635.0
|
||||
/// </summary>
|
||||
public static readonly string OSInfo = $"{RuntimeInformation.OSDescription} {Environment.OSVersion.Version}";
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static class SystemPaths {
|
||||
/// <summary>
|
||||
/// 系统盘符(含冒号和反斜杠),例如 "C:\"。
|
||||
/// </summary>
|
||||
public static string DriveLetter { get; } = Path.GetPathRoot(Environment.SystemDirectory)!;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using Microsoft.Win32;
|
||||
using PCL.Core.Logging;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public class SystemTheme {
|
||||
private const string ThemeRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
|
||||
private const string AppsUseLightThemeKey = "AppsUseLightTheme";
|
||||
|
||||
/// <summary>
|
||||
/// 检查系统是否处于深色模式。
|
||||
/// </summary>
|
||||
/// <returns>如果系统使用深色模式,则返回 true;否则返回 false(包括注册表不可访问的情况)。</returns>
|
||||
public static bool IsSystemInDarkMode() {
|
||||
try {
|
||||
using var registryKey = Registry.CurrentUser.OpenSubKey(ThemeRegistryPath);
|
||||
if (registryKey is null) {
|
||||
LogWrapper.Warn($"注册表键 {ThemeRegistryPath} 不存在");
|
||||
return false;
|
||||
}
|
||||
|
||||
var value = registryKey.GetValue(AppsUseLightThemeKey) as int?;
|
||||
return value == 0; // 0 表示深色模式(AppsUseLightTheme = false)
|
||||
} catch (Exception ex) when (ex is SecurityException or IOException) {
|
||||
LogWrapper.Warn(ex, $"无法访问注册表键 {ThemeRegistryPath}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace PCL.Core.Utils.OS;
|
||||
|
||||
public static partial class WindowInterop
|
||||
{
|
||||
// ReSharper disable InconsistentNaming UnusedMember.Local
|
||||
|
||||
// DWM 外边缘结构定义
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MARGINS { public int leftWidth, rightWidth, topHeight, bottomHeight; }
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static partial int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static partial int DwmIsCompositionEnabled([MarshalAs(UnmanagedType.Bool)] out bool pfEnabled);
|
||||
|
||||
// Win32 矩形结构定义
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct RECT { public int left; public int top; public int right; public int bottom; }
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||
|
||||
// MONITOR_DPI_TYPE enum
|
||||
private enum MONITOR_DPI_TYPE {
|
||||
MDT_EFFECTIVE_DPI = 0,
|
||||
MDT_ANGULAR_DPI = 1,
|
||||
MDT_RAW_DPI = 2,
|
||||
MDT_DEFAULT = MDT_EFFECTIVE_DPI
|
||||
}
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
private static partial IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags);
|
||||
|
||||
// Get the primary monitor handle
|
||||
private const int MONITOR_DEFAULTTOPRIMARY = 1;
|
||||
|
||||
[LibraryImport("shcore.dll", EntryPoint = "GetDpiForMonitor")]
|
||||
private static partial int GetDpiForMonitor(
|
||||
IntPtr hMonitor,
|
||||
MONITOR_DPI_TYPE dpiType,
|
||||
out uint dpiX,
|
||||
out uint dpiY
|
||||
);
|
||||
|
||||
// ReSharper enable InconsistentNaming UnusedMember.Local
|
||||
|
||||
/// <summary>
|
||||
/// 检测 DWM 组合是否可用
|
||||
/// </summary>
|
||||
public static bool IsCompositionEnabled()
|
||||
{
|
||||
var hResult = DwmIsCompositionEnabled(out var enabled);
|
||||
return hResult != 0 ? throw new Win32Exception(hResult, "Failed to check DWM status") : enabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 DWM 窗口边框到客户区域的扩展大小
|
||||
/// </summary>
|
||||
public static void ExtendFrameIntoClientArea(
|
||||
IntPtr hWnd, int marginLeft, int marginTop, int marginRight, int marginBottom)
|
||||
{
|
||||
MARGINS margins = new()
|
||||
{
|
||||
leftWidth = marginLeft,
|
||||
rightWidth = marginRight,
|
||||
topHeight = marginTop,
|
||||
bottomHeight = marginBottom
|
||||
};
|
||||
if (!IsCompositionEnabled()) return;
|
||||
var hResult = DwmExtendFrameIntoClientArea(hWnd, ref margins);
|
||||
if (hResult != 0) throw new Win32Exception(hResult, "Failed to extend frame into client area");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="ExtendFrameIntoClientArea(IntPtr, int, int, int, int)"/>
|
||||
/// </summary>
|
||||
public static void ExtendFrameIntoClientArea(IntPtr hWnd, int margin)
|
||||
=> ExtendFrameIntoClientArea(hWnd, margin, margin, margin, margin);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Win32 窗口矩形定义
|
||||
/// </summary>
|
||||
public static (int Left, int Top, int Right, int Bottom) GetWindowRectangle(IntPtr hWnd)
|
||||
{
|
||||
var hResult = GetWindowRect(hWnd, out var rect);
|
||||
return hResult ? (rect.left, rect.top, rect.right, rect.bottom)
|
||||
: throw new Win32Exception("Failed to get window rectangle");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Win32 窗口位置与大小
|
||||
/// </summary>
|
||||
public static (int X, int Y, int Width, int Height) ToWindowBounds(
|
||||
this (int Left, int Top, int Right, int Bottom) rect)
|
||||
{
|
||||
var (l, t, r, b) = rect;
|
||||
var x = l;
|
||||
var y = t;
|
||||
var width = r - l;
|
||||
var height = b - t;
|
||||
return (x, y, width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定屏幕的系统 DPI
|
||||
/// </summary>
|
||||
/// <param name="hWnd">位于指定屏幕上的任意窗口句柄,默认指定主屏</param>
|
||||
public static int GetSystemDpi(IntPtr hWnd = 0) {
|
||||
// Get the monitor handle
|
||||
var hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTOPRIMARY);
|
||||
// 0 is S_OK
|
||||
var hr = GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
|
||||
if (hr == 0)
|
||||
return (int)dpiX;
|
||||
// fallback to default DPI (96)
|
||||
return 96;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user