VSTO Outlook插件高DPI启动时GetWindowRect返回错误坐标问题
Outlook VSTO插件DPI缩放异常问题
我正在开发一款带窗口覆盖层的Outlook VSTO插件,UI基于WPF构建。遇到的问题是:
- 当系统缩放比例大于100%时启动插件,调用
GetWindowRect获取Outlook窗口矩形来附着WPF窗口(匹配Left/Top/Width/Height),得到的矩形值是错误的。 - 但如果以100%缩放比例启动插件,之后运行时修改任意缩放比例,所有功能都正常,且具备DPI感知能力。
- 两种场景下,
GetDpiForWindow都能返回正确的DPI值,这一点非常奇怪。 - 我在创建窗体时通过
SetThreadDpiAwareness设置了DPI感知,实在找不到问题根源,求各位指点。
相关代码
附着窗口代码
public void AttachTo(IntPtr src, AttachFlagEnum flags) { var nativeRectangle = new WinAPI.RECT(); if (!WinAPI.GetWindowRect(src, ref nativeRectangle)) { // throw new Win32Exception(Marshal.GetLastWin32Error()); return; } AttachToCoords(new Rectangle(nativeRectangle.Left, nativeRectangle.Top, nativeRectangle.Right - nativeRectangle.Left, nativeRectangle.Bottom - nativeRectangle.Top), flags); }
窗体创建代码
private void ThisAddIn_Startup(object sender, System.EventArgs e) { StateManager.Init(); OutlookUtils.WaitOutlookLoading(); using (var ctx = new DPIContextBlock(WinAPI.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)) { new Forms.One().Show(); new Forms.Overlay().Show(); new Forms.Two(); } }
覆盖层定时附着代码
private void OverlayThink(object ob) { if (Managers.StateManager.OutlookState == OutlookStateEnum.MINIMIZED || Managers.StateManager.UiState == UIStateEnum.DESCWND) { if (this.IsVisible) { this.Dispatcher.Invoke(() => this.Hide()); } return; } this.Dispatcher.Invoke(() => this.AttachTo(Utils.OutlookUtils.GetWordWindow(), AttachFlagEnum.OVERLAY)); this.Dispatcher.Invoke(() => this.Show()); }
解决方案建议
- 调整DPI感知设置时机
不要用临时上下文块包裹窗体创建逻辑,直接在插件启动初期全局设置进程级DPI感知,确保线程DPI状态在调用GetWindowRect前已生效:
private void ThisAddIn_Startup(object sender, System.EventArgs e) { // 先设置进程级Per-Monitor DPI感知 WinAPI.SetProcessDpiAwarenessContext(WinAPI.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); StateManager.Init(); OutlookUtils.WaitOutlookLoading(); new Forms.One().Show(); new Forms.Overlay().Show(); new Forms.Two(); }
- 手动转换矩形的DPI适配值
当进程DPI状态与窗口所在显示器不匹配时,GetWindowRect返回的是虚拟像素值,需要用GetDpiForWindow获取的DPI手动转换为物理像素:
public void AttachTo(IntPtr src, AttachFlagEnum flags) { var nativeRectangle = new WinAPI.RECT(); if (!WinAPI.GetWindowRect(src, ref nativeRectangle)) { return; } uint dpi = WinAPI.GetDpiForWindow(src); double scaleFactor = dpi / 96.0; int left = (int)(nativeRectangle.Left * scaleFactor); int top = (int)(nativeRectangle.Top * scaleFactor); int width = (int)((nativeRectangle.Right - nativeRectangle.Left) * scaleFactor); int height = (int)((nativeRectangle.Bottom - nativeRectangle.Top) * scaleFactor); AttachToCoords(new Rectangle(left, top, width, height), flags); }
- 开启WPF窗口的Per-Monitor DPI感知
在WPF窗口构造函数中显式启用Per-Monitor DPI支持:
public Overlay() { InitializeComponent(); // 启用Per-Monitor DPI感知 System.Windows.Media.VisualTreeHelper.SetDpiAwarenessContext(this, WinAPI.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); this.UseLayoutRounding = true; }
也可以在插件的配置文件中添加全局设置:
<application xmlns="urn:schemas-microsoft-com:asm.v3"> <windowsSettings> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware> </windowsSettings> </application>
- 确保在Outlook完全加载后操作窗口
替换OutlookUtils.WaitOutlookLoading(),改用监听Outlook的StartupComplete事件,在事件触发后再创建和附着窗口,避免在Outlook未完成DPI适配时获取窗口矩形。
内容的提问来源于stack exchange,提问作者UniqueUlysees
相关产品推荐
相关产品推荐

