WPF 自定义 ZoomBox 控件:实现缩略图同步、缩放调节与拖拽定位
实现 WPF 应用中画布的缩放控制与缩略图导航,支持滑块调节缩放比例、缩略图拖拽定位,实时同步主画布视图与缩略图视口位置。
-
缩放控制:通过
Slider值变化计算缩放比例,同步更新主画布ScaleTransform,并调整ScrollViewer偏移,确保缩放中心对齐视图中心。 -
缩略图同步: - 主画布
LayoutUpdated时,计算缩略图缩放比例,更新Thumb(视口)的大小与位置; - 拖拽Thumb时,反向计算主画布ScrollViewer的偏移量,实现快速定位。 -
依赖注入:通过
ScrollViewer依赖属性,建立自定义控件与主画布的关联,解耦控件与业务画布。
关键点
-
VisualBrush/RenderTargetBitmap:两种方式生成主画布缩略图(XAML 用VisualBrush实时同步,代码中CreateThumbnail方法用RenderTargetBitmap静态渲染)。 -
模板绑定:重写
OnApplyTemplate获取模板内命名元素(PART_前缀),确保控件结构合法性。 -
坐标换算:通过主画布与缩略图的尺寸比例,实现主视图偏移量与缩略图视口位置的双向映射。
添加用户控件 ZoomBoxView.xaml
点击查看代码
<UserControl
x:Class="WpfMiniaturesDemo.ZoomBoxView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfMiniaturesDemo"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<Border
MinHeight="30"
Background="#F6F6F6"
CornerRadius="6">
<Border.Effect>
<DropShadowEffect
BlurRadius="3"
ShadowDepth="1"
Color="#E8E8E8" />
</Border.Effect>
<!-- 可展开的区域 默认关闭 -->
<Expander Background="Transparent" IsExpanded="False">
<Border Height="220" Background="White">
<!-- Canvas画布,,并设置边距 -->
<Canvas Name="PART_ZoomCanvas">
<Canvas.Background>
<VisualBrush Stretch="Uniform" Visual="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:ZoomBoxView}}, Path=ScrollViewer.Content}" />
</Canvas.Background>
<!-- Thumb控件,并设置鼠标样式 -->
<Thumb Name="PART_ZoomThumb" Cursor="SizeAll">
<Thumb.Style>
<Style TargetType="Thumb">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<!-- 设置Thumb控件的样式矩形 -->
<Rectangle
Fill="Transparent"
Stroke="Black"
StrokeThickness="1" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Thumb.Style>
</Thumb>
</Canvas>
</Border>
<!-- 设置可展开区域的头部 -->
<Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- 滑块 -->
<Slider
Name="PART_ZoomSlider"
MinWidth="110"
Margin="0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsMoveToPointEnabled="False"
IsSnapToTickEnabled="True"
Maximum="150"
Minimum="80"
Ticks="80,85,90,95,100,105,110,115,120,125,130,135,140,145,150"
Value="100" />
<!-- 绑定到滑块的值 -->
<TextBlock
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{Binding ElementName=PART_ZoomSlider, Path=Value}" />
<!-- 显示百分号 -->
<TextBlock
Grid.Column="1"
Margin="1,0,-10,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="%" />
</Grid>
</Expander.Header>
</Expander>
</Border>
</UserControl>
点击查看代码
/// <summary>
/// ZoomBoxView.xaml 的交互逻辑
/// </summary>
public partial class ZoomBoxView : UserControl
{
public ZoomBoxView()
{
InitializeComponent();
}
#region 控件成员变量定义
/// <summary>
/// 滑块控件
/// </summary>
private Thumb zoomThumb;
/// <summary>
/// 缩略图画布
/// </summary>
private Canvas zoomCanvas;
/// <summary>
/// 滑条控件
/// </summary>
private Slider zoomSlider;
/// <summary>
/// 缩放变换
/// </summary>
private ScaleTransform scaleTransform;
/// <summary>
/// 原始画布
/// </summary>
private Canvas designerCanvas;
#endregion
#region ScrollViewer的依赖属性定义 public ScrollViewer ScrollViewer
/// <summary>
/// ScrollViewer的依赖属性定义
/// </summary>
public ScrollViewer ScrollViewer
{
get { return (ScrollViewer)GetValue(ScrollViewerProperty); }
set { SetValue(ScrollViewerProperty, value); }
}
/// <summary>
/// ScrollViewer依赖属性的注册
/// </summary>
public static readonly DependencyProperty ScrollViewerProperty =
DependencyProperty.Register("ScrollViewer", typeof(ScrollViewer), typeof(ZoomBoxView));
#endregion
#region 重构OnApplyTemplate方法 public override void OnApplyTemplate()
/// <summary>
/// 重构OnApplyTemplate方法, Template 属性发生变化时
/// </summary>
/// <exception cref="Exception"></exception>
public override void OnApplyTemplate()
{
try
{
base.OnApplyTemplate();
// 确保ScrollViewer不为空
if (this.ScrollViewer == null)
return;
// 尝试获取ScrollViewer的内容作为Canvas
this.designerCanvas = this.ScrollViewer.Content as Canvas;
if (this.designerCanvas == null)
throw new Exception("Canvas must not be null!");
// 获取模板中的控件
this.zoomThumb = this.PART_ZoomThumb;
if (this.zoomThumb == null)
throw new Exception("PART_ZoomThumb template is missing!");
this.zoomCanvas = this.PART_ZoomCanvas;
if (this.zoomCanvas == null)
throw new Exception("PART_ZoomCanvas template is missing!");
this.zoomSlider = this.PART_ZoomSlider;
if (this.zoomSlider == null)
throw new Exception("PART_ZoomSlider template is missing!");
// 监听相关事件
this.designerCanvas.LayoutUpdated += new EventHandler(this.DesignerCanvas_LayoutUpdated);
this.zoomThumb.DragDelta += new DragDeltaEventHandler(this.Thumb_DragDelta);
this.zoomSlider.ValueChanged += new RoutedPropertyChangedEventHandler<double>(this.ZoomSlider_ValueChanged);
// 初始化缩放变换
this.scaleTransform = new ScaleTransform();
this.designerCanvas.LayoutTransform = this.scaleTransform;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
#endregion
#region 缩放比例,滑条值变化时的处理函数 private void ZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
/// <summary>
/// 滑块值变化时的处理函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
try
{
if (e.OldValue == 0) return; // 避免除以0的情况
// 计算新的缩放比例
double scale = e.NewValue / e.OldValue;
// 计算并设置新的视口偏移
double halfViewportHeight = this.ScrollViewer.ViewportHeight / 2;
double newVerticalOffset = ((this.ScrollViewer.VerticalOffset + halfViewportHeight) * scale - halfViewportHeight);
double halfViewportWidth = this.ScrollViewer.ViewportWidth / 2;
double newHorizontalOffset = ((this.ScrollViewer.HorizontalOffset + halfViewportWidth) * scale - halfViewportWidth);
// 应用新的缩放比例
this.scaleTransform.ScaleX *= scale;
this.scaleTransform.ScaleY *= scale;
// 滚动到新的偏移位置
this.ScrollViewer.ScrollToHorizontalOffset(newHorizontalOffset);
this.ScrollViewer.ScrollToVerticalOffset(newVerticalOffset);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
#endregion
#region 缩略图拖动及布局更新时的处理函数
/// <summary>
/// 缩略图拖动时的处理函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
{
try
{
double scale, xOffset, yOffset;
this.InvalidateScale(out scale, out xOffset, out yOffset);
// 根据缩放比例和拖动量计算新的滚动偏移
this.ScrollViewer.ScrollToHorizontalOffset(this.ScrollViewer.HorizontalOffset + e.HorizontalChange / scale);
this.ScrollViewer.ScrollToVerticalOffset(this.ScrollViewer.VerticalOffset + e.VerticalChange / scale);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
/// <summary>
/// Canvas布局更新时的处理函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void DesignerCanvas_LayoutUpdated(object sender, EventArgs e)
{
try
{
double scale, xOffset, yOffset;
this.InvalidateScale(out scale, out xOffset, out yOffset);
// 根据缩放比例和偏移量更新缩略图的位置和大小
this.zoomThumb.Width = this.ScrollViewer.ViewportWidth * scale;
this.zoomThumb.Height = this.ScrollViewer.ViewportHeight * scale;
Canvas.SetLeft(this.zoomThumb, xOffset + this.ScrollViewer.HorizontalOffset * scale);
Canvas.SetTop(this.zoomThumb, yOffset + this.ScrollViewer.VerticalOffset * scale);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
#endregion
#region 计算缩放比例和偏移量 private void InvalidateScale(out double scale, out double xOffset, out double yOffset)
/// <summary>
/// 计算缩放比例和偏移量
/// </summary>
/// <param name="scale"></param>
/// <param name="xOffset"></param>
/// <param name="yOffset"></param>
private void InvalidateScale(out double scale, out double xOffset, out double yOffset)
{
try
{
// 计算设计画布和缩放后的尺寸
double w = this.designerCanvas.ActualWidth * this.scaleTransform.ScaleX;
double h = this.designerCanvas.ActualHeight * this.scaleTransform.ScaleY;
// 计算缩略图画布的尺寸
double x = this.zoomCanvas.ActualWidth;
double y = this.zoomCanvas.ActualHeight;
// 计算缩放比例
double scaleX = x / w;
double scaleY = y / h;
scale = (scaleX < scaleY) ? scaleX : scaleY;
// 计算偏移量
xOffset = (x - scale * w) / 2;
yOffset = (y - scale * h) / 2;
}
catch (Exception ex)
{
scale = xOffset = yOffset = 0;
MessageBox.Show(ex.Message);
}
}
#endregion
#region 画布内容生成缩略图 private void CreateThumbnail()
/// <summary>
/// 画布内容生成缩略图
/// </summary>
private void CreateThumbnail()
{
try
{
// RenderTargetBitmap 对象,用于将 Canvas 渲染成位图
RenderTargetBitmap renderBitmap = new RenderTargetBitmap(
(int)designerCanvas.Width, (int)designerCanvas.Height, // 宽度和高度
96d, 96d, // DPI
PixelFormats.Pbgra32); // 像素格式
// 将 Canvas 渲染到 RenderTargetBitmap
renderBitmap.Render(designerCanvas);
// 缩略图大小的 BitmapSource
TransformedBitmap transformedBitmap = new TransformedBitmap(
renderBitmap,
new ScaleTransform(0.2, 0.2)); // 缩放比例
// ImageBrush 并将缩略图设置为 ImageBrush 的 ImageSource
ImageBrush thumbnailBrush = new ImageBrush(transformedBitmap);
// 将 ImageBrush 设置为 Canvas 的背景
PART_ZoomCanvas.Background = thumbnailBrush;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
#endregion
}
修改MainWindow.xaml 文件,添加缩略图控件
点击查看代码
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer
x:Name="scrollViewer"
Grid.RowSpan="2"
Width="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=UserControl}}"
Height="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=UserControl}}"
CanContentScroll="True"
HorizontalScrollBarVisibility="Hidden"
PanningMode="None"
VerticalScrollBarVisibility="Hidden">
<!-- 画布视图 -->
<Canvas x:Name="canvas" Style="{StaticResource canvasBackground}">
<!-- 圆形装饰组 -->
<Ellipse
Canvas.Left="600"
Canvas.Top="300"
Width="100"
Height="100"
Fill="YellowGreen"
Opacity="0.7" />
<Ellipse
Canvas.Left="200"
Canvas.Top="775"
Width="100"
Height="100"
Fill="Red"
Opacity="0.7" />
</Canvas>
</ScrollViewer>
<!-- 视图缩略图控件 -->
<local:ZoomBoxView
Grid.Row="1"
Width="220"
Height="Auto"
Margin="18"
HorizontalAlignment="Right"
ScrollViewer="{Binding ElementName=scrollViewer}" />
</Grid>
效果图


浙公网安备 33010602011771号