我以前做桌面程序,对"分辨率"这件事是真的没做这么细。
界面能跑、控件不重叠、在开发机上看着正常,基本就算交付了。直到有人跟我说"桌面程序控制在 1K 以内",我才发现,这个看似简单的要求,背后藏着一整套关于像素、缩放、渲染目标和性能预算的工程认知。而这些东西,恰恰是桌面开发里最容易被"能跑就行"掩盖掉的部分。
这篇文章,算是我对这件事的一次补课。补课的方式是:把每一个模糊的概念,都落到 C++ Qt 的具体代码和 API 上,最后给出一个能直接照着做的判断顺序。
一、"1K 以内"到底在说什么
先厘清概念。在显示领域,"K"是横向像素的数量级:1K 通常指 1920×1080 这一档,2K 是 2560×1440,4K 是 3840×2160。所以"控制在 1K 以内",字面意思是渲染的像素规模不超过约 1080p 这一档,总像素量在 200 万左右。
它不是"长宽各不超过 1000 像素",也不是物理尺寸。它是像素总量的预算。
这个区分在 Qt 里尤其关键。因为 Qt 的坐标系使用的是设备无关像素 :创建一个 QWidget 并设为 200×200,在设备像素比为 1.0 的普通屏幕上覆盖 200×200 个物理像素,但在设备像素比为 2.0 的高密度屏幕上会覆盖 400×400 个物理像素。你限制了逻辑尺寸,物理像素可能已经翻倍。
所以"限制分辨率"这句话,在 Qt 里必须先翻译成:"你要限制的是 QWidget 的逻辑尺寸,还是它实际占用的物理像素,还是内部某个渲染目标的缓冲区大小?"
二、改 .ui 文件为什么不够
最直觉的做法是去 mainwindow.ui 里把窗口尺寸调小。但这只是设计时的初始值。程序跑起来,用户照样能拉伸窗口;高 DPI 屏幕上,同样的逻辑尺寸会渲染出更多物理像素;布局管理器还会根据内容撑开控件。
.ui 管的是"长什么样",管不了"运行时占多少像素"。真正要限制,得在 C++ 代码里做。
如果只是"窗口别太大",setMaximumSize(1280, 720) 一行就够。它限制的是逻辑尺寸,用户无法把窗口拉到超过这个值。如果想彻底锁死,setFixedSize(1280, 720) 同时设置最小和最大尺寸为同一个值,窗口完全不可调整。
但这仍然只是逻辑层面的控制。如果应用内部有 QOpenGLWidget、视频渲染区域或离屏画布,真正该限制的是那个渲染目标的尺寸。主窗口可以大,但内部画布要有明确的物理像素上限。这两者不是一回事。
三、被忽略的变量:devicePixelRatio
高 DPI 是桌面开发里最容易被低估的变量。Qt 6 默认启用了高 DPI 缩放,这意味着同一个 setFixedSize(1280, 720),在 100% 缩放的屏幕上渲染 1280×720 物理像素,在 200% 缩放的屏幕上渲染 2560×1440。你以为自己控制了 1K,实际上已经跑到 2K 以上了。
Qt 官方文档对这个模型的描述很清晰:应用程序在设备无关像素上运行,然后通过设备像素比映射到显示器的物理像素。QWindow::devicePixelRatio() 返回当前窗口所在屏幕的缩放因子,QScreen::devicePixelRatio() 返回屏幕级别的因子。
要真正锁定物理像素,得用这个比值反算逻辑尺寸:
cpp
scss
// 注意:不要在 MainWindow 构造函数里调 windowHandle(),此时可能还没有原生句柄
qreal dpr = screen()->devicePixelRatio(); // 或 QGuiApplication::primaryScreen()->devicePixelRatio()
setFixedSize(qRound(1280 / dpr), qRound(720 / dpr));
但这里有一个陷阱:窗口可能在不同 DPI 的屏幕之间移动。如果在构造函数里一次性取 dpr,用户把窗口拖到另一块屏幕上时,限制就会失效。更稳妥的做法是监听 QWindow::screenChanged 信号,在屏幕变化时重新计算并应用尺寸约束。
另外,如果在 resizeEvent 里做任何尺寸调整,不要直接调用 resize() 或 setGeometry(),会导致无限递归。要么用标志位防重入,要么改用事件过滤器或定时器延迟处理。
这里有一个更深层的认知转变:桌面程序的"分辨率"不是单一概念,而是一条链。 QWidget 逻辑尺寸 → devicePixelRatio → 物理像素 → 渲染缓冲区 → GPU 纹理。任何一环没对齐,"1K 以内"就只是个幻觉。以前我只看第一环,后面的全凭系统默认,出问题也不知道从哪查。
四、C++ Qt 中的分层控制策略
把上面的认知落到代码上,我给自己整理了一套分层策略。不同层管不同的事,不要混用。
第一层:窗口逻辑尺寸约束。 这是最粗粒度的控制,解决"用户别把窗口拉太大"的问题。setMaximumSize() 或 setFixedSize() 就够了。如果需要根据内容自适应又不想超过上限,可以配合布局的 sizeConstraint,比如 QLayout::SetMaximumSize 会让主窗口的最大尺寸跟随布局的 maximumSize()。
第二层:渲染目标的物理像素约束。 如果窗口内有 OpenGL 渲染、视频输出或自定义绘制区域,在创建渲染目标时根据 devicePixelRatio 反算实际需要的缓冲区尺寸。比如一个 QOpenGLFramebufferObject 或 QImage 的尺寸,应该是"目标物理像素 / dpr",而不是直接传入逻辑尺寸。
第三层:性能预算兜底。 如果需求方说"1K 以内"的实质是"别让 GPU 跑太满",那么真正该监控的是渲染缓冲区的总像素量。可以在渲染循环或 resizeEvent 中计算当前实际渲染的像素数,超过阈值时触发降采样或限制渲染区域。这比单纯锁窗口尺寸更贴近性能问题的本质。
五、到底该选哪一层:判断顺序
三层讲完,真正的问题是:拿到"1K 以内"这个需求,我该设哪个?我的判断顺序是这样的。
第一步,先问需求方一句。 "1K 是指窗口逻辑尺寸不超过 1280×720,还是指实际渲染的物理像素不超过 1080p 档?"这一句能省掉后面所有返工。需求方说不清的情况很常见,但问过之后,至少你知道该按哪种理解去交付。
第二步,默认按第一层做。 如果对方说不清,或者只在高 DPI 缩放的普通机器上验收,setMaximumSize(1280, 720) 就是最直观、验收最容易的做法,覆盖大多数"窗口别太大"的真实意图。改 .ui 不算限制,这一层才是最小可用的限制。
第三步,有渲染目标就加第二层。 如果应用里有 QOpenGLWidget、视频播放或离屏渲染,第一层就不够了。主窗口可以大,但内部画布的物理像素要有上限,按 devicePixelRatio 反算缓冲区尺寸。这是真正吃 GPU 的地方,也是"1K 以内"最可能指的东西。
第四步,慎用"物理像素严格锁定"。 按 dpr 反算、让窗口物理像素严格不超过 1080p 档,技术上可行,但代价是窗口在高 DPI 屏上看起来会很小,界面元素也会偏小。只有在明确要求"物理像素严格受限"时才用,并且要接受这个代价。
一句话收尾:改 .ui 不算限制;setMaximumSize 管逻辑尺寸;按 devicePixelRatio 反算才管物理像素;真正该限制的往往是内部渲染缓冲区,不是主窗口。 先确认语义,再落代码。
六、从"能跑"到"可预期"
这件事让我重新理解了桌面开发和 Web 开发的一个差异。
Web 开发里,视口、像素比、响应式布局是绕不开的日常,开发者天然对"像素从哪来、到哪去"有感知。而 Qt 这种框架,把很多细节封装得太好了------好到你可以完全不懂 devicePixelRatio,程序也能跑起来。QWidget 的布局管理器自动处理尺寸,QPainter 自动利用高 DPI,一切看起来都很顺滑。于是"能跑"成了默认标准,直到遇到"控制在 1K 以内"这种具体要求,才发现自己对底层一无所知。
但"可预期"比"能跑"高一个层级。可预期意味着:我知道这个窗口在 100%、150%、200% 缩放下分别占多少物理像素;我知道 QOpenGLWidget 的渲染缓冲区最大不会超过多少;我知道 QImage 离屏渲染时实际分配的内存上限在哪。这些不是靠 .ui 文件能回答的,得靠对整条像素链的理解,以及知道该调哪个 Qt API 来干预每一环。
补完这一课,我给自己留下一条兜底原则:任何涉及像素尺寸的代码,都要问一句"在 200% 缩放下会怎样"。 固定尺寸、截图、离屏渲染、QImage 创建,全是高 DPI 的重灾区。QWindow::devicePixelRatio() 和 QScreen::devicePixelRatio() 应该像 width() 和 height() 一样熟悉。
结语
"桌面程序控制在 1K 以内"这句话,表面上是个尺寸要求,实际上是一次对工程细度的拷问。它逼着我去区分逻辑像素和物理像素,去理解 .ui 和运行时的边界,去正视 devicePixelRatio 这个一直被我忽略的变量,去在 C++ 代码里找到每一个可以干预的 API 节点。
以前我觉得 Qt 桌面程序"能跑就行",是因为我没遇到过真正较真的需求。现在回头看,那些被框架封装掉的细节,不是不重要,只是还没到暴露的时候。
做细一点,不是为了应付某一条要求,而是让自己对程序的每一像素都有数。而这条路的第一步,就是先确认"1K"到底指哪一层,再落代码。