Qt 自定义无边框对话框无法居中的终极排查:一个被忽略的窗口标志

环境:Windows + Qt 5.12.10(mingw73_64)
场景:QDialog + Qt::FramelessWindowHint 自定义弹窗,要求在父窗口中央弹出

一、问题背景

项目需要一个「系统连接与功能配置」弹窗,采用无边框设计(去掉系统标题栏,自己画头部),要求像原生模态对话框一样稳定居中在软件正中央,并支持全屏 / 非全屏 / 多显示器切换。

弹窗结构:

  • config_dlg.ui:自定义布局,windowFlags 设为无边框
  • config_dlg.cpp:构造函数里 setWindowFlags(Qt::FramelessWindowHint)
  • mainwindow.cpp:点击按钮时创建弹窗 + 背面暗色遮罩 + exec() 模态弹出

二、现象

弹窗始终无法正确居中,表现反复横跳:

  1. 弹窗偏右下角,偏离父窗口中心;
  2. 换一个显示器后,只显示背面遮罩,弹窗消失
  3. 各种 move() 方案都试了,位置怎么改都不对

三、排查过程:走过的四段弯路

弯路 1:exec() 之前 move()

ConfigDlg dlg(this);
dlg.move(center.x() - dlg.width() / 2, center.y() - dlg.height() / 2);
if (dlg.exec() == QDialog::Accepted) { ... }

结果:偏右下。

原因:无边框窗口在 show() 时,会被 Windows 窗口管理器按默认级联策略放置,覆盖掉 show() 之前的 move()

弯路 2:showEvent() + QTimer::singleShot(0)

showEvent 里用 QTimer::singleShot(0, ...) 在事件循环空闲时再居中一次。

结果:能居中,但被判定为「不可取的 hack」,要求回退。

弯路 3:show() 之后 move()

dlg.show();
dlg.move(center - dlg.rect().center());
dlg.exec();

先把窗口 show() 出来(WM 已分配默认位置),紧随其后 move() 覆盖居中。

结果:单屏下勉强可用,但仍然不稳定、多屏依旧异常

弯路 4:屏幕坐标 + screenAt() 约束

frameGeometry().center() 求全局中心,QApplication::screenAt() 定位所在屏幕,availableGeometry() 做边界约束。

结果:逻辑上「无可挑剔」,实际依然不居中

四、真正的根因:一个被忽略的窗口标志

走到弯路 4,move() 的目标坐标在数学上已经是绝对正确的了,但窗口依然偏位。这说明问题根本不在 move() 的坐标计算,而在窗口自身的类型状态已经损坏

关键陷阱:setWindowFlags() 是「替换」,不是「追加」

// config_dlg.cpp 构造函数
setWindowFlags(Qt::FramelessWindowHint);
<!-- config_dlg.ui -->
<property name="windowFlags"><set>Qt::FramelessWindowHint</set></property>

这两处写法都犯了一个经典错误:setWindowFlags() 的语义是完全替换原有的窗口标志,而不是在原有基础上「添加」。

QDialog 默认带有 Qt::Dialog 类型标志。当传入 Qt::FramelessWindowHint 时,Qt::Dialog 被覆盖丢失了

Qt::FramelessWindowHint 只是修饰位,不是窗口类型

Qt::FramelessWindowHint 是一个修饰标志(modifier),它的作用仅是「去掉边框/标题栏」,并不能单独构成一个有效的顶层窗口类型。

QDialog 需要 Qt::Dialog(或 Qt::Window)这类类型标志才能维持对话框的完整行为。

丢失 Qt::Dialog 的连锁反应

窗口类型从「对话框」降级为「普通窗口」后:

能力 Qt::Dialog 存在 Qt::Dialog 丢失
模态阻塞父窗口 ❌ 失效
基于父窗口的自动居中 ❌ 失效
move() 定位基准 ✅ 正确 ❌ 异常
窗口归属/层级 ✅ 正确 ❌ 混乱

这就是「怎么改 move() 都不居中」的根本原因——move() 算得再准,作用在一个「类型损坏」的窗口上也无济于事。

五、解决方案

保留 Qt::Dialog 类型标志,同时附加无边框修饰:

1. config_dlg.cpp

setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);

2. config_dlg.ui<set> 同样是替换语义,必须显式补上 Qt::Dialog

<property name="windowFlags"><set>Qt::Dialog|Qt::FramelessWindowHint</set></property>

3. 保持 show()move() 的居中逻辑不变(避开 WM 级联覆盖)

ConfigDlg dlg(this);
dlg.adjustSize();
dlg.show();                                   // 先 show,让 WM 分配默认位置
dlg.move(this->geometry().center() - dlg.rect().center());  // 再覆盖居中
dlg.raise();
dlg.activateWindow();
dlg.exec();

改完后,全屏 / 非全屏 / 多屏切换下均能正确居中。

六、最佳实践总结

  1. setWindowFlags() 是替换不是追加:需要保留原有标志时,用 windowFlags() | newFlags,或显式写出全部需要的标志。
  2. 无边框 ≠ 无类型Qt::FramelessWindowHint 只是修饰位,必须与 Qt::Dialog / Qt::Window 等类型标志组合使用。
  3. .ui 里的 <set> 同样是替换语义:别只写 <set>Qt::FramelessWindowHint</set>,要写 <set>Qt::Dialog|Qt::FramelessWindowHint</set>
  4. 无边框窗口的居中顺序adjustSize()show()move()exec()move() 必须在 show() 之后。
  5. 改了 .ui 必须重新 qmake + 完整编译,否则 windowFlags 改动不会进入生成的 ui_*.h
THE END