利用OpacityMask制作打洞效果


起因

項目上存在一個連線功能,在設計的原型中,在連線中間文字上下各有15像素的空白。接手的同事覺得沒思路,問我能不能在不影響連線后面的背景情況下解決該問題。我就抽了點時間給他寫了個Demo。回家后趁熱打鐵,重新寫了個Demo,添加和完善了些功能。下面是效果圖:

1

代碼實現

OpacityMask

在最開始看到效果圖的時候,我就想到利用OpacityMask來解決問題。可能這個屬性平時很多朋友都沒注意到,因為一般情況下用Opacity就足夠了。

OpacityMask定義在UIElement中,類型為Brush。僅使用提供的 Brush 的任意 Alpha 通道值。 Brush 呈現內容的其他通道(紅色、綠色或藍色)被忽略。具體來說,在Brush中Alpha通道值為0的地方將為透明,不為0的將顯示在UIElement中定義的背景。下面就以Demo中的三個例子簡單分享下怎么利用OpacityMask。

矩形空洞

OpacityMask是一個VisualBrush,VisualBrush中有一個三行三列的Grid,Grid中除第二行第二列的單元格外,其余的單元格均用黑色的矩形填充。這樣就會在第二行第二列的單元格處形成一個空洞。

主要代碼在RectangleHoleConverter中,代碼如下:

namespace HoleWithOpacityMask
{
    using System;
    using System.Globalization;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Media;
    using System.Windows.Shapes;

    /// <summary>
    /// 矩形空洞的轉換器
    /// </summary>
    public class RectangleHoleConverter : IMultiValueConverter
    {
        /// <summary>
        /// 轉換成矩形空洞
        /// </summary>
        /// <param name="values">
        /// 轉換值列表,第一個表示起始寬度,第二個表示起始高度,
        /// 第三個表示總寬度,第四個表示總高度
        /// 第五個表示宿主寬度,第六個表示宿主寬度
        /// </param>
        /// <param name="targetType"></param>
        /// <param name="parameter"></param>
        /// <param name="culture"></param>
        /// <returns></returns>
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values == null || values.Length != 6 || values.HaveNullItem()
                || !values.IsAllInstanceOfType(typeof(double)))
            {
                return DependencyProperty.UnsetValue;
            }

            var maskStartWidth = (double)values[0];
            var maskStartHeight = (double)values[1];
            var maskTotalWidth = (double)values[2];
            var maskTotalHeight = (double)values[3];
            var hostWidth = (double)values[4];
            var hostHeight = (double)values[5];
            if (hostWidth == 0.0 || hostHeight == 0.0)
            {
                return null;
            }

            var maskGrid = new Grid { Width = hostWidth, Height = hostHeight };

            var opacityStartColumnDefinition = new ColumnDefinition { Width = new GridLength(maskStartWidth) };
            var transparentColumnDefinition = new ColumnDefinition { Width = new GridLength(maskTotalWidth) };
            ColumnDefinition opacityEndColumnDefinition = new ColumnDefinition();
            opacityEndColumnDefinition.Width = new GridLength(1.0, GridUnitType.Star);
            maskGrid.ColumnDefinitions.Add(opacityStartColumnDefinition);
            maskGrid.ColumnDefinitions.Add(transparentColumnDefinition);
            maskGrid.ColumnDefinitions.Add(opacityEndColumnDefinition);

            var opacityStartRowDefinition = new RowDefinition { Height = new GridLength(maskStartHeight) };
            var transparentRowDefinition = new RowDefinition { Height = new GridLength(maskTotalHeight) };
            RowDefinition opacityEndRowDefinition = new RowDefinition();
            opacityEndRowDefinition.Height = new GridLength(1.0, GridUnitType.Star);
            maskGrid.RowDefinitions.Add(opacityStartRowDefinition);
            maskGrid.RowDefinitions.Add(transparentRowDefinition);
            maskGrid.RowDefinitions.Add(opacityEndRowDefinition);

            for (int i = 0; i < 3; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    if ((i != 1) || (j != 1))
                    {
                        Rectangle opacityRectangle = new Rectangle { Fill = Brushes.Black };
                        Grid.SetRow(opacityRectangle, i);
                        Grid.SetColumn(opacityRectangle, j);
                        maskGrid.Children.Add(opacityRectangle);
                    }
                }
            }

            return new VisualBrush(maskGrid);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return new[] { Binding.DoNothing };
        }
    }
}

