1. 这个场景到底在解决什么问题先把这个标题里的关键词拆开念一遍“Qt 窗口嵌入桌面”“窗口放在桌面”。本质上想干的事情是让你的Qt程序窗口不再以普通顶层窗口的形式悬在桌面上而是“长”进桌面壁纸那一层——就像动态壁纸、桌面日历、桌面便签、系统监控小组件那样。窗口永远在桌面上、在桌面图标的下面任何窗口都盖不住它也不需要抢焦点开机自启后静静趴着。这种效果在工作里经常有人想要。比如做一个桌面宠物、天气预报挂件、桌面备忘录甚至公司内部的信息滚动屏、运营数据大屏的保留小窗让同事一开电脑就能看到又不会被弹出来的聊天窗口挡住。很多人一上来直接搜“Qt 窗口嵌入桌面”搜出来的帖子很多年久失修代码拷下来运行直接崩溃或者黑屏于是来来回回折腾。这类需求的本质其实不是Qt层面的问题而是操作系统窗口管理的问题。窗口到底属于哪一层、父窗口是谁、由谁来重绘、会不会被桌面重新绘制时清掉这一整套逻辑都握在系统手里。Qt只是我们用来创建窗口的框架所以搞清楚系统窗口层级比闷头写Qt代码重要得多。这篇文章就把我实际跑通过的一套做法完整写出来包含Windows平台下的核心实现、踩过的坑、动态壁纸和任务栏场景下的处理方式以及Linux/X11下的大致思路。不管你是刚接触Qt还是已经有几年经验照着做能少走两个月的弯路。提示本文主要针对Windows平台。macOS那边窗口层级管理完全是另一套逻辑Qt常规手段做不了真正的“嵌入桌面壁纸层”不建议在macOS上死磕这个方案。2. 嵌入桌面的方案选型与核心思路2.1 为什么不直接置底窗口有人第一反应是把Qt窗口的WindowFlags设置成Qt::Tool或者Qt::WindowStaysOnBottomHint再加个Qt::FramelessWindowHint是不是就一直在底层了这个思路听着简单实测会出一堆问题。Qt::WindowStaysOnBottomHint的确能让窗口保持在z序底部但它只是“尽量置底”不是真正“贴进桌面层”。打开几个窗口来回切换、按WinD显示桌面、切换虚拟桌面窗口就会浮出来盖住桌面图标或者干脆被任务栏遮挡表现非常不稳定。而嵌入桌面的最终目标是把自己变成桌面的一部分。鼠标按“显示桌面”时桌面图标和你的窗口一起留着按WinD时你的窗口也不会被最小化任何窗口都盖不住你因为你压根不在普通窗口的层级里。2.2 方案对比WorkerW窗口注入Windows实现桌面挂件的主流方法是找到桌面的“壁纸窗口”把我们的窗口塞进去当子窗口。具体来说系统里存在一个叫WorkerW的窗口这个窗口是Explorer创建的它下面挂着SHELLDLL_DefView就是装桌面图标的那个视图而WorkerW就是壁纸的宿主窗口。动态壁纸软件、Wallpaper Engine这些基本都走这个路子。流程整体是这样的找到桌面上那个类名是Progman的窗口也就是“程序管理器”桌面图标的最底层宿主。向Progman发送一条特殊消息0x052C让系统拆分出WorkerW窗口。枚举顶层窗口找到类名为WorkerW、并且它下面有SHELLDLL_DefView子窗口的那个。把WorkerW作为父窗口调用SetParent把我们Qt窗口设置为它的子窗口。调整尺寸覆盖全屏幕或者只在某个区域显示。这套方法的厉害之处在于只要WorkerW还在你的窗口就跟着桌面一起显示、一起隐藏按WinD时桌面程序管理器会被隐藏你的窗口作为子窗口也跟着隐藏但不会最小化。动态壁纸也是这么干的所以兼容性已经被验证得很充分了。2.3 为什么不用SetWindowPos置底也有一种做法是不改父窗口只把Qt窗口用SetWindowPos设置到HWND_BOTTOM。这个做法属于“治标不治本”。一旦你的窗口失去焦点或者某个全屏程序切换过来它就会跑到普通窗口栈里屏幕闪一下窗口跑到最上面来了。改成WorkerW子窗口后层次关系是稳定的不需要时刻盯防。还有一个细节很多教程会让你去“隐藏桌面图标”再“找WorkerW”其实没必要这么做用0x052C消息让系统把WorkerW拆出来就行。有些系统版本上如果不发消息直接找WorkerW会找不到因为WorkerW可能压根还没创建。所以发消息这个环节不能省。3. 核心实现从找窗口到嵌入一步步来3.1 头文件与关键API准备在Windows上做这个功能Qt项目里必然要引入Windows API。如果是MinGW环境直接包含windows.h如果是MSVC环境也一样。这里给出一份能直接用的完整代码结构。// mainwindow.h #pragma once #include Windows.h #include QMainWindow class QScreen; class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); protected: bool nativeEvent(const QByteArray eventType, void *message, qintptr *result) override; private: void embedIntoDesktop(); void findDesktopWorkerW(); void applyDesktopWorkaround(); HWND m_desktopWorkerW nullptr; };在.cpp文件里需要先声明几个关键的东西。Progman窗口的类名是LProgmanWorkerW的类名是LWorkerW子窗口视图类名是SHELLDLL_DefView。// mainwindow.cpp #include mainwindow.h #include QDebug #include QScreen #include QApplication #include QWindow #include dwmapi.h static const UINT g_uRestart 0x052C; static const wchar_t kProgmanClass[] LProgman; static const wchar_t kWorkerWClass[] LWorkerW; static const wchar_t kShellViewClassName[] LSHELLDLL_DefView;3.2 通过 EnumWindows 找到正确的 WorkerW这段代码是整套方案的核心。思路是先用FindWindow拿到Progman然后发送0x052C消息再枚举顶层窗口逐个检查类名和子窗口。void MainWindow::embedIntoDesktop() { // 1. 先找到桌面程序管理器Progman HWND progman ::FindWindowW(kProgmanClass, nullptr); if (!progman) { qWarning() FindWindow(Progman) failed; return; } // 2. 发送消息触发系统拆出 WorkerW ::SendMessageTimeoutW(progman, g_uRestart, 0, 0, SMTO_NORMAL, 1000, nullptr); // 3. 枚举窗口找到挂载 SHELLDLL_DefView 的那个 WorkerW ::EnumWindows([](HWND hwnd, LPARAM lParam) - BOOL { // 获取窗口类名 wchar_t className[256] {0}; ::GetClassNameW(hwnd, className, 255); if (wcscmp(className, kWorkerWClass) ! 0) { return TRUE; // 继续枚举 } // 检查是否包含了桌面图标视图 HWND shellView ::FindWindowExW(hwnd, nullptr, kShellViewClassName, nullptr); if (shellView) { // 这就是我们要的 WorkerW *reinterpret_castHWND*(lParam) hwnd; return FALSE; // 找到就停 } return TRUE; }, reinterpret_castLPARAM(m_desktopWorkerW)); if (!m_desktopWorkerW) { qWarning() WorkerW not found; return; } qInfo() WorkerW found: m_desktopWorkerW; }这里有两个容易出问题的地方。第一个是SendMessageTimeout换成SendMessage行不行我建议别换。原因很直白SendMessage会一直阻塞直到消息被处理万一Explorer卡住你的程序也会一起卡住。SendMessageTimeout带上超时比较安全。第二个是EnumWindows的Lambda回调里不要做耗时操作否则会拖慢整个枚举过程。我们只做GetClassNameW和FindWindowExW这两个都是轻量调用没问题。3.3 SetParent 把窗口挂进去找到WorkerW之后剩下的事情就简单了把一个不可见的空窗口变成WorkerW的子窗口然后放到合适的位置。void MainWindow::embedIntoDesktop() { // ... 上面的查找逻辑 ... // 把窗口设置为 WorkerW 的子窗口 ::SetParent(reinterpret_castHWND(this-winId()), m_desktopWorkerW); // 去掉窗口边框、去掉标题栏如果用的是普通QMainWindow this-setWindowFlags(Qt::FramelessWindowHint | Qt::Window); // 获取屏幕尺寸铺满整个工作区 QScreen *screen QApplication::primaryScreen(); QRect screenRect screen-geometry(); // 注意这里不能用 availableGeometry否则会避开任务栏区域我们要的是全屏 this-setGeometry(screenRect); this-show(); }有几个细节值得展开说说。关于winId()winId()是Qt窗口最终映射到系统窗口句柄的入口。直接拿this-winId()传给SetParent是可行的但有个前提窗口必须先创建完成。如果一个窗口从未show()过winId()会触发窗口的延迟创建这时候拿到的句柄是临时的后面窗口show出来之后句柄可能已经变了SetParent就白干了。稳妥的做法是在show()之后再去拿winId()或者先强制winId()再操作。我在实际代码里通常先this-show()一下再调用嵌入流程顺序颠倒就会出幺蛾子。关于窗口尺寸用geometry()拿到的屏幕尺寸是包含任务栏的整个屏幕物理区域而不是工作区。这里必须用完整屏幕尺寸因为我们希望窗口覆盖到任务栏后面、铺满整个WorkerW客户区。如果用availableGeometry()会出现一条边差一截看起来像贴歪了。一旦成了WorkerW的子窗口任务栏是否覆盖在你窗口上面取决于WorkerW的尺寸本身。通常情况下WorkerW就是整块屏幕那么大所以我们直接用全屏尺寸。关于 setWindowFlags 的调用时机setWindowFlags放在SetParent之后调用这个顺序是我踩过坑才定的。如果先设置Qt::FramelessWindowHint再SetParent在某些Windows版本上窗口会出现一瞬间的闪白或者SetParent之后窗口边界计算异常。先挂父窗口再改标志实测最稳。3.4 动态壁纸模式的特殊处理如果你想做得更高级一点比如窗口本身透明桌面图标浮在你的颜色之上或者想让自己的窗口变成一张“动态壁纸”那处理逻辑就要稍微变一下。原理是WorkerW下面还有一个SHELLDLL_DefView它是桌面图标的宿主。如果你想让自己的窗口出现在图标下方那就把窗口嵌入到Progman下面、SHELLDLL_DefView的上面如果你想让窗口出现在图标上方、壁纸下方就把它嵌入到WorkerW下面同时在SHELLDLL_DefView的上面。实际做动态壁纸时很多人把窗口设成透明再把它放到Progman底下这样桌面图标依然在最上层。具体操作就是在找到Progman之后不完全依赖拆出来的WorkerW而是直接把窗口挂到Progman下然后手动把SHELLDLL_DefView的层级调高。这里有一个实用函数用来把桌面图标视图置顶void MainWindow::applyDesktopWorkaround() { HWND progman ::FindWindowW(kProgmanClass, nullptr); if (!progman) return; HWND defView ::FindWindowExW(progman, nullptr, kShellViewClassName, nullptr); if (!defView) { // 在某些系统上SHELLDLL_DefView 不在 Progman 直接子级需要去 WorkerW 里找 HWND workerw nullptr; ::EnumWindows([](HWND hwnd, LPARAM lParam) - BOOL { wchar_t className[256] {0}; ::GetClassNameW(hwnd, className, 255); if (wcscmp(className, kWorkerWClass) 0) { HWND shellView ::FindWindowExW(hwnd, nullptr, kShellViewClassName, nullptr); if (shellView) { *reinterpret_castHWND*(lParam) shellView; return FALSE; } } return TRUE; }, reinterpret_castLPARAM(defView)); } if (defView) { // 让桌面图标视图保持在Z序最上面 ::SetWindowPos(defView, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); } }这段代码并不复杂但很多时候就是这一小块代码决定你的桌面挂件是在图标下面还是在图标上面。默认不做这步你的窗口挂到WorkerW下会盖在图标上——如果窗口是不透明的桌面图标直接被挡住用户会觉得莫名其妙。3.5 完整初始化流程把上面几段组织到构造函数里注意顺序和时序MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 无边框、不抢焦点 setWindowFlags(Qt::FramelessWindowHint | Qt::WindowDoesNotAcceptFocus); setAttribute(Qt::WA_TranslucentBackground, false); // 根据需求设置 // 先显示出来确保 winId() 有效 this-show(); // 关键步骤嵌入桌面 embedIntoDesktop(); }Qt::WindowDoesNotAcceptFocus这个标志非常重要。桌面挂件一般不参与鼠标点击焦点轮换——你点一下挂件上一个正在操作的窗口不会因此失焦。这一点在写实用型挂件时体验差异很大。如果你的挂件需要接收键盘输入那这个标志就不要加。4. 嵌入后的消息派遣与事件坑4.1 WM_SIZE/WM_WINDOWPOSCHANGING 问题窗口变成WorkerW的子窗口后系统对Windows大小同步的时机和普通顶层窗口不太一样。工作里遇到比较多的是你改了this-setGeometry()但窗口实际尺寸没变或者变了一部分界面显示不对。原因在于SetParent之后窗口的坐标系统相对父窗口发生了偏移。WorkerW的客户区原点是(0,0)但屏幕坐标可能也是(0,0)正常没区别但如果任务栏在左侧或者你用了多显示器子窗口的坐标就可能是相对父窗口的负数区域。解决办法是在嵌入后强制发一次WM_SIZE给Qt窗口void MainWindow::nativeEvent(const QByteArray eventType, void *message, qintptr *result) { MSG *msg static_castMSG*(message); if (msg-message WM_SIZE) { // 按需处理 qDebug() WM_SIZE received msg-wParam; } return QMainWindow::nativeEvent(eventType, message, result); }这里不建议对消息做额外拦截除非你有特殊需求。Qt本身会处理大部分尺寸变化。真正容易翻车的是在resizeEvent里递归调用setGeometry导致消息风暴。我的经验是嵌入后只设置一次尺寸之后让Qt自己处理重绘不要反复调用系统级接口。4.2 焦点与鼠标穿透桌面挂件经常需要“看得见但不挡操作”。如果你的挂件只是显示信息不需要点击那就干脆让鼠标点上去直接穿透。实现“鼠标穿透”最简单的方式是给窗口设置WS_EX_TRANSPARENT和WS_EX_LAYERED扩展样式void MainWindow::setClickThrough(bool enabled) { HWND hwnd reinterpret_castHWND(this-winId()); LONG_PTR style ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE); if (enabled) { style | WS_EX_LAYERED | WS_EX_TRANSPARENT; } else { style ~(WS_EX_LAYERED | WS_EX_TRANSPARENT); } ::SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style); }设置之后鼠标会直接穿透这个窗口点击到下面的桌面图标或者壁纸。适合做纯展示型的挂件比如天气、时间、系统状态监控。这里有个细节WS_EX_TRANSPARENT不改变窗口的视觉透明度只影响鼠标命中测试。很多人以为它会让窗口变透明这是个常见误解。4.3 关于 DPI 缩放Windows高DPI缩放对Qt窗口的尺寸计算影响非常大。如果你使用setGeometry(QScreen::geometry())在缩放率150%的屏幕上Qt拿到的geometry其实已经是逻辑像素了Qt会自动按设备像素比映射。但SetParent之后系统层面的坐标换算用的是物理像素之间容易出现20像素左右的偏差。两套体系混在一起最典型的症状是窗口显示后上下左右各有一条细边露出桌面壁纸或者窗口在屏幕右上角偏离了半个挂件的宽度。解决办法是统一使用Qt的逻辑坐标。全部用QScreen和QRect计算不要手工混用GetSystemMetrics。如果确实需要系统层面的屏幕尺寸要额外除以devicePixelRatioF()。QScreen *screen QApplication::primaryScreen(); QRect screenRect screen-geometry(); qreal dpr screen-devicePixelRatio(); QRect physicalRect( static_castint(screenRect.x() * dpr), static_castint(screenRect.y() * dpr), static_castint(screenRect.width() * dpr), static_castint(screenRect.height() * dpr) );真实项目中在embedded环境下如果你想用物理像素去移动窗口或者获取位置建议直接通过Qt的QWindow::setGeometry走逻辑坐标省心。5. 常见问题与排查技巧实录5.1 WorkerW 找不到窗口嵌入后消失这是被问得最多的一个问题。现象是程序运行了没有报错窗口也有但屏幕上什么都看不到。排查路径基本固定。先用FindWindowW(LProgman)确认Progman存在接着确认0x052C消息有没有发到位再在EnumWindows里输出所有类名为WorkerW的窗口句柄和子窗口信息。有一个高概率原因0x052C发送时机太早。如果你的程序开机自启在Explorer还没有完全初始化的时候就去找WorkerW消息是发出去了但系统还没来得及拆分WorkerW枚举自然找不到。这种情况下加延时重试即可。我写过一个简单的重试封装void MainWindow::retryFindWorkerW(int retries) { for (int i 0; i retries; i) { if (findWorkerWAndEmbed()) return; QThread::msleep(200); } qWarning() Failed to embed desktop after retries; }在实际使用中重试2~3次基本就稳了。5.2 嵌入成功后窗口一闪而过就消失这个现象多半是窗口被当作“临时子窗口”回收了。比如你在构造函数里先调用了embedIntoDesktop()然后Qt内部创建窗口时把旧的winId()替换了导致父子关系失效窗口被系统销毁。还有一个常见原因m_desktopWorkerW在窗口销毁或Explorer刷新时被系统回收。当你切换主题、重启Explorer、或者系统更新后WorkerW会被重建你的旧窗口会被自动销毁程序虽然还在跑窗口却消失了。需要监听系统事件检测到WM_SETTINGCHANGE或者其他桌面相关事件后重新嵌入。监听的方式是在nativeEvent里捕获if (msg-message WM_SETTINGCHANGE) { // 延时重新嵌入避免在系统刷新时同步操作 QTimer::singleShot(200, this, [this]() { embedIntoDesktop(); }); }5.3 嵌入后窗口没有内容白屏或黑屏白屏大概率是窗口大小设置错了或者SetParent之后系统重绘信号没有触发。黑屏通常是渲染后端的问题尤其Qt5使用OpenGL或D3D渲染时子窗口的表面和父窗口的合成出现问题。一条最实用的建议把窗口的WA_TranslucentBackground属性先关掉排除透明通道干扰把渲染后端切到software或raster确认是渲染问题还是嵌入问题int main(int argc, char *argv[]) { QApplication app(argc, argv); // 强制 raster 渲染调试嵌入问题时很管用 app.setAttribute(Qt::AA_UseSoftwareOpenGL); // 或者 // qputenv(QT_OPENGL, software); }如果是Qt6还可以试试qputenv(QSG_RHI_BACKEND, software);把这块设成软件渲染后白屏黑屏基本都能定位到原因。确认没问题了再切回硬件加速毕竟动态壁纸如果每一帧都用软件渲染CPU占用会比较高。5.4 快捷键 WinD 之后窗口不跟着隐藏WinD显示桌面会让Explorer发送命令隐藏Progman以及所有子窗口。正常情况下窗口作为WorkerW的子窗口应该一并隐藏。但偶尔会出现嵌入后有窗口还挂在桌面上WinD之后还在。排查点集中在你的窗口是否真的挂在了WorkerW下面而不是挂在了DefView或Progman的另一个子窗口下。如果只是挂在Progman下WinD时行为可能不同。其次检查一下自己是不是调用了SetParent后又把窗口用SetWindowPos移到了HWND_TOP这会改变它在父窗口内部的z序但不会让它脱离子窗口关系不应该出现这种情况。真的遇到这种不跟随的情况可以在nativeEvent里手动监听WM_SETTINGCHANGE、或监听WM_SIZE判断桌面当前状态然后主动ShowWindow(hwnd, SW_HIDE)或SW_SHOW。虽然避开了根本问题但作为兜底手段是可行的。5.5 任务栏还在但窗口被任务栏盖住这是正常现象。WorkerW的客户区本来就不包括任务栏区域或者说任务栏是独立最高层窗口始终盖住WorkerW底部。但这不影响挂件功能因为挂件区域一般都在屏幕中间或右侧不太会跟任务栏重叠。但如果你的窗口确实要覆盖任务栏区域比如做一个全屏动态壁纸希望任务栏也变成透明或覆盖在上面那这条路就不通了。需要额外处理任务栏窗口把任务栏作为父窗口或者调整任务栏的透明度这是另一个话题不建议跟桌面嵌入混在一起做。6. 跨平台思路Linux/X11下的桌面嵌入不少用户是在国产操作系统统信UOS、麒麟等上做桌面挂件或者直接用X11环境。X11和Windows的思路完全不同没有WorkerW这个东西也没有0x052C这种消息。X11下的常规做法是找到桌面窗口通常类名是Desktop或者通过_NET_CLIENT_LIST判断把窗口设为桌面窗口的子窗口或者利用窗口管理器提供的特殊类型属性。Qt在X11下有一个接近原生的标志setWindowFlags(Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint);Qt::X11BypassWindowManagerHint让窗口不参与窗口管理器的层级管理类似于Windows里的WS_EX_TOOLWINDOWHWND_BOTTOM组合。但即便有这个标志也不一定能绕到底层。很多Linux桌面的合成器Mutter、KWin对桌面层的处理不一致。更实用的做法是通过Xlib直接找桌面窗口然后XReparentWindowDisplay *display XOpenDisplay(nullptr); Window root DefaultRootWindow(display); Window desktopWindow ...; // 通过 XGetWindowProperty 获取 _NET_DESKTOP_GEOMETRY 或者枚举窗口 XReparentWindow(display, this-winId(), desktopWindow, 0, 0);不过这里的难点在于如何在Qt已经创建了窗口之后拿到原生X11窗口ID再做reparent。Qt6里可以通过winId()直接拿QPA层面没问题。如果你在Linux上做建议直接先用简单的Qt::X11BypassWindowManagerHint跑通基础效果再考虑深层的reparent。说实话跨平台桌面嵌入这种活儿每换一个桌面环境可能就得适配一次投入产出比不一定划算。7. 扩展玩法与最好用的几个实践方向嵌入桌面的能力打通之后可以做的事情非常多这里列几个我实际做过或者观察到的典型用法。桌面动态壁纸最常规的玩法。把播放视频的Qt窗口嵌入到Progman下或者把OpenGL渲染的粒子系统窗口嵌进去桌面图标始终浮在上面。性能优化上注意视频渲染用硬解用QMediaPlayer配合QVideoWidget时可能需要额外处理透明叠加层。桌面监控面板做一个圆形的系统监控挂件显示CPU、内存、磁盘IO、网速用半透明背景嵌入桌面上角。不需要点击时开启鼠标穿透需要点击交互时关闭穿透。这种挂件在企业内部信息屏上很好用。桌面日历与便签比系统自带的桌面小工具更灵活。嵌入桌面后便签永远在桌面上不会因为打开应用被遮挡。写了个内部任务清单挂件团队里几个人反馈很好。会议倒计时与任务提醒团队大屏上放一个嵌入桌面的倒计时窗口每天下班前全屏投屏时都能看到不受投屏软件窗口覆盖影响。企业欢迎屏/信息发布屏在接待区电脑上做一个自动轮播的信息屏嵌入桌面后即使访客打开了浏览器或者PPT信息内容依然在桌面层显示不会遮挡正常工作。这些都是“窗口嵌入桌面”能力的自然延伸。核心底层代码是同一套剩下的就看你往窗口里装什么内容了。8. 写在最后的经验之谈这套东西我在实际项目里调了一两周才算彻底稳下来。最开始也试过只依赖Qt的窗口标志折腾两天发现不可靠最后回到原生API才真正解决问题。中间踩过的坑比如winId()时机、DPI换算、Explorer刷新后窗口丢失、WinD行为不一致每一个都值得单独聊几段。如果你只是想做个小工具自用照着本文第3节的代码直接跑大概率一次就成。但如果你要做成公司内部长期运行的信息屏、监控屏建议把以下几点纳入考虑开机自启后加入QTimer重试机制等Explorer完全就绪再嵌入。监听WM_SETTINGCHANGEExplorer崩溃重启或主题切换后自动重新嵌入。不要用老的FindWindowSendMessage组合处理动画密集场景SendMessageTimeout更稳。使用Qt的QTimer做重绘/刷新不要让UI操作堵塞GUI线程长期运行时很有帮助。多显示器环境下先枚举所有QScreen计算好目标屏幕的geometry如果嵌入后还需要移动位置用QWindow::setPosition结合屏幕坐标来操作。最后再分享一个高DPI环境的小经验。Win10/Win11上如果发现嵌入后窗口偏了位置或者大小不对先看devicePixelRatio然后检查Qt窗口是不是被系统自动缩放了一次。最省事的办法是用QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough)避免出现多个显示器、不同缩放比例时坐标换算错乱。桌面挂件这个功能Qt本身没提供开箱即用的接口但通过组合原生API能实现得很优雅。Windows的窗口体系虽然老却也给足了折腾空间。希望这篇文章能帮你少走一些弯路。