WPF中的图片组件,本身是支持不同的拉伸效果。具体如下:

        None,   //不做拉伸
        Fill,   //完全填充(会变形)
        Uniform,    //等比缩放,不会变形
        UniformToFill   //等比缩放,并完全填充。不会变形,但是长的部分会被裁剪

但是,如果我们要实现像QQ或者微信这样子的聊天气泡功能,直接使用图片组件就无法满足要求了。

我们可以观察下微信的聊天气泡,他的宽度和高度可以根据我们输入的内容自动调整,并且背景图片也不会存在变形的问题。

今天我们就用WPF来实现这个功能!

要实现不变形的拉伸功能,我们可以针对1个像素来进行拉伸,这样拉伸出来的图片,除了拉伸区域的像素都是一样的,其它区域还是保留了原来的图片的外观。

这里主要需要用到CroppedBitmap类,该类主要用于裁剪,可以对BitmapImage进行裁剪。

微信聊天气泡这种场景,它需要支持水平和垂直的方向的拉伸效果,我们可以利用CroppedBitmap,将原始图片裁剪成9张图,渲染的时候,我们分别将9张图渲染到对应的位置。 拉伸的区域就是9张图中的上面中间位置,下面中间位置,左边中间位置,右边中间位置以及最中间的位置。这几张图片,都按1个像素进行裁剪,这样就不会出现拉伸的图片了。

