Tauri (23)——为什么每台电脑位置显示效果不一致?

在做跨平台桌面应用(Tauri / Electron / Flutter Desktop)时,"窗口尺寸/位置在不同电脑上不一致" 是非常高频、且非常隐蔽的坑:

  • 同一份代码,在你电脑上完美居中,在同事电脑上却偏上/偏下;
  • 外接显示器后又变了;
  • Windows 125% 缩放时尤其明显。

本文结合我们 Coco 在 Tauri 里的真实代码(src-tauri/src/lib.rs:320-375)来解释这些关键词背后的"坐标体系",并总结我们在项目里会遇到的问题与解决方案,帮助以后 一次性把 DPI/缩放坑填平


1. 先看真实场景:我们在做什么?

Coco 在显示主窗口时,会把窗口移动到 "当前鼠标所在的显示器",并做居中:

  • show_coco 会调用 move_window_to_active_monitorsrc-tauri/src/lib.rs:261-295
  • move_window_to_active_monitor 核心逻辑在这里(src-tauri/src/lib.rs:320-375

用户选中的关键片段(src-tauri/src/lib.rs:352-357):

rust 复制代码
// 590 是我们主窗口的默认高度
let window_height = 590 * scale_factor as i32;

// Horizontal center uses actual width, vertical center uses 590 baseline
let window_x = monitor_position.x + (monitor_size.width as i32 - window_width) / 2;
let window_y = monitor_position.y + (monitor_size.height as i32 - window_height) / 2;

你会看到几个关键字:

  • scale_factor:缩放比 / DPI 缩放因子
  • monitor_position / monitor_size:屏幕位置与尺寸
  • window_width / window_height:窗口宽高
  • PhysicalPosition:设置窗口位置时采用 "物理坐标"(src-tauri/src/lib.rs:358-360

问题的根源往往就出在:这些变量到底属于"物理像素"还是"逻辑像素"?如果单位混了,居中必然错。


2. 三个像素概念:物理像素、逻辑像素、缩放比

2.1 物理像素(Physical Pixel)

显示器的真实像素点,比如:

  • 1920×1080
  • 2560×1440
  • 3840×2160(4K)

物理像素是硬件层面的网格,不会因为系统缩放而改变

2.2 逻辑像素(Logical Pixel / DIP / Point)

操作系统为了让 UI 在不同 DPI 上看起来 "差不多大",引入了 "设备无关像素"。

直觉理解:

  • UI 的 590px 更接近"逻辑像素"
  • 操作系统/渲染层会把逻辑像素映射到物理像素

2.3 缩放比(Scale Factor / Device Pixel Ratio)

缩放比描述两者的关系:

  • physical = logical * scale_factor
  • logical = physical / scale_factor

典型取值:

  • macOS Retina:经常是 2.0
  • Windows:可能是 1.0 / 1.25 / 1.5 / 1.75 / 2.0 ...(尤其 125%/150% 非常常见)
  • 多显示器时:不同屏幕可能不同缩放比(Per-monitor DPI

3. 为什么每个人电脑效果不一致?

3.1 每个人的缩放设置不同(尤其 Windows)

同样是 27 寸 2K 屏:

  • A 设置 100%(scale_factor=1.0
  • B 设置 125%(scale_factor=1.25
  • C 设置 150%(scale_factor=1.5

如果你的代码用 "写死的像素值" 去做位置/大小计算,但没有明确它属于逻辑还是物理,就会导致 某些机器上偏移量大

3.2 多显示器 + 不同缩放比:坐标系统随屏幕变化

用户把窗口从内屏(2.0)拖到外接屏(1.0),或者鼠标在外接屏但窗口在内屏,这时:

  • "鼠标所在屏幕" 的坐标空间
  • "窗口当前 scale_factor"
  • "你用来计算的 monitor.size/position"

可能并不在同一个缩放体系里。

Coco 的做法是用 cursor_position 找鼠标位置,再用 monitor_from_point 找对应屏幕(src-tauri/src/lib.rs:323-340)。方向是对的,但更关键的是:后续计算必须统一单位。

3.3 最容易忽略:小数缩放比 + 整数截断导致系统性偏差

rust 复制代码
let window_height = 590 * scale_factor as i32;

这里有两个隐患:

  1. scale_factor 通常是 f64,比如 1.251.5
  2. 直接 as i32截断(不是四舍五入)

举个例子:

  • scale_factor = 1.25 时,scale_factor as i32 = 1
  • window_height = 590 * 1 = 590

这等价于完全忽略了 125% 缩放,必然会出现"纵向居中不准"。

即使你写成:

rust 复制代码
let window_height = (590.0 * scale_factor) as i32;

也会因为 as i32 截断导致 "每次都偏一两像素到几十像素",在某些显示器上会非常明显(尤其窗口吸附、居中时更敏感)。

3.4 WebView/CSS 的 px 语义与窗口 API 的 px 语义可能不同

前端(Web)里 px 基本等同于逻辑像素 (CSS 像素)。 Tauri 的窗口 API 有时要求你明确是 LogicalPosition/Size 还是 PhysicalPosition/Size

如果你用"CSS 里看起来是 590 高"的概念,直接塞到 PhysicalPosition 体系里去算位置,就会出现 "同事电脑 UI 高度一样,但窗口居中不一样" 的错觉。


4. 在实际项目中会遇到哪些典型问题?

问题 A:窗口居中在某些机器上偏上/偏下

常见原因:

  • 用逻辑高度(如 590)去减物理屏幕高度(monitor_size 可能是物理),单位混用
  • 或者用物理高度,但 scale_factor 计算/取整不对

问题 B:外接显示器后,窗口出现在错误的屏幕或坐标跑飞

常见原因:

  • 仍然按"主屏幕"缩放来换算
  • cursor_position / monitor_from_point / window.scale_factor 没有统一语义(鼠标在 A 屏,scale_factor 却来自 B 屏)

问题 C:Windows 125% 缩放时错得最离谱

常 见原因:

  • 1.25 这类小数缩放最容易被 as i32 截断
  • 还会引入大量"0.5 像素"类的 rounding 差,叠加后误差扩大

5. 解决方案:唯一原则------全程统一坐标空间

你可以选两条路线之一:

路线 1:全程用逻辑坐标计算,最后用 LogicalPosition 设置

适用于:你的 UI 设计是按"逻辑尺寸"(例如 590 这个 baseline)定义的。

推荐写法(示意):

rust 复制代码
use tauri::dpi::{LogicalPosition, LogicalSize};

let scale = window.scale_factor()?;

// 注意:monitor.position/size 多数情况下是 Physical,需要转 logical
let monitor_pos_l = monitor.position().to_logical::<f64>(scale);
let monitor_size_l = monitor.size().to_logical::<f64>(scale);

// window.inner_size() 通常返回 PhysicalSize,也转 logical
let window_size_l = window.inner_size()?.to_logical::<f64>(scale);

// 你的 baseline 直接用逻辑值
let baseline_h = 590.0_f64;

let x = monitor_pos_l.x + (monitor_size_l.width - window_size_l.width) / 2.0;
let y = monitor_pos_l.y + (monitor_size_l.height - baseline_h) / 2.0;

window.set_position(LogicalPosition::new(x, y))?;

核心好处:

  • 590 就是你 UI 里的 590(逻辑)
  • 计算更直观,少踩坑

路线 2:全程用物理坐标计算,最后用 PhysicalPosition 设置

适用于:你已经在使用物理 API,且 monitor/window 的 size/position 都是 physical。

关键点是:不要把 scale_factor 截断成整数 ,要用浮点乘完再 round()

rust 复制代码
use tauri::dpi::PhysicalPosition;

let scale = window.scale_factor()?; // f64

let baseline_h_physical = (590.0 * scale).round() as i32;

let monitor_position = monitor.position(); // PhysicalPosition<i32>
let monitor_size = monitor.size();         // PhysicalSize<u32>
let window_size = window.inner_size()?;    // PhysicalSize<u32>

let window_x =
  monitor_position.x + (monitor_size.width as i32 - window_size.width as i32) / 2;

let window_y =
  monitor_position.y + (monitor_size.height as i32 - baseline_h_physical) / 2;

window.set_position(PhysicalPosition::new(window_x, window_y))?;

对比 Coco 当前代码(src-tauri/src/lib.rs:351-356),差异点就是:

  • scale_factor as i32 改为 round() 后再转整数
  • 明确 590 属于逻辑 baseline,换算成物理高度再参与物理计算

6. 为什么我们会遇到这种问题?(根因总结)

一句话:跨平台窗口系统把"像素"拆成了两套坐标,而我们很容易混用。

你看到的 590 可能来自:

  • 前端 CSS 的设计尺寸(逻辑)
  • 产品/交互定义的 baseline(逻辑)
  • 但 monitor/window API 返回的却可能是 physical
  • 再加上 Windows 小数缩放与多屏 per-monitor DPI,混用后必然出现"每个人不一样"

7. 防踩坑 Checklist(强烈建议团队约定)

  • 任何涉及窗口的 x/y/width/height 计算,先写清楚:这段代码使用 Logical 还是 Physical
  • 禁止scale_factor as i32 这种截断式转换(尤其 Windows 125%)
  • 需要整数时:优先 round(),其次 floor/ceil(视 UI 期望决定)
  • 多屏场景:以"目标屏幕"的 scale_factor 做换算(鼠标在哪个屏幕、窗口要移动到哪个屏幕,就用那个屏幕对应的缩放体系)
  • 保持"一段计算"不要混入不同单位:
    • 要么都 logical
    • 要么都 physical
    • 中间转换一次,之后不再混用
  • 当你发现"只有部分同事电脑不对",优先怀疑:
    • Windows 缩放比(125%/150%)
    • 外接屏 DPI 不一致
    • 小数缩放被截断 / rounding 不一致

8. 回到 Coco:这段代码在做什么?哪里最容易踩雷?

Coco 当前实现(src-tauri/src/lib.rs:320-375)整体思路是合理的:

  • 用鼠标位置找 monitor(src-tauri/src/lib.rs:323-340
  • 用 monitor 的 position/size 计算居中(src-tauri/src/lib.rs:339-356
  • PhysicalPosition 设置窗口位置(src-tauri/src/lib.rs:358-360

但最危险的一行就是(src-tauri/src/lib.rs:352):

rust 复制代码
let window_height = 590 * scale_factor as i32;

它会在 scale_factor=1.25 等情况下直接截断,导致纵向居中在不同缩放比设备上差异巨大。


9. 结语

桌面应用 "看起来像网页",但窗口系统的像素体系远比网页复杂:DPI、缩放比、多显示器、物理/逻辑坐标任何一个点处理不一致,都会让 "同一份代码在不同人电脑上不一样"。

最靠谱的工程化做法只有一个:
统一坐标空间,明确转换边界,避免截断误差。

只要团队把这套约定固化下来,后续再做窗口居中、吸附、跟随鼠标、跨显示器移动,都会稳定很多。

开源

如果你对我们的技术细节感兴趣,或者想体验一下这款全能的生产力工具,欢迎访问我们的开源仓库和官网:

相关推荐
果壳~2 小时前
【前端】【canvas】图片颜色填充工具实现详解
前端
¥懒大王¥2 小时前
XSS-Game靶场教程
前端·安全·web安全·xss
ssshooter2 小时前
为什么移动端 safari 用 translate 移动元素卡卡的
前端·css·性能优化
闲云一鹤2 小时前
Claude Code 接入第三方AI模型(MiMo-V2-Flash)
前端·后端·claude
惜.己2 小时前
前端笔记(四)
前端·笔记
小北方城市网2 小时前
第 5 课:Vue 3 HTTP 请求与 UI 库实战 —— 从本地数据到前后端交互应用
大数据·前端·人工智能·ai·自然语言处理
踢球的打工仔2 小时前
ajax的基本使用(上传文件)
前端·javascript·ajax
樊小肆2 小时前
ollmam+langchain.js实现本地大模型简单记忆对话-内存版
前端·langchain·aigc
徐小夕2 小时前
pxcharts 多维表格开源!一款专为开发者和数据分析师打造的轻量化智能表格
前端·架构·github