WPF自定义控件——图像查看控件(二)

  • Post author:
  • Post category:WPF
  • Post comments:0评论

上一篇(WPF自定义控件——图像查看控件(一))介绍构建一个简单的图像查看控件,不过这个控件功能仅仅是与原生控件相当,这一篇里细说如何实现允许用户随意缩放和移动。

先写个函数实现一下用户操作缩放,上一篇中ImageViewer里有一个ImageScale的属性,这个属性是缩放的关键,没有任何缩放时ImageScale1 ,放大缩小只需要将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();
}

改动后效果如下:

缩放中心从(0,0)到了图像中心

下一步,图片铺满容器后缩放中心移到鼠标指针的位置

再改造一下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;
}

好了,大功告成!

缩放效果如下:

发表回复