NodifyEditor Zoom 机制分析
- [NodifyEditor Zoom 机制分析](#NodifyEditor Zoom 机制分析)
-
- 概述
- [1. 核心属性](#1. 核心属性)
- [2. Zoom 触发入口](#2. Zoom 触发入口)
-
- [2.1 入口点概览](#2.1 入口点概览)
- [2.2 鼠标滚轮缩放](#2.2 鼠标滚轮缩放)
- [2.3 ZoomIn / ZoomOut 命令](#2.3 ZoomIn / ZoomOut 命令)
- [2.4 FitToScreen 命令](#2.4 FitToScreen 命令)
- [3. Zoom 执行流程](#3. Zoom 执行流程)
-
- [3.1 核心方法: ZoomAtPosition](#3.1 核心方法: ZoomAtPosition)
- [3.2 流程图](#3.2 流程图)
- [3.3 焦点保持算法](#3.3 焦点保持算法)
- [4. 变换机制](#4. 变换机制)
-
- [4.1 变换组合](#4.1 变换组合)
- [4.2 变换属性](#4.2 变换属性)
- [4.3 OnViewportZoomChanged 回调](#4.3 OnViewportZoomChanged 回调)
- [5. 约束机制](#5. 约束机制)
-
- [5.1 缩放值约束](#5.1 缩放值约束)
- [5.2 最小/最大值约束](#5.2 最小/最大值约束)
- [5.3 约束链](#5.3 约束链)
- [6. 视口大小计算](#6. 视口大小计算)
-
- [6.1 计算公式](#6.1 计算公式)
- [6.2 关系说明](#6.2 关系说明)
- [7. 缩放因子计算](#7. 缩放因子计算)
-
- [7.1 鼠标滚轮](#7.1 鼠标滚轮)
- [7.2 ZoomIn / ZoomOut](#7.2 ZoomIn / ZoomOut)
- [7.3 FitToScreen](#7.3 FitToScreen)
- [8. 相关命令绑定](#8. 相关命令绑定)
- [9. 架构图](#9. 架构图)
- [10. 总结](#10. 总结)
NodifyEditor Zoom 机制分析
概述
NodifyEditor 的缩放(Zoom)机制基于 Avalonia 的变换系统实现,通过 ScaleTransform 和 TranslateTransform 的组合来实现平滑的缩放体验。
1. 核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ViewportZoom |
double | 1.0 | 当前缩放级别 |
MinViewportZoom |
double | 0.1 | 最小缩放级别 |
MaxViewportZoom |
double | 2.0 | 最大缩放级别 |
属性定义位置
csharp
// NodifyEditor.cs 第 35-37 行
public static readonly StyledProperty<double> ViewportZoomProperty =
AvaloniaProperty.Register<NodifyEditor, double>(nameof(ViewportZoom), 1.0,
defaultBindingMode: BindingMode.TwoWay,
coerce: ConstrainViewportZoomToRange);
public static readonly StyledProperty<double> MinViewportZoomProperty =
AvaloniaProperty.Register<NodifyEditor, double>(nameof(MinViewportZoom), 0.1d,
coerce: CoerceMinViewportZoom);
public static readonly StyledProperty<double> MaxViewportZoomProperty =
AvaloniaProperty.Register<NodifyEditor, double>(nameof(MaxViewportZoom), 2.0d,
coerce: CoerceMaxViewportZoom);
2. Zoom 触发入口
2.1 入口点概览
┌─────────────────────────────────────────────────────────────────┐
│ 用户触发缩放 │
├─────────────────────────────────────────────────────────────────┤
│ ① 鼠标滚轮 + 修饰键 (默认 Ctrl) │
│ → OnMouseWheel() → ZoomAtPosition(zoom, mousePos) │
│ │
│ ② ZoomIn() / ZoomOut() 命令 │
│ → 在视口中心放大/缩小 │
│ │
│ ③ FitToScreen() 命令 │
│ → 计算合适的 zoom 值使所有节点适应视口 │
│ │
│ ④ 直接设置 ViewportZoom 属性 │
│ → 绑定或代码设置 │
└─────────────────────────────────────────────────────────────────┘
2.2 鼠标滚轮缩放
文件位置 : NodifyEditor.Avalonia.cs
csharp
// 第 27-37 行
if (!e.Handled && EditorGestures.Mappings.Editor.ZoomModifierKey == e.KeyModifiers)
{
double zoom = Math.Pow(2.0, e.Delta.Y / 3.0 / MouseWheelDeltaForOneLine
* MouseWheelAvaloniaToWpfScale
* PointerTouchGestureMagnifyScale);
ZoomAtPosition(zoom, e.GetPosition(ItemsHost));
}
2.3 ZoomIn / ZoomOut 命令
文件位置 : NodifyEditor.cs 第 984-992 行
csharp
/// <summary>
/// Zoom in at the viewports center
/// </summary>
public void ZoomIn() => ZoomAtPosition(
Math.Pow(2.0, 120.0 / 3.0 / MouseWheelDeltaForOneLine),
(Point)((Vector)ViewportLocation + ViewportSize.ToVector() / 2));
/// <summary>
/// Zoom out at the viewports center
/// </summary>
public void ZoomOut() => ZoomAtPosition(
Math.Pow(2.0, -120.0 / 3.0 / MouseWheelDeltaForOneLine),
(Point)((Vector)ViewportLocation + ViewportSize.ToVector() / 2));
2.4 FitToScreen 命令
文件位置 : NodifyEditor.cs 第 1071-1087 行
csharp
public void FitToScreen(Rect? area = null)
{
Rect extent = area ?? ItemsExtent;
extent = extent.Inflate(FitToScreenExtentMargin);
if (extent.Width > 0 && extent.Height > 0)
{
double widthRatio = ViewportSize.Width / extent.Width;
double heightRatio = ViewportSize.Height / extent.Height;
double zoom = Math.Min(widthRatio, heightRatio);
var center = new Point(extent.X + extent.Width / 2, extent.Y + extent.Height / 2);
ZoomAtPosition(zoom, center);
BringIntoView(center, animated: false);
}
}
3. Zoom 执行流程
3.1 核心方法: ZoomAtPosition
文件位置 : NodifyEditor.cs 第 999-1018 行
csharp
public void ZoomAtPosition(double zoom, Point location)
{
if (!DisableZooming)
{
double prevZoom = ViewportZoom;
ViewportZoom *= zoom;
if (Math.Abs(prevZoom - ViewportZoom) > 0.0000001)
{
// 获取实际缩放值(因为 Zoom 可能被 Coerce)
zoom = ViewportZoom / prevZoom;
Vector position = (Vector)location;
var dist = position - (Vector)ViewportLocation;
var zoomedDist = dist * zoom;
var diff = zoomedDist - dist;
ViewportLocation += diff / zoom;
}
}
}
3.2 流程图
ZoomAtPosition(zoom, location)
│
▼
┌─────────────────────────────────────┐
│ 1. 检查 DisableZooming │
│ 若禁用则直接返回 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 2. 计算新缩放值 │
│ ViewportZoom *= zoom │
│ (自动被 Coerce 到 [Min, Max] 范围) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 3. 调整视口位置 (保持焦点不变) │
│ • 计算 location 到视口的距离 │
│ • 根据缩放比例调整视口位置 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 4. OnViewportZoomChanged 触发 │
│ • 创建新 ScaleTransform │
│ • 更新 ViewportSize │
│ • 更新 ViewportTransform │
│ • 触发 OnViewportUpdated │
└─────────────────────────────────────┘
3.3 焦点保持算法
缩放时保持焦点位置不变的数学原理:
设:
- location: 缩放焦点位置(图空间坐标)
- ViewportLocation: 视口左上角位置
- zoom: 缩放因子
则:
dist = location - ViewportLocation // 焦点到视口左上角的距离
zoomedDist = dist * zoom // 缩放后的距离
diff = zoomedDist - dist // 距离变化量
新 ViewportLocation = 旧 ViewportLocation + diff / zoom
4. 变换机制
4.1 变换组合
文件位置 : NodifyEditor.cs 第 50-63 行
csharp
private static void UpdateViewportTransform(NodifyEditor editor)
{
var transform = new TransformGroup();
transform.Children.Add(editor.ScaleTransform); // 先缩放
transform.Children.Add(editor.TranslateTransform); // 后平移
editor.SetCurrentValue(ViewportTransformProperty, transform);
// DPI 缩放版本
var dpiScaledTransform = new TransformGroup();
dpiScaledTransform.Children.Add(editor.ScaleTransform);
dpiScaledTransform.Children.Add(editor.DpiScaledTranslateTransform);
editor.SetCurrentValue(DpiScaledViewportTransformProperty, dpiScaledTransform);
}
4.2 变换属性
| 属性 | 类型 | 用途 |
|---|---|---|
ScaleTransform |
ScaleTransform | 应用缩放变换 |
TranslateTransform |
TranslateTransform | 应用平移变换 |
ViewportTransform |
Transform | 组合变换(应用到子元素) |
DpiScaledViewportTransform |
Transform | DPI 缩放版本(高 DPI 屏幕) |
4.3 OnViewportZoomChanged 回调
文件位置 : NodifyEditor.cs 第 91-107 行
csharp
private static void OnViewportZoomChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var editor = (NodifyEditor)d;
double zoom = (double)e.NewValue;
// 创建新的 ScaleTransform(Avalonia 的 workaround)
editor.ScaleTransform = new();
editor.ScaleTransform.ScaleX = zoom;
editor.ScaleTransform.ScaleY = zoom;
// 更新视口大小
editor.ViewportSize = new Size(editor.Bounds.Width / zoom, editor.Bounds.Height / zoom);
// 更新变换
UpdateViewportTransform(editor);
// 触发视口更新事件
editor.OnViewportUpdated();
}
5. 约束机制
5.1 缩放值约束
文件位置 : NodifyEditor.cs 第 133-146 行
csharp
private static double ConstrainViewportZoomToRange(DependencyObject d, double value)
{
var editor = (NodifyEditor)d;
var num = (double)value;
double minimum = editor.MinViewportZoom;
if (num < minimum)
{
return minimum;
}
double maximum = editor.MaxViewportZoom;
return num > maximum ? maximum : value;
}
5.2 最小/最大值约束
csharp
// MinViewportZoom 最小值为 0.1
private static double CoerceMinViewportZoom(DependencyObject d, double value)
=> (double)value > 0.1d ? value : 0.1d;
// MaxViewportZoom 不能小于 MinViewportZoom
private static double CoerceMaxViewportZoom(DependencyObject d, double value)
{
var editor = (NodifyEditor)d;
double min = editor.MinViewportZoom;
return (double)value < min ? min : value;
}
5.3 约束链
设置 MinViewportZoom
│
▼
┌─────────────────────────────────┐
│ CoerceMaxViewportZoom │
│ 确保 Max >= Min │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ CoerceValue(ViewportZoom) │
│ 确保 ViewportZoom 在有效范围内 │
└─────────────────────────────────┘
6. 视口大小计算
6.1 计算公式
csharp
ViewportSize = new Size(Bounds.Width / zoom, Bounds.Height / zoom);
6.2 关系说明
| Zoom 值 | 效果 | ViewportSize |
|---|---|---|
| > 1.0 | 放大 | 变小(看到的图空间范围小) |
| = 1.0 | 正常 | 等于控件大小 |
| < 1.0 | 缩小 | 变大(看到的图空间范围大) |
7. 缩放因子计算
7.1 鼠标滚轮
csharp
double zoom = Math.Pow(2.0, e.Delta.Y / 3.0 / 120);
// 滚轮滚动 120 单位 ≈ 1.26x 缩放
7.2 ZoomIn / ZoomOut
csharp
double zoom = Math.Pow(2.0, ±120.0 / 3.0 / 120); // ≈ 1.26x
7.3 FitToScreen
csharp
double zoom = Math.Min(
ViewportSize.Width / extent.Width,
ViewportSize.Height / extent.Height
);
8. 相关命令绑定
文件位置 : EditorCommands.cs
| 命令 | 触发方法 | 可执行条件 |
|---|---|---|
ZoomIn |
editor.ZoomIn() |
ViewportZoom < MaxViewportZoom |
ZoomOut |
editor.ZoomOut() |
ViewportZoom > MinViewportZoom |
FitToScreen |
editor.FitToScreen() |
始终可执行 |
9. 架构图
┌─────────────────────────────────────────────────────────────┐
│ NodifyEditor │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────────────────────┐│
│ │ Zoom 入口 │ │ 核心属性 ││
│ │ │ │ • ViewportZoom ││
│ │ • MouseWheel │ │ • MinViewportZoom ││
│ │ • ZoomIn() │ │ • MaxViewportZoom ││
│ │ • ZoomOut() │ │ • ViewportLocation ││
│ │ • FitToScreen()│ │ • ViewportSize ││
│ └────────┬────────┘ └─────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ZoomAtPosition(zoom, location) ││
│ │ • 应用缩放因子 ││
│ │ • 调整视口位置保持焦点 ││
│ └─────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ OnViewportZoomChanged ││
│ │ • 更新 ScaleTransform ││
│ │ • 更新 ViewportSize ││
│ │ • 更新 ViewportTransform ││
│ └─────────────────────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ViewportTransform ││
│ │ = ScaleTransform + TranslateTransform ││
│ │ ││
│ │ 应用到 ItemsHost (所有子元素) ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
10. 总结
NodifyEditor 的 Zoom 机制具有以下特点:
- 属性驱动 : 通过
ViewportZoom属性的变化触发整个更新流程 - 焦点保持: 缩放时通过数学计算保持鼠标焦点位置不变
- 值约束 : 自动将缩放值约束在
[MinViewportZoom, MaxViewportZoom]范围内 - 变换组合 : 使用
ScaleTransform+TranslateTransform组合实现视口变换 - 多入口支持: 支持鼠标滚轮、命令、代码等多种触发方式