其中用到了兩個擴展方法如下:

namespace HoleTest
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    public static class EnumerableExtension
    {
        /// <summary>
        /// 枚舉器中是否存在null條目
        /// </summary>
        /// <typeparam name="T">元素類型</typeparam>
        /// <param name="enumerable">元素枚舉</param>
        /// <returns>存在null條目返回true,否則返回false</returns>
        public static bool HaveNullItem<T>(this IEnumerable<T> enumerable)
        {
            return enumerable.Any(item => item == null);
        }

        /// <summary>
        /// 枚舉器中是否全為指定類型的實例
        /// </summary>
        /// <typeparam name="T">元素類型</typeparam>
        /// <param name="enumerable">元素枚舉</param>
        /// <returns>全為指定類型的實例返回true,否則返回false</returns>
        public static bool IsAllInstanceOfType<T>(this IEnumerable<T> enumerable, Type type)
        {
            return enumerable.All(item => type.IsInstanceOfType(item));
        }
    }
}

橢圓形空洞

OpacityMask是一個DrawingBrush,DrawingBrush是利用GeometryDrawing繪制。而GeometryDrawing中是一個由黑色填充的形狀,該形狀是在矩形中除去一個橢圓。這樣就會在矩形中形成一個空洞。

主要代碼在EllipseHoleConverter中,代碼如下:

namespace HoleWithOpacityMask
{
    using System;
    using System.Globalization;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Media;

    /// <summary>
    /// 橢圓形空洞的轉換器
    /// </summary>
    public class EllipseHoleConverter : IMultiValueConverter
    {
        /// <summary>
        /// 轉換成矩形空洞
        /// </summary>
        /// <param name="values">
        /// 轉換值列表,第一個表示起始寬度,第二個表示起始高度,
        /// 第三個表示總寬度,第四個表示總高度
        /// 第五個表示宿主寬度,第六個表示宿主寬度
        /// </param>
        /// <param name="targetType"></param>
        /// <param name="parameter"></param>
        /// <param name="culture"></param>
        /// <returns></returns>
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values == null || values.Length != 6 || values.HaveNullItem()
                || !values.IsAllInstanceOfType(typeof(double)))
            {
                return DependencyProperty.UnsetValue;
            }

            var maskEllipseCenterX = (double)values[0];
            var maskEllipseCenterY = (double)values[1];
            var maskRadiusX = (double)values[2];
            var maskRadiusY = (double)values[3];
            var hostWidth = (double)values[4];
            var hostHeight = (double)values[5];
            if (hostWidth == 0.0 || hostHeight == 0.0)
            {
                return null;
            }

            var maskRectangle = new RectangleGeometry(new Rect(new Size(hostWidth, hostHeight)));
            var maskEllipse = new EllipseGeometry(
                new Point(maskEllipseCenterX, maskEllipseCenterY),
                maskRadiusX,
                maskRadiusY);
            var combinedGeometry = Geometry.Combine(maskRectangle, maskEllipse, GeometryCombineMode.Exclude, null);
            var drawingBrush = new DrawingBrush(new GeometryDrawing(Brushes.Black, null, combinedGeometry));

            return drawingBrush;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return new[] { Binding.DoNothing };
        }
    }
}

圖形空洞

其實最常用的還是ImageBrush,因為很多復雜的效果用VisualBrush或者DrawingBrush來實現效果很復雜且不清晰。在上面的Demo中我使用的是一張PNG圖片,當然一般也用PNG圖標,誰叫它支持透明呢。圖片是

pig

XAML代碼如下:

<Border Width="180"
        Height="180"
        Margin="3"
        Background="Aquamarine">
    <Border Background="LightPink">
        <Border.OpacityMask>
            <ImageBrush ImageSource="pig.png" />
        </Border.OpacityMask>
    </Border>
</Border>

容易看出,PNG圖片中透明的就真的透明了(直接看到外層Border上級的背景BurlyWood了),不透明的地方就使用了當前Border設置的背景色LightPink。

更多

還可使用線性漸變(LinearGradientBrush)、徑向漸變(RadialGradientBrush)實現更多有趣的效果,下圖是MSDN中的一個例子

Snap2

純色畫刷(SolidColorBrush)基本用不到,要么透明,要么不透明,還不如直接設置Opacity。

下載鏈接

博客園:HoleWithOpacityMask


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM