WPF 视图缩略图控件(支持缩放调节与拖拽定位)

news/2025/9/20 12:31:14/文章来源:https://www.cnblogs.com/dosswy/p/19102290

实现 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

<UserControlx: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"><BorderMinHeight="30"Background="#F6F6F6"CornerRadius="6"><Border.Effect><DropShadowEffectBlurRadius="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控件的样式矩形  --><RectangleFill="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><!--  滑块  --><SliderName="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" /><!--  绑定到滑块的值  --><TextBlockGrid.Column="1"HorizontalAlignment="Right"VerticalAlignment="Center"Text="{Binding ElementName=PART_ZoomSlider, Path=Value}" /><!--  显示百分号  --><TextBlockGrid.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的内容作为Canvasthis.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, // DPIPixelFormats.Pbgra32); // 像素格式// 将 Canvas 渲染到 RenderTargetBitmaprenderBitmap.Render(designerCanvas);// 缩略图大小的 BitmapSourceTransformedBitmap transformedBitmap = new TransformedBitmap(renderBitmap,new ScaleTransform(0.2, 0.2)); // 缩放比例// ImageBrush 并将缩略图设置为 ImageBrush 的 ImageSourceImageBrush 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><ScrollViewerx: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}"><!--  圆形装饰组  --><EllipseCanvas.Left="600"Canvas.Top="300"Width="100"Height="100"Fill="YellowGreen"Opacity="0.7" /> <EllipseCanvas.Left="200"Canvas.Top="775"Width="100"Height="100"Fill="Red"Opacity="0.7" /> </Canvas></ScrollViewer><!--  视图缩略图控件  --><local:ZoomBoxViewGrid.Row="1"Width="220"Height="Auto"Margin="18"HorizontalAlignment="Right"ScrollViewer="{Binding ElementName=scrollViewer}" /></Grid>

效果图

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/908326.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实用指南:Dify关联Ollama

实用指南:Dify关联Ollama2025-09-20 12:21 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; f…

ik中文分词器使用

IK分词器介绍 在ElasticSearch中默认使用的分词器为Standard分词器,该分词器对中文不友好,对中文的处理方式是按单个汉字分词,无法识别中文里的词语、短语等语义单元。例如对于 "汉朝" 这个词,默认分词器…

动态水印也能去除?ProPainter一键视频抠图整合包下载

ProPainter是一个基于E2FGVI实现的AI视频编辑工具,它结合了增强的传播和Transformer机制,能够快速高效地进行视频修复和水印去除功能特点对象移除:智能地检测和移除视频中的动态物体,对于去除不需要的元素或错误…

SpringBoot整合RustFS:全方位优化文件上传性能

SpringBoot整合RustFS:全方位优化文件上传性能作为一名多年深耕分布式存储的架构师,我在多个企业级项目中成功实施SpringBoot与RustFS的集成。本文将分享一套​经过实战检验的性能优化方案,帮助你的文件上传速度提升…

javaScript(WebAPI) - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

windows使用es-client插件

下载插件并集成到浏览器 在各大浏览器应用商店搜索es-client,这里以edge浏览器作为演示,bing搜索“微软商店 插件 es-clint”,搜索结果第一条记录点击进去.点击右边的“获取”按钮,将插件添加到浏览器中。这里由于…

AI学习日记 - 实践

AI学习日记 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Co…

es中的端点

_doc端点 用于对单个****文档的crud操作(如创建、获取、更新、删除单个文档),常见用法如下向指定索引添加新文档: POST /索引名/_docES会自动生成唯一文档ID,例:POST /books/_doc → 向 books索引添加新文档。 根…

解码C语言宏

预处理概述 基本概念 预处理是C语言编译过程的第一步,所有以#开头的指令都由预处理器处理,这些指令不属于C语言语法本身。 预处理指令类型头文件包含:#include 宏定义:#define 宏取消:#undef 条件编译:#if, #ifd…

es中的索引

索引的概念 在ES中,索引(Index) 是核心的数据存储和检索单元,其本质是一组结构相似的文档(Document)的集合,同时包含了文档的元数据(如字段类型、分词器配置)和检索所需的 “倒排索引” 结构。ES软件的索引类…

es中的数据类型

字符串 文本(Text)作用:适用于全文搜索的文本字段,例如文章内容、电子邮件正文、产品描述等长文本。 特点:ES会对文本内容进行分词处理,将字符串转换为单个术语的列表,支持全文搜索和模糊查询。但通常不用于排序或…

防御安全播客第214期:数据泄露与漏洞攻防实战

本期播客深入探讨GDPR隐私规则与安全的平衡、伊朗黑客攻击美国大学数据泄露事件、Guccifer 2.0身份揭秘、Orbitz支付卡盗窃案及SamSam勒索软件攻击亚特兰大事件,并分析网络犯罪分子最常利用的顶级漏洞。媒体链接音频源…

windows使用kibana

下载Kibana安装包 https://www.elastic.co/downloads/past-releases#kibana(如果知道版本号也可以直接将最后的kibana换成对应的版本号)上图中两个下拉框分别选择对应的产品和版本,选择好之后点击右边蓝色的Downloa…

VIVADO的IP核 DDS快速采用——生成正弦波,线性调频波

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

03作业

03作业 一、关于static修饰符的问题适合用static修饰的方法:工具类方法(如Math类的abs()、StringUtils的isEmpty()) 工厂方法(用于创建对象的方法) 单例模式的getInstance()方法 与类本身相关而非实例相关的操作不…

软工作业个人项目

这个作业属于哪个课程 计科23级12班这个作业要求在哪里 [个人项目-作业](个人项目 - 作业 - 计科23级12班 - 班级博客 - 博客园)这个作业的目标 设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改…

rapidxml中接口函数

引言 RapidXML 是一个轻量级 XML 解析库,核心接口围绕 文档解析、节点操作、属性操作 三大类展开。(基于 rapidxml.hpp 核心头文件) 一、文档操作(xml_document<> 类) xml_document<> 是 XML 文档的核心…

YOLO进阶提升 6模型训练与测试

进阶提升 6模型训练与测试 核心概念训练过程:通过迭代(epoch)不断优化损失函数,直至收敛。 Checkpoints(模型权重保存点):训练中定期保存模型参数,用于恢复或测试。 测试/推理(Inference):利用训练好的模型…

深入解析:C语言---判断语句

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

vue基于Springboot框架网上电子书店商城好书推荐管理系统 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …