WPF 自定义 ZoomBox 控件:实现缩略图同步、缩放调节与拖拽定位

实现 WPF 应用中画布的缩放控制与缩略图导航,支持滑块调节缩放比例、缩略图拖拽定位,实时同步主画布视图与缩略图视口位置。

  1. 缩放控制:通过 Slider 值变化计算缩放比例,同步更新主画布 ScaleTransform,并调整 ScrollViewer 偏移,确保缩放中心对齐视图中心。

  2. 缩略图同步: - 主画布 LayoutUpdated 时,计算缩略图缩放比例,更新 Thumb(视口)的大小与位置; - 拖拽 Thumb 时,反向计算主画布 ScrollViewer 的偏移量,实现快速定位。

  3. 依赖注入:通过 ScrollViewer 依赖属性,建立自定义控件与主画布的关联,解耦控件与业务画布。

关键点

  1. VisualBrush/RenderTargetBitmap:两种方式生成主画布缩略图(XAML 用 VisualBrush 实时同步,代码中 CreateThumbnail 方法用 RenderTargetBitmap 静态渲染)。

  2. 模板绑定:重写 OnApplyTemplate 获取模板内命名元素(PART_前缀),确保控件结构合法性。

  3. 坐标换算:通过主画布与缩略图的尺寸比例,实现主视图偏移量与缩略图视口位置的双向映射。

添加用户控件 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>

效果图

image

posted @ 2025-09-20 12:30  笺上知微  阅读(65)  评论(0)    收藏  举报