2025-12-23 21:35:54 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Windows;
|
|
|
|
|
|
using System.Windows.Controls;
|
|
|
|
|
|
using System.Windows.Media;
|
|
|
|
|
|
using System.Windows.Media.Animation;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
using System.Windows.Threading;
|
2026-01-02 17:30:41 +08:00
|
|
|
|
using Melskin.Assists;
|
|
|
|
|
|
using Melskin.Extensions;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2026-01-02 17:30:41 +08:00
|
|
|
|
namespace Melskin.Controls;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// 锚点
|
2025-08-12 23:08:54 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
[TemplatePart(Name = "PART_AnchorList", Type = typeof(ListBox))]
|
|
|
|
|
|
[TemplatePart(Name = "PART_ContentScroller", Type = typeof(ScrollViewer))]
|
|
|
|
|
|
public class Anchor : ContentControl
|
|
|
|
|
|
{
|
|
|
|
|
|
#region 私有字段
|
|
|
|
|
|
|
2025-08-20 12:10:35 +08:00
|
|
|
|
private ListBox? anchorList;
|
|
|
|
|
|
private ScrollViewer? contentScroller;
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 扫描防抖
|
2025-08-20 12:10:35 +08:00
|
|
|
|
private DispatcherTimer? scanDebounceTimer;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 缓存锚点映射
|
2025-08-20 12:10:35 +08:00
|
|
|
|
private readonly Dictionary<string, UIElement?> anchorElements = new();
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 标志位:是否正在通过代码设置选中项(防止触发滚动)
|
|
|
|
|
|
private bool isInternalSelectionChange;
|
|
|
|
|
|
|
|
|
|
|
|
// 标志位:是否正在执行点击导航导致的自动滚动(防止滚动时频繁更新高亮)
|
|
|
|
|
|
private bool isAutoScrolling;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 动画中介器(复用实例以便取消动画)
|
|
|
|
|
|
private readonly ScrollViewerOffsetMediator mediator = new();
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
|
|
|
|
|
static Anchor()
|
|
|
|
|
|
{
|
|
|
|
|
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(Anchor), new FrameworkPropertyMetadata(typeof(Anchor)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// <inheritdoc />
|
2025-08-12 23:08:54 +08:00
|
|
|
|
public override void OnApplyTemplate()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnApplyTemplate();
|
|
|
|
|
|
UnbindEvents();
|
|
|
|
|
|
scanDebounceTimer?.Stop();
|
|
|
|
|
|
|
2025-08-24 13:49:55 +08:00
|
|
|
|
anchorList = GetTemplateChild("PART_AnchorList") as ListBox;
|
|
|
|
|
|
contentScroller = GetTemplateChild("PART_ContentScroller") as ScrollViewer;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
|
|
|
|
|
if (anchorList != null && contentScroller != null)
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 初始化中介器关联
|
|
|
|
|
|
mediator.ScrollViewer = contentScroller;
|
|
|
|
|
|
|
2025-08-12 23:08:54 +08:00
|
|
|
|
BindEvents();
|
|
|
|
|
|
InitializeScanTimer();
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 延迟一点以等待布局完成
|
2025-08-12 23:08:54 +08:00
|
|
|
|
contentScroller.Loaded += RequestScan;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UnbindEvents()
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
if (anchorList != null) anchorList.SelectionChanged -= AnchorList_SelectionChanged;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
if (contentScroller != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
contentScroller.ScrollChanged -= ContentScroller_ScrollChanged;
|
|
|
|
|
|
contentScroller.SizeChanged -= RequestScan;
|
2025-12-23 21:35:54 +08:00
|
|
|
|
if (contentScroller.Content is FrameworkElement fe) fe.LayoutUpdated -= RequestScan;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void BindEvents()
|
|
|
|
|
|
{
|
2025-08-20 12:10:35 +08:00
|
|
|
|
if (anchorList != null) anchorList.SelectionChanged += AnchorList_SelectionChanged;
|
|
|
|
|
|
if (contentScroller == null) return;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
contentScroller.ScrollChanged += ContentScroller_ScrollChanged;
|
|
|
|
|
|
contentScroller.SizeChanged += RequestScan;
|
2025-12-23 21:35:54 +08:00
|
|
|
|
if (contentScroller.Content is FrameworkElement fe) fe.LayoutUpdated += RequestScan;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void InitializeScanTimer()
|
|
|
|
|
|
{
|
|
|
|
|
|
scanDebounceTimer = new DispatcherTimer(DispatcherPriority.ContextIdle, Dispatcher)
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
Interval = TimeSpan.FromMilliseconds(150)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
};
|
2025-08-20 12:10:13 +08:00
|
|
|
|
scanDebounceTimer.Tick += (_, _) =>
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
|
|
|
|
|
scanDebounceTimer.Stop();
|
|
|
|
|
|
ScanAnchors();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
#region 逻辑处理
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-10-04 08:52:23 +08:00
|
|
|
|
private void RequestScan(object? sender, EventArgs e)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
|
|
|
|
|
scanDebounceTimer?.Stop();
|
|
|
|
|
|
scanDebounceTimer?.Start();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ScanAnchors()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (contentScroller == null || anchorList == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
anchorElements.Clear();
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 查找所有锚点
|
2026-01-02 17:30:41 +08:00
|
|
|
|
// 注意:这里假设 Melskin.Extensions 的 FindVisualChildren 工作正常
|
2025-12-23 21:35:54 +08:00
|
|
|
|
var anchorUiElements = contentScroller.FindVisualChildren<UIElement>()
|
2025-08-20 12:10:35 +08:00
|
|
|
|
.Where(el => VisualTreeHelper.GetParent(el) != null &&
|
2025-12-23 21:35:54 +08:00
|
|
|
|
!string.IsNullOrEmpty(ControlAssist.GetAnchorHeaderText(el)))
|
2025-08-12 23:08:54 +08:00
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var element in anchorUiElements)
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
anchorElements[ControlAssist.GetAnchorHeaderText(element)] = element;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var newNames = anchorElements.Keys.ToList();
|
|
|
|
|
|
var currentNames = anchorList.ItemsSource?.Cast<string>().ToList() ?? new List<string>();
|
|
|
|
|
|
|
|
|
|
|
|
if (!currentNames.SequenceEqual(newNames))
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 保持当前选中的项(如果在列表中还存在)
|
|
|
|
|
|
var oldSelection = anchorList.SelectedItem;
|
|
|
|
|
|
|
|
|
|
|
|
isInternalSelectionChange = true;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
anchorList.ItemsSource = newNames;
|
2025-12-23 21:35:54 +08:00
|
|
|
|
|
|
|
|
|
|
if (oldSelection != null && newNames.Contains(oldSelection))
|
|
|
|
|
|
{
|
|
|
|
|
|
anchorList.SelectedItem = oldSelection;
|
|
|
|
|
|
}
|
|
|
|
|
|
isInternalSelectionChange = false;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 初始扫描后更新一次高亮
|
2025-08-12 23:08:54 +08:00
|
|
|
|
UpdateHighlight();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// 用户点击列表项触发
|
2025-08-12 23:08:54 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void AnchorList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 如果是代码设置的选中(如滚动触发的高亮更新),则不执行滚动逻辑,防止死循环
|
|
|
|
|
|
if (isInternalSelectionChange) return;
|
|
|
|
|
|
if (e.AddedItems.Count == 0) return;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
if (e.AddedItems[0] is string selectedAnchorName &&
|
|
|
|
|
|
anchorElements.TryGetValue(selectedAnchorName, out var targetElement))
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 标记为自动滚动,暂停高亮更新
|
|
|
|
|
|
isAutoScrolling = true;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
ScrollToElement(targetElement);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-24 13:49:55 +08:00
|
|
|
|
private void ScrollToElement(UIElement? targetElement)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
if (contentScroller == null || targetElement == null) return;
|
|
|
|
|
|
|
2025-08-12 23:08:54 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 获取目标相对位置
|
|
|
|
|
|
var transform = targetElement.TransformToVisual(contentScroller);
|
2025-08-12 23:08:54 +08:00
|
|
|
|
var position = transform.Transform(new Point(0, 0));
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 目标 offset = 当前 offset + 相对位置
|
|
|
|
|
|
var currentOffset = contentScroller.VerticalOffset;
|
|
|
|
|
|
var targetOffset = currentOffset + position.Y;
|
|
|
|
|
|
|
|
|
|
|
|
// 边界检查
|
|
|
|
|
|
if (targetOffset < 0) targetOffset = 0;
|
|
|
|
|
|
if (targetOffset > contentScroller.ScrollableHeight) targetOffset = contentScroller.ScrollableHeight;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果距离很短,直接跳过去,不动画(可选优化)
|
|
|
|
|
|
if (Math.Abs(targetOffset - currentOffset) < 2.0)
|
|
|
|
|
|
{
|
|
|
|
|
|
contentScroller.ScrollToVerticalOffset(targetOffset);
|
|
|
|
|
|
isAutoScrolling = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 停止旧动画
|
2025-08-12 23:08:54 +08:00
|
|
|
|
mediator.BeginAnimation(ScrollViewerOffsetMediator.VerticalOffsetProperty, null);
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 确保 Mediator 的起始值与当前 ScrollViewer 一致
|
|
|
|
|
|
// 这一步很重要,否则动画可能会从上一次动画结束的位置开始跳变
|
|
|
|
|
|
mediator.VerticalOffset = currentOffset;
|
|
|
|
|
|
|
2025-08-12 23:08:54 +08:00
|
|
|
|
var animation = new DoubleAnimation(
|
2025-12-23 21:35:54 +08:00
|
|
|
|
currentOffset,
|
2025-08-20 12:10:35 +08:00
|
|
|
|
targetOffset,
|
2025-12-23 21:35:54 +08:00
|
|
|
|
new Duration(TimeSpan.FromMilliseconds(400))) // 建议400-500ms
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
|
|
|
|
|
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-08-20 12:10:13 +08:00
|
|
|
|
animation.Completed += (_, _) =>
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
isAutoScrolling = false;
|
|
|
|
|
|
// 动画结束后强制校准一次高亮,防止浮点误差导致的选中项错误
|
|
|
|
|
|
UpdateHighlight();
|
2025-08-12 23:08:54 +08:00
|
|
|
|
};
|
2025-08-20 12:10:35 +08:00
|
|
|
|
|
2025-08-12 23:08:54 +08:00
|
|
|
|
mediator.BeginAnimation(ScrollViewerOffsetMediator.VerticalOffsetProperty, animation);
|
|
|
|
|
|
}
|
2025-12-23 21:35:54 +08:00
|
|
|
|
catch (Exception)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
isAutoScrolling = false;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ContentScroller_ScrollChanged(object sender, ScrollChangedEventArgs e)
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 如果是点击导航导致的滚动,不要更新高亮,以免列表项乱跳
|
|
|
|
|
|
if (isAutoScrolling) return;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 只有垂直滚动才处理
|
|
|
|
|
|
if (Math.Abs(e.VerticalChange) > 0)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
|
|
|
|
|
UpdateHighlight();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateHighlight()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (anchorElements.Count == 0 || anchorList == null || contentScroller == null) return;
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 获取 Viewport 的信息
|
|
|
|
|
|
//double viewportTop = 0;
|
|
|
|
|
|
//double viewportHeight = contentScroller.ViewportHeight;
|
|
|
|
|
|
double scrollOffset = contentScroller.VerticalOffset;
|
|
|
|
|
|
double scrollHeight = contentScroller.ScrollableHeight;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
string? bestMatch = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 策略1:如果已经滚动到底部,直接选中最后一个
|
|
|
|
|
|
// 使用一个小的容差 (1.0)
|
|
|
|
|
|
if (scrollOffset >= scrollHeight - 1.0 && scrollHeight > 0)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
bestMatch = anchorElements.Keys.LastOrDefault();
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 策略2:寻找当前视口中最靠上的元素
|
|
|
|
|
|
// 也就是:Top 值 <= 某个阈值(比如 ViewportHeight 的 1/3 或一个固定像素值)的最后一个元素
|
|
|
|
|
|
|
|
|
|
|
|
// 我们寻找所有 "Top < 20px" 的元素,取其中位置最大的一个(最靠近顶部的)
|
|
|
|
|
|
// 解释:如果元素A的Top是 -100,元素B的Top是 -10,元素C的Top是 50。
|
|
|
|
|
|
// 我们认为A和B都已经滚过去了,B是当前“激活”的块。
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
double threshold = 20.0; // 20像素的宽容度,解决刚好停在边缘的问题
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var kvp in anchorElements)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
var element = kvp.Value;
|
|
|
|
|
|
if (element == null || !element.IsVisible) continue;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 获取元素相对于 ScrollViewer 可视区域左上角的坐标
|
|
|
|
|
|
var transform = element.TransformToVisual(contentScroller);
|
|
|
|
|
|
var position = transform.Transform(new Point(0, 0));
|
|
|
|
|
|
var top = position.Y;
|
|
|
|
|
|
|
|
|
|
|
|
// 核心逻辑:
|
|
|
|
|
|
// 只要元素的 Top 小于等于 阈值,它就是候选者。
|
|
|
|
|
|
// 因为字典通常是按顺序扫描生成的,后面的元素会覆盖前面的 bestMatch
|
|
|
|
|
|
if (top <= threshold)
|
|
|
|
|
|
{
|
|
|
|
|
|
bestMatch = kvp.Key;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 因为是顺序排列,一旦遇到 Top > threshold 的,说明后面的都在视口下面了,可以直接停止
|
|
|
|
|
|
// 除非你的布局不是垂直线性的,否则这样效率最高
|
|
|
|
|
|
break;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 忽略异常
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 如果没有任何元素满足条件(比如第一个元素都在下面),则选中第一个
|
|
|
|
|
|
if (bestMatch == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
bestMatch = anchorElements.Keys.FirstOrDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 更新 ListBox 选中项
|
|
|
|
|
|
if (bestMatch != null && (string?)anchorList.SelectedItem != bestMatch)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
// 标记为内部变更,防止触发 AnchorList_SelectionChanged 里的 ScrollToElement
|
|
|
|
|
|
isInternalSelectionChange = true;
|
|
|
|
|
|
anchorList.SelectedItem = bestMatch;
|
|
|
|
|
|
anchorList.ScrollIntoView(bestMatch); // 确保左侧列表也跟随滚动
|
|
|
|
|
|
isInternalSelectionChange = false;
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-12-23 21:35:54 +08:00
|
|
|
|
/// 辅助类保持不变,但建议确保它能复用
|
2025-08-12 23:08:54 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
internal class ScrollViewerOffsetMediator : Animatable
|
|
|
|
|
|
{
|
2025-08-20 12:10:35 +08:00
|
|
|
|
public ScrollViewer? ScrollViewer
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
|
|
|
|
|
get => (ScrollViewer)GetValue(ScrollViewerProperty);
|
|
|
|
|
|
set => SetValue(ScrollViewerProperty, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty ScrollViewerProperty =
|
2025-12-23 21:35:54 +08:00
|
|
|
|
DependencyProperty.Register(nameof(ScrollViewer), typeof(ScrollViewer), typeof(ScrollViewerOffsetMediator), new PropertyMetadata(null));
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
|
|
|
|
|
public double VerticalOffset
|
|
|
|
|
|
{
|
|
|
|
|
|
get => (double)GetValue(VerticalOffsetProperty);
|
|
|
|
|
|
set => SetValue(VerticalOffsetProperty, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static readonly DependencyProperty VerticalOffsetProperty =
|
2025-12-23 21:35:54 +08:00
|
|
|
|
DependencyProperty.Register(nameof(VerticalOffset), typeof(double), typeof(ScrollViewerOffsetMediator), new PropertyMetadata(0.0, OnVerticalOffsetChanged));
|
2025-08-12 23:08:54 +08:00
|
|
|
|
|
|
|
|
|
|
private static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
|
|
|
{
|
2025-12-23 21:35:54 +08:00
|
|
|
|
if (d is ScrollViewerOffsetMediator mediator && mediator.ScrollViewer != null)
|
2025-08-12 23:08:54 +08:00
|
|
|
|
{
|
2025-08-20 12:10:35 +08:00
|
|
|
|
mediator.ScrollViewer.ScrollToVerticalOffset((double)e.NewValue);
|
|
|
|
|
|
}
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-23 21:35:54 +08:00
|
|
|
|
protected override Freezable CreateInstanceCore() => new ScrollViewerOffsetMediator();
|
2025-08-12 23:08:54 +08:00
|
|
|
|
}
|