上一篇(WPF自定义控件——图像查看控件(一))介绍构建一个简单的图像查看控件,不过这个控件功能仅仅是与原生控件相当,这一篇里细说如何实现允许用户随意缩放和移动。
先写个函数实现一下用户操作缩放,上一篇中ImageViewer
里有一个ImageScale
的属性,这个属性是缩放的关键,没有任何缩放时ImageScale
为 1 ,放大缩小只需要将ImageScale
加上或减去一个缩放间隔,再与图片原始的宽高相乘,这样图片就实现了等比缩放。
实现如下:
/// <summary> /// 缩放比间隔 /// </summary> private const double ScaleInternal = 0.2; protected override void OnMouseWheel(MouseWheelEventArgs e) { base.OnMouseWheel(e); ScaleImg(e.Delta > 0); } private void ScaleImg(bool isEnlarge) { //获取放大/缩小系数 var tempScale = isEnlarge ? ImageScale + ScaleInternal : ImageScale - ScaleInternal; //最小允许缩小到缩放比间隔,最大允许放大到50倍 if (Abs(tempScale) < ScaleInternal || Abs(tempScale - 50) < 0.001) return; //缩放 ImageScale = tempScale; }
但这个缩放存在一个问题,缩放中心在(0,0),这就意味着图像左上角不动,往右、下边拉伸,这通常与用户意愿不符,合理的缩放应该是用户将鼠标移到关心的位置,缩放时焦点应该一直保持在这个位置,以便用户观察细节
要实现缩放时焦点保持在鼠标位置,那就需要移动图像来配合,在做这个功能之前,先实现一个最简单的效果——居中缩放,图像在未铺满容器前能看到所有内容,这时候不允许移动,因此在可移动前先居中缩放至铺满容器
改造一下ScaleImg
函数:
private void ScaleImg(bool isEnlarge) { //获取放大/缩小系数 var tempScale = isEnlarge ? ImageScale + ScaleInternal : ImageScale - ScaleInternal; //最小允许缩小到缩放比间隔,最大允许放大到50倍 if (Abs(tempScale) < ScaleInternal || Abs(tempScale - 50) < 0.001) return; //缩放 ImageScale = tempScale; var baseMarginX = .5 * _scaleInternalWidth; var baseMarginY = .5 * _scaleInternalHeight; //默认偏移都只取缩放间隔的一半,以便居中 var marginX = baseMarginX; var marginY = baseMarginY; ImageMargin = isEnlarge ? new Thickness(ImageMargin.Left - marginX, ImageMargin.Top - marginY, 0, 0) : new Thickness(ImageMargin.Left + marginX, ImageMargin.Top + marginY, 0, 0); ; }
这里在缩放后,ImageMargin
的Top、Left分别 +/- 上缩放间隔宽高的一半就实现了居中,宽高的缩放间隔在得到图像后就不再变化了,因此放在初始化的时候计算
/// <summary> /// 缩放高度间隔 /// </summary> private double _scaleInternalHeight; /// <summary> /// 缩放宽度间隔 /// </summary> private double _scaleInternalWidth; private void Init() { if (ImageSource == null || !_isLoaded) return; ImageScale = 1; //记录宽高 ImageWidth = ImageSource.PixelWidth; ImageHeight = ImageSource.PixelHeight; //原始宽高 ImageOriWidth = ImageSource.PixelWidth; ImageOriHeight = ImageSource.PixelHeight; //记录当前缩放间隔下的缩放宽高 _scaleInternalWidth = ImageOriWidth * ScaleInternal; _scaleInternalHeight = ImageOriHeight * ScaleInternal; //记录图像宽高比 _imgWidHeiScale = _imgWidHeiScale = ImageWidth / ImageHeight; ImgSizeAdaption(); AutoCentering(); }
改动后效果如下:
下一步,图片铺满容器后缩放中心移到鼠标指针的位置
再改造一下ScaleImg
函数,当图像铺满容器后让ImageMargin
以鼠标位置在图像长宽上的比例移动,这时候缩放中心就到了鼠标指针位置了
private void ScaleImg(bool isEnlarge) { var beforeScaleImgWidth = ImageWidth; var beforeScaleImgHeight = ImageHeight; //获取放大/缩小系数 var tempScale = isEnlarge ? ImageScale + ScaleInternal : ImageScale - ScaleInternal; //最小允许缩小到缩放比间隔,最大允许放大到50倍 if (Abs(tempScale) < ScaleInternal || Abs(tempScale - 50) < 0.001) return; //缩放 ImageScale = tempScale; //获取鼠标在画布上的位置 var posCanvas = Mouse.GetPosition(_imgPanel); //对应在图像上的坐标 var posImg = new Point(posCanvas.X - ImageMargin.Left, posCanvas.Y - ImageMargin.Top); var baseMarginX = .5 * _scaleInternalWidth; var baseMarginY = .5 * _scaleInternalHeight; //默认偏移都只取缩放间隔的一半,以便居中 var marginX = baseMarginX; var marginY = baseMarginY; //图片尺寸未大于容器尺寸前以图片中点作为缩放中心,图像尺寸大于容器尺寸时以鼠标位置作为缩放中心 if (beforeScaleImgWidth >= ActualWidth && beforeScaleImgHeight >= ActualHeight) { //按鼠标位置与图片长度的比例调整xy移动 marginX = posImg.X / beforeScaleImgWidth * _scaleInternalWidth; marginY = posImg.Y / beforeScaleImgHeight * _scaleInternalHeight; } ImageMargin = isEnlarge ? new Thickness(ImageMargin.Left - marginX, ImageMargin.Top - marginY, 0, 0) : new Thickness(ImageMargin.Left + marginX, ImageMargin.Top + marginY, 0, 0); ; }
看效果
这时候又引入了一个新问题,在不同点缩放后图像不再居中了,更严重的时候图像甚至会移动到容器之外
再做一点改动来解决这个问题,加上两个策略
- 缩小时当某一边缘与容器相接时该边缘不再移动
- 图片尺寸与容器尺寸相近时恢复居中
private void ScaleImg(bool isEnlarge) { var beforeScaleImgWidth = ImageWidth; var beforeScaleImgHeight = ImageHeight; //获取放大/缩小系数 var tempScale = isEnlarge ? ImageScale + ScaleInternal : ImageScale - ScaleInternal; //最小允许缩小到缩放比间隔,最大允许放大到50倍 if (Abs(tempScale) < ScaleInternal || Abs(tempScale - 50) < 0.001) return; //缩放 ImageScale = tempScale; //获取鼠标在画布上的位置 var posCanvas = Mouse.GetPosition(_imgPanel); //对应在图像上的坐标 var posImg = new Point(posCanvas.X - ImageMargin.Left, posCanvas.Y - ImageMargin.Top); var baseMarginX = .5 * _scaleInternalWidth; var baseMarginY = .5 * _scaleInternalHeight; //默认偏移都只取缩放间隔的一半,以便居中 var marginX = baseMarginX; var marginY = baseMarginY; //图片尺寸未大于容器尺寸前以图片中点作为缩放中心,图像尺寸大于容器尺寸时以鼠标位置作为缩放中心 if (beforeScaleImgWidth >= ActualWidth && beforeScaleImgHeight >= ActualHeight) { //按鼠标位置与图片长度的比例调整xy移动 marginX = posImg.X / beforeScaleImgWidth * _scaleInternalWidth; marginY = posImg.Y / beforeScaleImgHeight * _scaleInternalHeight; } Thickness thickness; //缩放时移动 向上/向左 增大/缩小 的值 if (isEnlarge) { thickness = new Thickness(ImageMargin.Left - marginX, ImageMargin.Top - marginY, 0, 0); } else { var marginActualX = ImageMargin.Left + marginX; var marginActualY = ImageMargin.Top + marginY; //修正因图片尺寸大于容器尺寸时缩放中心移动而产生的偏移,当某一边缘与容器相接时该边缘不再移动 var right = Abs(beforeScaleImgWidth - ActualWidth + ImageMargin.Left); //靠近左右边缘时 if (Abs(ImageMargin.Left) < baseMarginX || right < baseMarginX) { marginActualX = ImageMargin.Left + ImageMargin.Left / (ActualWidth - beforeScaleImgWidth) * _scaleInternalWidth; } var bottom = Abs(beforeScaleImgHeight - ActualHeight + ImageMargin.Top); //靠近上下边缘时 if (Abs(ImageMargin.Top) < baseMarginY || bottom < baseMarginY) { marginActualY = ImageMargin.Top + ImageMargin.Top / (ActualHeight - beforeScaleImgWidth) * _scaleInternalWidth; } //图片尺寸与容器尺寸相近时恢复居中 var subX = ImageWidth - ActualWidth; var subY = ImageHeight - ActualHeight; if (subX < 0.001) marginActualX = (ActualWidth - ImageWidth) / 2; if (subY < 0.001) marginActualY = (ActualHeight - ImageHeight) / 2; thickness = new Thickness(marginActualX, marginActualY, 0, 0); } ImageMargin = thickness; }
好了,大功告成!
缩放效果如下: