ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Unity调用Windows API实现透明可点击悬浮窗:P/Invoke实战指南

Unity调用Windows API实现透明可点击悬浮窗:P/Invoke实战指南 1. 项目概述与核心价值最近在折腾Unity桌面美化想做一个既好看又实用的悬浮小部件比如一个透明的系统监控面板或者一个快捷启动栏让它能一直“浮”在所有窗口之上并且还能点击交互。这听起来像是需要深入到Windows系统底层才能实现的功能对吧没错要实现这个效果单纯靠Unity的UI系统是远远不够的我们必须借助Windows的“原生力量”——也就是user32.dll这个系统库。这个项目本质上就是一次C#与Windows API的深度握手通过P/Invoke技术调用user32.dll中的函数来实现对窗口行为的终极控制从而打造出我们想要的透明、可点击、始终置顶的悬浮小部件。为什么不用现成的插件一方面很多插件功能臃肿不一定完全符合我们的定制化需求另一方面自己动手实现一遍能让你对Windows窗口机制、消息循环以及Unity与原生平台的交互有更深刻的理解。这对于想在Windows平台做高级UI定制或者开发桌面工具的开发者来说是一项非常宝贵的实战技能。无论你是想做一个个性化的桌面助手还是为你的Unity应用增加一个炫酷的悬浮控制台这套方法都能为你提供清晰的路径。2. 核心原理Windows API与P/Invoke要理解我们如何实现这个悬浮小部件首先得搞清楚几个核心概念Windows的窗口、消息机制以及C#如何与这些原生代码对话。2.1 Windows窗口与消息机制在Windows系统中屏幕上你能看到的一切从桌面图标到应用程序窗口本质上都是一个“窗口”HWND。每个窗口都有大量的属性比如位置、大小、样式如是否有边框、是否置顶、是否透明等。系统通过向窗口发送“消息”如鼠标点击、键盘输入、绘制指令来驱动其行为窗口则通过“窗口过程”函数来处理这些消息。我们想要实现的效果可以拆解为几个具体的窗口属性修改始终置顶让我们的Unity窗口永远显示在其他窗口之上。点击穿透让窗口在某些区域比如透明背景不拦截鼠标点击使点击能落到下层的窗口或桌面上。视觉透明让窗口背景透明只显示我们设计的UI内容。2.2 使用P/Invoke调用user32.dllUnity使用C#开发而user32.dll是Windows的原生C/C动态链接库。要让C#代码调用其中的函数就需要用到“平台调用”Platform Invoke简称P/Invoke技术。P/Invoke就像一位翻译官它知道如何将C#的数据类型和方法签名转换成原生代码能理解的形式并负责在托管代码C#和非托管代码C/C DLL之间传递参数和返回值。在C#中我们通过[DllImport]特性来声明一个外部方法。例如设置窗口置顶的函数SetWindowPos是这样声明的[DllImport(user32.dll)] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);这里IntPtr hWnd代表窗口句柄它是Windows系统中标识一个窗口的唯一ID。uint uFlags是一组标志位用来指定如何设置窗口位置和大小。注意P/Invoke调用是相对底层的操作参数类型和调用约定必须与原生函数严格匹配否则可能导致程序崩溃或行为异常。网上能找到很多现成的声明但使用时最好对照微软的官方文档进行核对。2.3 Unity窗口的句柄获取在Unity中我们的应用程序本身就是一个窗口。要修改它的属性首先得拿到这个窗口的句柄HWND。在Windows上每个进程都有唯一的进程ID每个窗口也有唯一的句柄。我们可以通过Unity提供的System.Diagnostics.Process类获取当前进程进而得到主窗口句柄。一个常见的获取窗口句柄的方法如下using System.Diagnostics; using System.Runtime.InteropServices; public class WindowHandleHelper { [DllImport(user32.dll)] private static extern IntPtr GetActiveWindow(); // 方法1尝试获取当前活动窗口在编辑器模式下可能不准确 public static IntPtr GetWindowHandle() { return GetActiveWindow(); } // 方法2通过进程获取主窗口句柄更可靠尤其对于打包后的应用 public static IntPtr GetWindowHandleByProcess() { Process process Process.GetCurrentProcess(); return process.MainWindowHandle; } }在实际打包后的应用中MainWindowHandle通常是更可靠的选择。而在Unity编辑器内运行时获取的句柄可能是编辑器的窗口而非游戏视图窗口这需要在测试时注意区分。3. 实战步骤打造透明可点击悬浮窗掌握了原理我们就可以开始动手了。我将整个过程分为四个核心环节创建基础UI、实现窗口置顶、设置窗口透明、配置点击穿透。3.1 创建Unity基础UI与项目设置首先我们在Unity中创建一个简单的UI作为悬浮小部件的内容。新建一个Unity项目选择2D或3D模板均可。在场景中创建一个Canvas。将Canvas的Render Mode设置为Screen Space - Overlay。这样UI将渲染在最顶层不受3D摄像机影响。在Canvas下创建一个Panel作为背景再添加一些Text、Image或Button作为小部件的内容。例如做一个系统监控面板可以显示CPU、内存使用率。为了后续方便管理我们创建一个空的GameObject命名为“FloatingWidgetManager”并将我们编写的C#脚本挂载上去。实操心得在开始编写与系统交互的代码前先把UI布局和逻辑做好。建议使用Anchor和Stretch进行布局这样当窗口大小或屏幕分辨率变化时UI能自适应。另外考虑到悬浮窗可能比较小UI元素的字体和图标不宜过大要确保在小尺寸下的可读性。3.2 实现窗口置顶Top-Most让窗口置顶我们需要调用SetWindowPos函数并将hWndInsertAfter参数设置为HWND_TOPMOST(值为 -1)。我们在脚本中定义必要的常量和函数using UnityEngine; using System; using System.Runtime.InteropServices; public class FloatingWidgetController : MonoBehaviour { private IntPtr _windowHandle; // 引入user32.dll中的函数 [DllImport(user32.dll)] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); // 常用的标志位常量 private const uint SWP_NOSIZE 0x0001; private const uint SWP_NOMOVE 0x0002; private const uint SWP_NOACTIVATE 0x0010; private static readonly IntPtr HWND_TOPMOST new IntPtr(-1); private static readonly IntPtr HWND_NOTOPMOST new IntPtr(-2); void Start() { // 获取当前窗口句柄 _windowHandle GetActiveWindow(); // 或者使用 GetWindowHandleByProcess if (_windowHandle ! IntPtr.Zero) { SetTopMost(true); } } // 封装设置置顶的函数 public void SetTopMost(bool isTopMost) { if (_windowHandle IntPtr.Zero) return; IntPtr insertAfter isTopMost ? HWND_TOPMOST : HWND_NOTOPMOST; // SWP_NOSIZE | SWP_NOMOVE 表示保持当前窗口大小和位置不变 SetWindowPos(_windowHandle, insertAfter, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE); } [DllImport(user32.dll)] private static extern IntPtr GetActiveWindow(); }将脚本挂载到场景中运行游戏打包成exe后运行效果更明显你会发现你的Unity窗口已经悬浮在所有其他窗口之上了即使你点击其他应用它也不会被遮盖。3.3 实现窗口透明与不规则形状仅仅置顶还不够我们想要一个没有标题栏、没有边框、背景透明的“真正”悬浮窗。这需要修改窗口的扩展样式Extended Window Style。3.3.1 关键APISetWindowLong 和 SetLayeredWindowAttributes我们需要用到以下函数GetWindowLong/SetWindowLong用于获取和设置窗口的基本样式GWL_STYLE和扩展样式GWL_EXSTYLE。SetLayeredWindowAttributes专门用于设置分层窗口的属性如透明度Alpha和颜色键透明。首先我们定义所需的常量和函数声明[DllImport(user32.dll)] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport(user32.dll)] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport(user32.dll)] private static extern bool SetLayeredWindowAttributes(IntPtr hWnd, uint crKey, byte bAlpha, uint dwFlags); // 窗口样式索引 private const int GWL_EXSTYLE -20; private const int GWL_STYLE -16; // 扩展窗口样式 private const int WS_EX_LAYERED 0x80000; private const int WS_EX_TRANSPARENT 0x20; private const int WS_EX_TOOLWINDOW 0x80; // 使窗口不显示在任务栏 // 基本窗口样式用于去除边框和标题栏 private const int WS_POPUP unchecked((int)0x80000000); private const int WS_VISIBLE 0x10000000; // SetLayeredWindowAttributes 的标志 private const uint LWA_COLORKEY 0x1; private const uint LWA_ALPHA 0x2;3.3.2 将窗口设置为分层和透明在Start方法中获取句柄后我们添加设置窗口样式的代码void Start() { _windowHandle GetActiveWindow(); if (_windowHandle ! IntPtr.Zero) { MakeWindowTransparent(); SetTopMost(true); } } private void MakeWindowTransparent() { // 1. 获取当前的扩展样式 int extendedStyle GetWindowLong(_windowHandle, GWL_EXSTYLE); // 2. 添加分层窗口和工具窗口样式 // WS_EX_LAYERED 允许设置透明度和颜色键 // WS_EX_TOOLWINDOW 使窗口不在任务栏显示更像一个“小工具” extendedStyle | WS_EX_LAYERED | WS_EX_TOOLWINDOW; SetWindowLong(_windowHandle, GWL_EXSTYLE, extendedStyle); // 3. 设置窗口为完全透明背景Alpha0 // 这里使用LWA_ALPHA并将bAlpha设为0。注意这会使整个窗口客户区透明。 // 但我们的UI元素Canvas是由Unity渲染的它们本身是不透明的。 // 所以我们需要一个更精细的方案让Unity渲染的背景透明并设置颜色键。 // 我们先尝试设置一个颜色键透明。 // 假设我们想让某种特定颜色如纯黑 RGB(0,0,0)透明 uint colorKey 0x000000; // 黑色 SetLayeredWindowAttributes(_windowHandle, colorKey, 0, LWA_COLORKEY); }但是这里有一个关键问题SetLayeredWindowAttributes的颜色键透明是针对整个窗口的包括Unity渲染的内容。如果我们把黑色设为透明键那么UI中所有的黑色像素都会变透明这通常不是我们想要的。3.3.3 正确的透明方案Unity摄像机背景与窗口Alpha更常见的做法是在Unity中设置摄像机背景为完全透明如果你的Canvas是Screen Space - Overlay它不依赖摄像机背景。如果你的Canvas是Screen Space - Camera或使用普通摄像机需要将摄像机的Clear Flags设置为Solid Color并将背景颜色的Alpha值设为0。实际上对于Overlay模式的Canvas它直接绘制到屏幕上不需要处理摄像机背景。透明效果主要靠UI元素自身的Alpha。移除窗口本身的边框和标题栏并设置一个整体的窗口Alpha可选我们可以通过设置基本样式WS_POPUP来移除标准窗口的边框和标题栏。private void RemoveWindowBorder() { // 获取当前基本样式并设置为POPUP样式无边框并保持可见 int style WS_POPUP | WS_VISIBLE; SetWindowLong(_windowHandle, GWL_STYLE, style); }调用SetLayeredWindowAttributes并启用LWA_ALPHA可以设置整个窗口的透明度。但这样会让所有内容包括UI都变半透明。如果你只需要背景透明UI不透明这并非最佳选择。更常用的方法是结合LWA_COLORKEY和精心设计的背景。终极方案让Unity渲染一个透明背景并让窗口对该背景色做颜色键透明。在Canvas下创建一个全屏的RawImage或Image将其颜色设置为一个你几乎不会用到的特定颜色比如RGBA(1, 0, 1, 1)品红色。将这个Image置于UI的最底层。在C#代码中将这个颜色RGB(255, 0, 255)设置为颜色键。这样只有这个特定的品红色背景会变透明而其上层的所有UI元素按钮、文字都会正常显示。重要注意事项修改窗口样式特别是WS_POPUP可能会导致窗口失去默认的关闭、最小化按钮也无法通过拖动标题栏来移动窗口。你需要自己实现窗口拖动功能通过处理鼠标消息这增加了复杂性。对于悬浮小部件通常我们更倾向于保留一个极细的边框或自定义标题栏以提供可拖动区域。直接设为WS_POPUP需谨慎。3.4 实现可点击与点击穿透现在我们的窗口是透明且置顶的但你会发现鼠标点击会完全被这个窗口拦截你无法点击到它后面的任何东西。这显然不是我们想要的“小部件”体验。我们需要实现“点击穿透”即鼠标点击在透明或非交互区域时事件应该传递到下层的窗口。3.4.1 使用 WS_EX_TRANSPARENT 扩展样式有一个简单的方案为窗口添加WS_EX_TRANSPARENT扩展样式。private void MakeWindowClickThrough() { int extendedStyle GetWindowLong(_windowHandle, GWL_EXSTYLE); extendedStyle | WS_EX_TRANSPARENT; // 添加透明样式 SetWindowLong(_windowHandle, GWL_EXSTYLE, extendedStyle); }添加此样式后窗口将变得完全“透明”于鼠标点击。但是请注意这会导致窗口完全无法接收任何鼠标消息包括你希望交互的UI按钮也将无法点击。这通常只适用于完全不需要交互、仅用于显示的悬浮信息板。3.4.2 精细化点击穿透处理WM_NCHITTEST消息对于需要部分区域可交互、部分区域可穿透的小部件我们需要更精细的控制。这需要用到Windows的消息机制。我们可以“劫持”Unity窗口的WM_NCHITTEST消息。当鼠标在窗口上移动或点击时系统会发送此消息来询问“鼠标当前在窗口的哪个部分”。通过处理这个消息我们可以动态地告诉系统“现在鼠标在透明背景上请穿透返回HTTRANSPARENT”“现在鼠标在按钮上请按正常按钮处理返回HTCLIENT”。在C#中处理Windows消息需要设置一个“窗口过程”WndProc回调。这比简单的样式设置复杂得多。基本步骤如下声明必要的常量和委托public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); [DllImport(user32.dll)] private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, WndProcDelegate newProc); [DllImport(user32.dll)] private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); private const int GWLP_WNDPROC -4; private const uint WM_NCHITTEST 0x0084; // WM_NCHITTEST 的返回值 private const int HTTRANSPARENT -1; private const int HTCLIENT 1; // 其他返回值如HTCAPTION标题栏可拖动、HTLEFT左边框等保存旧的回调并设置新的private WndProcDelegate _newWndProc; private IntPtr _oldWndProc; void Start() { _windowHandle GetActiveWindow(); if (_windowHandle ! IntPtr.Zero) { // ... 其他初始化 InstallHook(); } } void OnDestroy() { UninstallHook(); } private void InstallHook() { _newWndProc new WndProcDelegate(WindowProc); _oldWndProc SetWindowLongPtr(_windowHandle, GWLP_WNDPROC, _newWndProc); } private void UninstallHook() { if (_oldWndProc ! IntPtr.Zero) { SetWindowLongPtr(_windowHandle, GWLP_WNDPROC, _oldWndProc); } }实现自定义窗口过程private IntPtr WindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg WM_NCHITTEST) { // 获取鼠标在屏幕上的坐标 int x (short)(lParam.ToInt32() 0xFFFF); int y (short)((lParam.ToInt32() 16) 0xFFFF); // 将屏幕坐标转换为Unity UI的局部坐标 Vector2 mousePos new Vector2(x, y); // 注意屏幕坐标原点在左上角Unity UI坐标原点可能在左下角Canvas的Screen Space - Overlay模式 // 需要进行坐标转换 mousePos.y Screen.height - mousePos.y; // 关键逻辑判断鼠标位置是否在可交互的UI元素上 bool isOverInteractiveUI IsPointerOverInteractiveUI(mousePos); if (isOverInteractiveUI) { // 如果在可交互UI上返回HTCLIENT让窗口正常处理鼠标事件 return (IntPtr)HTCLIENT; } else { // 否则返回HTTRANSPARENT让点击穿透 return (IntPtr)HTTRANSPARENT; } } // 其他消息交给默认窗口过程处理 return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam); } private bool IsPointerOverInteractiveUI(Vector2 screenPos) { // 这里需要实现一个检测逻辑判断给定屏幕坐标是否落在了任何一个可交互的UI元素如Button上。 // 可以使用Unity的 EventSystem.current.RaycastAll 方法。 // 这是一个简化的示例 PointerEventData eventData new PointerEventData(EventSystem.current); eventData.position screenPos; ListRaycastResult results new ListRaycastResult(); EventSystem.current.RaycastAll(eventData, results); foreach (var result in results) { // 检查射线击中的物体是否有可交互组件Button, Toggle, Slider等 if (result.gameObject.GetComponentSelectable() ! null) { return true; } } return false; }这个方案非常强大它允许你精确控制窗口的哪些区域响应鼠标哪些区域穿透。但实现起来也最复杂涉及到Windows消息循环、坐标转换和Unity UI事件系统的交互。踩坑实录在实现IsPointerOverInteractiveUI时要特别注意坐标系的转换。Windows的屏幕坐标原点在左上角Y轴向下为正。而Unity的Screen Space - Overlay模式其Input.mousePosition的原点在左下角Y轴向上为正。必须进行正确的mousePos.y Screen.height - mousePos.y转换否则检测会完全错位。此外RaycastAll在UI元素很多时可能有性能开销可以考虑使用GraphicRaycaster或进行结果缓存优化。4. 性能优化与高级特性一个优秀的悬浮小部件不仅要功能完备还要运行高效、稳定。4.1 降低CPU占用一个始终置顶、可能还需要实时更新内容如系统监控的窗口如果不加优化可能会持续消耗CPU资源。更新频率控制对于实时数据如CPU使用率不要每帧都更新。使用InvokeRepeating或协程配合WaitForSeconds将更新频率降低到每秒1-2次这对显示来说已经足够流畅。void Start() { // 每秒更新一次系统信息 InvokeRepeating(UpdateSystemInfo, 0f, 1.0f); } void UpdateSystemInfo() { // 获取并更新CPU、内存信息到UI }避免不必要的UI重绘确保只有数据变化时才去更新UI Text或Image的显示属性。使用数据绑定模式或手动比较新旧值可以减少Canvas.SendWillRenderCanvases的开销。简化WM_NCHITTEST处理在自定义的WindowProc中IsPointerOverInteractiveUI函数里的RaycastAll调用是有成本的。可以优化为只在鼠标移动时通过判断上次检测位置是否变化或定期进行检测而不是每次收到WM_NCHITTEST消息都调用。4.2 实现窗口拖动当我们移除了标准标题栏WS_POPUP后窗口就无法拖动了。我们需要自己实现拖动逻辑。一个常见的做法是在UI上设计一个自定义的“标题栏”区域比如一个Panel当鼠标在这个区域内按下并拖动时调用user32.dll的SendMessage函数发送WM_NCLBUTTONDOWN消息并指定HTCAPTION参数系统就会接管后续的拖动操作。[DllImport(user32.dll)] private static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam); private const int WM_NCLBUTTONDOWN 0xA1; private const int HTCAPTION 2; // 将此方法绑定到自定义标题栏UI元素的鼠标按下事件上 public void OnDragAreaPointerDown() { if (_windowHandle ! IntPtr.Zero) { // 释放鼠标捕获让Windows处理 SendMessage(_windowHandle, WM_NCLBUTTONDOWN, HTCAPTION, 0); } }在Unity中你可以给作为拖动区域的UI GameObject添加EventTrigger组件监听PointerDown事件并关联到OnDragAreaPointerDown方法。4.3 边缘停靠与自动隐藏为了更酷的桌面集成体验可以增加边缘停靠像一些任务栏工具和鼠标靠近显示、远离隐藏的功能。边缘停靠在Update中检测窗口位置Screen.width,Screen.height。当窗口靠近屏幕边缘一定像素内时使用SetWindowPos平滑地将窗口移动到紧贴边缘的位置。自动隐藏同样在Update中检测鼠标位置Input.mousePosition。计算鼠标与窗口边缘的距离。当距离大于阈值且持续一段时间后将窗口移出屏幕或设置为极小的尺寸/透明度。当鼠标再次靠近屏幕边缘的触发区域时再将窗口恢复。实现这些功能需要处理好状态切换和动画过渡避免生硬的跳变。可以使用Mathf.Lerp或DOTween等插件来实现平滑移动和淡入淡出效果。5. 常见问题与调试技巧在实际开发中你肯定会遇到各种奇怪的问题。这里记录一些我踩过的坑和解决方法。5.1 窗口句柄获取失败或为0问题GetActiveWindow()或Process.MainWindowHandle返回IntPtr.Zero。排查时机问题在Awake或过早的Start中调用窗口可能还未创建。尝试在Start之后如用Invoke延迟几帧或在第一次Update中获取。编辑器 vs 打包后在Unity编辑器中GetActiveWindow获取的可能是编辑器窗口句柄。使用Process.MainWindowHandle并确保你的游戏视图是独立的非嵌入模式可能会更准。最可靠的测试方式是直接打包成exe运行。权限问题极少数情况下可能需要以管理员权限运行应用才能成功修改某些窗口属性。5.2 透明或点击穿透效果异常问题设置了透明样式但背景不透明或者点击穿透了整个窗口导致UI无法交互。排查样式叠加顺序确保设置样式的顺序正确。通常先设置WS_EX_LAYERED再调用SetLayeredWindowAttributes。颜色键匹配检查你在SetLayeredWindowAttributes中设置的颜色键crKey是否与Unity中作为背景的UI元素的颜色完全一致包括RGB值。一个像素的差异都会导致透明失败。建议使用一个非常独特的颜色。WS_EX_TRANSPARENT的误用记住这个样式会让整个窗口对鼠标事件“隐身”。除非你确定小部件完全不需要点击否则不要用它。优先使用处理WM_NCHITTEST消息的方案。WM_NCHITTEST处理中的坐标错误这是最常见的原因。务必进行屏幕坐标到Unity UI坐标的Y轴翻转。在调试时可以将鼠标坐标打印出来与Input.mousePosition对比确保转换正确。5.3 打包后功能失效问题在编辑器里运行得好好的打包成exe后置顶、透明等功能都没了。排查目标平台确保项目Build Settings中的目标平台是“Windows, Mac, Linux” - “PC, Mac Linux Standalone”并且架构是x86_64。在Player Settings中检查“Resolution and Presentation”下的“Fullscreen Mode”不是独占全屏Exclusive Fullscreen建议设为“Windowed”。代码条件编译有时我们可能写了只在编辑器下执行的代码。确保所有调用Windows API的代码没有放在#if UNITY_EDITOR ... #endif块中。依赖项纯P/Invoke调用user32.dll一般不需要额外依赖因为它是系统核心库。但确保没有其他插件冲突。5.4 性能问题问题小部件导致CPU占用率偏高。排查Profiler是利器在Unity编辑器中运行打包后的exe通过Attach to Profiler使用Profiler查看CPU占用。重点关注Update、WM_NCHITTEST处理函数以及任何频繁调用的UI更新逻辑。降低频率将所有的实时数据获取、UI更新逻辑从Update移到按固定时间间隔执行。优化碰撞检测如果使用了WM_NCHITTEST方案优化IsPointerOverInteractiveUI函数。可以考虑为可交互UI区域建立一个简单的矩形边界列表用几何计算代替昂贵的RaycastAll。5.5 窗口样式修改导致的副作用问题窗口无法关闭、无法移动、任务栏出现多个图标等。排查与解决无法关闭移除了标准窗口样式后AltF4可能失效。你需要在自己的UI上提供一个关闭按钮并在其点击事件中调用Application.Quit()。任务栏图标添加WS_EX_TOOLWINDOW样式可以防止窗口出现在任务栏。如果同时有多个窗口可能需要更复杂的处理。窗口阴影某些窗口样式如WS_POPUP可能会丢失系统默认的窗口阴影。如果你需要阴影可能需要自己用UI绘制一个模拟阴影或者探索其他API如DwmExtendFrameIntoClientArea来启用。开发这类与系统深度交互的工具就是一个不断遇到问题、查阅文档MSDN是宝库、调试和解决的过程。每一次问题的解决都会让你对Windows桌面编程的理解更深一层。最后记得在发布前在不同的Windows版本如Win10, Win11上进行充分的测试因为不同版本的系统对某些API的行为可能有细微差别。
返回列表