关键代码:

        
        private ImageSource GetCroppedBitmap(double x, double y, double width, double height)
        {
            return new CroppedBitmap(ImageSource, new Int32Rect((int)x, (int)y, (int)width, (int)height));
        }

        /// <summary>
        /// 获取水平偏移像素值
        /// </summary>
        /// <returns></returns>
        private int GetStretchHeight()
        {
            return (int)(RenderSize.Height - ImageSource.PixelHeight);
        }

        /// <summary>
        /// 获取垂直位置的偏移像素值
        /// </summary>
        /// <returns></returns>
        private int GetStretchWidth()
        {
            return (int)(RenderSize.Width - ImageSource.PixelWidth);
        }

        /// <summary>
        /// 水平拉伸是否可用
        /// </summary>
        private bool IsHorizontalStretchEnabled
        {
            get
            {
                if (HorizontalOffset > 0 && HorizontalOffset < ImageSource.PixelWidth)
                {
                    return true;
                }
                return false;
            }
        }

        /// <summary>
        /// 垂直拉伸是否可用
        /// </summary>
        private bool IsVerticalStretchEnabled
        {
            get
            {
                if (VerticalOffset > 0 && VerticalOffset < ImageSource.PixelHeight)
                {
                    return true;
                }
                return false;
            }
        }

        //绘制水平+垂直拉伸的方法
        protected override void OnRender(DrawingContext drawingContext)
        {
            var stretchHeight = GetStretchHeight();
                    if (stretchHeight < 0) stretchHeight = 0;
                    var stretchWidth = GetStretchWidth();
                    if (stretchWidth < 0) stretchWidth = 0;

                    if(stretchHeight==0 && stretchWidth == 0)
                    {
                        drawingContext.DrawImage(ImageSource, new Rect(0, 0, ImageSource.PixelWidth, ImageSource.PixelHeight));
                        return;
                    }

                    //这个需要9张图
                    //左上,左中,左下,右上,右中,右下,水平中,垂直中
                    var leftTop = GetCroppedBitmap(0, 0, HorizontalOffset, VerticalOffset);
                    var leftBottom = GetCroppedBitmap(0, VerticalOffset + 1, HorizontalOffset, ImageSource.PixelHeight - VerticalOffset - 1);
                    var rightTop = GetCroppedBitmap(HorizontalOffset + 1, 0, ImageSource.PixelWidth - HorizontalOffset - 1, VerticalOffset);
                    var rightBottom = GetCroppedBitmap(HorizontalOffset + 1, VerticalOffset + 1, ImageSource.PixelWidth - HorizontalOffset - 1, ImageSource.PixelHeight - VerticalOffset - 1);
                    //最中间的
                    var center = GetCroppedBitmap(HorizontalOffset, VerticalOffset, 1, 1);
                    var leftCenter = GetCroppedBitmap(0, VerticalOffset, HorizontalOffset, 1);
                    var rightCenter = GetCroppedBitmap(HorizontalOffset + 1, VerticalOffset, ImageSource.PixelWidth - HorizontalOffset - 1, 1);
                    var topCenter = GetCroppedBitmap(HorizontalOffset, 0, 1, VerticalOffset);
                    var bottomCenter = GetCroppedBitmap(HorizontalOffset, VerticalOffset + 1, 1, ImageSource.PixelHeight - VerticalOffset - 1);

                    //------------------------------- 上面的逻辑是切图,下面的逻辑是绘制 -----------------------------------


                    drawingContext.DrawImage(leftTop, new Rect(0, 0, HorizontalOffset, VerticalOffset));

                    drawingContext.DrawImage(rightTop, new Rect(HorizontalOffset + stretchWidth, 0, ImageSource.PixelWidth - HorizontalOffset - 1, VerticalOffset));

                    if (stretchHeight > 0)
                    {
                        drawingContext.DrawImage(leftCenter, new Rect(0, VerticalOffset, HorizontalOffset, stretchHeight));
                        drawingContext.DrawImage(rightCenter, new Rect(HorizontalOffset + stretchWidth, VerticalOffset, ImageSource.PixelWidth - HorizontalOffset - 1, stretchHeight));
                    }

                    if (stretchWidth > 0)
                    {
                        drawingContext.DrawImage(topCenter, new Rect(HorizontalOffset, 0, stretchWidth, VerticalOffset));
                        drawingContext.DrawImage(bottomCenter, new Rect(HorizontalOffset, VerticalOffset + stretchHeight, stretchWidth, ImageSource.PixelHeight - VerticalOffset - 1));
                    }

                    if (stretchHeight > 0 && stretchWidth > 0)
                    {
                        drawingContext.DrawImage(center, new Rect(HorizontalOffset, VerticalOffset, stretchWidth, stretchHeight));
                    }

                    drawingContext.DrawImage(leftBottom, new Rect(0, VerticalOffset + stretchHeight, HorizontalOffset, ImageSource.PixelHeight - VerticalOffset - 1));
                    drawingContext.DrawImage(rightBottom, new Rect(HorizontalOffset + stretchWidth, VerticalOffset + stretchHeight, ImageSource.PixelWidth - HorizontalOffset - 1, ImageSource.PixelHeight - VerticalOffset - 1));
        }

        //仅支持水平方向拉伸
        protected override void OnRender(DrawingContext drawingContext)
        {
            var stretchWidth = GetStretchWidth();

                    if (stretchWidth <= 0)
                    {
                        drawingContext.DrawImage(ImageSource, new Rect(0, 0, ImageSource.PixelWidth, ImageSource.PixelHeight));
                        return;
                    }

                    var left = GetCroppedBitmap(0, 0, HorizontalOffset, ImageSource.PixelHeight);
                    var center = GetCroppedBitmap(HorizontalOffset, 0, 1, ImageSource.PixelHeight);
                    var right = GetCroppedBitmap(HorizontalOffset + 1, 0, ImageSource.PixelWidth - HorizontalOffset - 1, ImageSource.PixelHeight);

                    drawingContext.DrawImage(left, new Rect(0, 0, HorizontalOffset, ImageSource.PixelHeight));
                    
                    if (stretchWidth > 0)
                    {
                        drawingContext.DrawImage(center, new Rect(HorizontalOffset, 0, stretchWidth, ImageSource.PixelHeight));
                    }
                    else
                    {
                        stretchWidth = 0;
                    }

                    drawingContext.DrawImage(right, new Rect(HorizontalOffset + stretchWidth, 0, ImageSource.PixelWidth - HorizontalOffset - 1, ImageSource.PixelHeight));
        }

        //仅支持垂直方向拉伸
        protected override void OnRender(DrawingContext drawingContext)
        {
            var stretchHeight = GetStretchHeight();

                    if (stretchHeight <= 0)
                    {
                        drawingContext.DrawImage(ImageSource, new Rect(0, 0, ImageSource.PixelWidth, ImageSource.PixelHeight));
                        return;
                    }

                    //裁剪还是按原始图片尺寸来裁剪,这里的原始图片尺寸是按dpi 96计算的
                    var top = GetCroppedBitmap(0, 0, ImageSource.PixelWidth, VerticalOffset);
                    var center = GetCroppedBitmap(0, VerticalOffset, ImageSource.PixelWidth, 1);
                    var bottom = GetCroppedBitmap(0, VerticalOffset + 1, ImageSource.PixelWidth, ImageSource.PixelHeight - VerticalOffset - 1);

                    //这里用 PixelWidth,而不是 Width,主要是因为显示的时候,要按图片的解码尺寸来显示,否则图片就会变大或者变小,这个是由DPI决定的
                    drawingContext.DrawImage(top, new Rect(0, 0, ImageSource.PixelWidth, VerticalOffset));

                    //超长才需要显示拉伸部分的图片
                    if (stretchHeight > 0)
                    {
                        drawingContext.DrawImage(center, new Rect(0, VerticalOffset, ImageSource.PixelWidth, stretchHeight));
                    }
                    else
                    {
                        stretchHeight = 0;
                    }
                    drawingContext.DrawImage(bottom, new Rect(0, VerticalOffset + stretchHeight, ImageSource.PixelWidth, ImageSource.PixelHeight - VerticalOffset - 1));
        }

        //测量布局大小,这里要记得重写下。这个版本不支持不同尺寸的分辨率,可以通过计算缩放比来实现
        private Size MeasureCore(Size size, ImageSource imgSource)
        {
            if (imgSource == null) return size;

            Size naturalSize;

            if (IsHorizontalStretchEnabled && IsVerticalStretchEnabled)
            {
                naturalSize = new Size(size.Width, size.Height);
            }
            else if (IsHorizontalStretchEnabled)
            {
                naturalSize = new Size(size.Width, imgSource.PixelHeight);
            }
            else if (IsVerticalStretchEnabled)
            {
                naturalSize = new Size(imgSource.PixelWidth, size.Height);
            }
            else
            {
                return size;
            }

            return naturalSize;
        }

以上代码就可以实习水平拉伸,垂直拉伸或者水平+垂直拉伸的功能了。目前的测试代码还不支持不同分辨率的图片,demo中的计算是使用了ImageSource的宽高。如果需要支持任意分辨率,可以按渲染的宽高和图片的实际宽高做个比例缩放运算即可。

微信公众号:

承哥技术交流小作坊

欢迎转载分享,如若转载,请标注署名。


本文会经常更新,请阅读原文: https://huchengv5.gitee.io//post/WPF-%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%9B%BE%E7%89%87%E6%8C%89%E5%83%8F%E7%B4%A0%E6%8B%89%E4%BC%B8.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名胡承(包含链接: https://huchengv5.gitee.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系