取消

流畅设计 Fluent Design System 中的光照效果 RevealBrush,WPF 也能模拟实现啦!

UWP 才能使用的流畅设计效果好惊艳,写新的 UWP 程序可以做出更漂亮的 UI 啦!然而古老的 WPF 项目也想解解馋怎么办?

于是我动手实现了一个!


迫不及待看效果

光照效果
▲ 是不是很像 UWP 中的 RevealBorderBrush

不止是效果像,连 XAML 写法也像:

1
2
3
4
5
6
7
8
9
10
<Border BorderThickness="1" Margin="50,34,526,348">
    <Border.BorderBrush>
        <demo:RevealBorderBrush />
    </Border.BorderBrush>
</Border>
<Border BorderThickness="1" Margin="50,76,526,306">
    <Border.BorderBrush>
        <demo:RevealBorderBrush Color="White" FallbackColor="Gray" />
    </Border.BorderBrush>
</Border>

▲ 模拟得很像的 RevealBorderBrush 的 XAML 写法

当然,窗口背景那张图是直接用的高斯模糊效果,并不是亚克力 Acrylic 效果。鉴于那张被模糊得看不清的图是我自己画的,所以我一定要单独放出来给大家看🤓!

我自己画的图,不忍直视,只好模糊掉作为背景了。请点击查看:图片

以下是我后来使用此模拟的效果制作的应用。这些应用虽然看起来整个儿都很像 UWP 应用,但都是 100% 纯 WPF;因为我模拟了 UWP 的风格:

2019 年 1 月更新:

Cloud Keyboard
▲ 源码在这个仓库:Walterlv.CloudKeyboard

2019 年 3 月更新:

Diagnostics Window

话不多说看源码

UWP 里的 CompositionBrush 是用一个 ShaderEffect 做出所有控件的所有效果的。正如 叛逆者如何评价微软在 Build 2017 上提出的 Fluent Design System? - 知乎 一文中说的,只需要极少的计算量就能完成。

不过 Win32 窗口并没有得到眷恋,所以我只好自己实现。但限于只能使用 WPF 内建机制,故性能上当然不能比了。但在小型项目的局部用用还是非常不错的——尤其是个人项目!不过话说现在个人项目谁还用 WPF 呢 (逃

思路是画一个径向渐变,即 RadialGradientBrush,然后当鼠标在窗口内移动时,改变径向渐变的渐变中心为鼠标所在点。

以下是全部源码。不要在意基类啦!WPF 不让我们实现自己的 Brush,所以只好用 MarkupExtension 绕道实现了。

2019 年 3 月更新:以下源码中现在使用了全局光照,也就是说,就算你的控件不在一个固定的窗口中,也会使用到光照效果了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;

// ReSharper disable CheckNamespace

namespace Walterlv.Effects
{
    /// <summary>
    /// Paints a control border with a reveal effect using composition brush and light effects.
    /// </summary>
    public class RevealBorderBrushExtension : MarkupExtension
    {
        [ThreadStatic]
        private static Dictionary<RadialGradientBrush, WeakReference<FrameworkElement>> _globalRevealingElements;

        /// <summary>
        /// The color to use for rendering in case the <see cref="MarkupExtension"/> can't work correctly.
        /// </summary>
        public Color FallbackColor { get; set; } = Colors.White;

        /// <summary>
        /// Gets or sets a value that specifies the base background color for the brush.
        /// </summary>
        public Color Color { get; set; } = Colors.White;

        public Transform Transform { get; set; } = Transform.Identity;

        public Transform RelativeTransform { get; set; } = Transform.Identity;

        public double Opacity { get; set; } = 1.0;

        public double Radius { get; set; } = 100.0;

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            // 如果没有服务,则直接返回。
            if (!(serviceProvider.GetService(typeof(IProvideValueTarget)) is IProvideValueTarget service)) return null;
            // MarkupExtension 在样式模板中,返回 this 以延迟提供值。
            if (service.TargetObject.GetType().Name.EndsWith("SharedDp")) return this;
            if (!(service.TargetObject is FrameworkElement element)) return this;
            if (DesignerProperties.GetIsInDesignMode(element)) return new SolidColorBrush(FallbackColor);

            var brush = CreateGlobalBrush(element);
            return brush;
        }

        private Brush CreateBrush(UIElement rootVisual, FrameworkElement element)
        {
            var brush = CreateRadialGradientBrush();
            rootVisual.MouseMove += OnMouseMove;
            return brush;

            void OnMouseMove(object sender, MouseEventArgs e)
            {
                UpdateBrush(brush, e.GetPosition(element));
            }
        }

        private Brush CreateGlobalBrush(FrameworkElement element)
        {
            var brush = CreateRadialGradientBrush();
            if (_globalRevealingElements is null)
            {
                CompositionTarget.Rendering -= OnRendering;
                CompositionTarget.Rendering += OnRendering;
                _globalRevealingElements = new Dictionary<RadialGradientBrush, WeakReference<FrameworkElement>>();
            }

            _globalRevealingElements.Add(brush, new WeakReference<FrameworkElement>(element));
            return brush;
        }

        private void OnRendering(object sender, EventArgs e)
        {
            if (_globalRevealingElements is null)
            {
                return;
            }

            var toCollect = new List<RadialGradientBrush>();
            foreach (var pair in _globalRevealingElements)
            {
                var brush = pair.Key;
                var weak = pair.Value;
                if (weak.TryGetTarget(out var element))
                {
                    Reveal(brush, element);
                }
                else
                {
                    toCollect.Add(brush);
                }
            }

            foreach (var brush in toCollect)
            {
                _globalRevealingElements.Remove(brush);
            }

            void Reveal(RadialGradientBrush brush, IInputElement element)
            {
                UpdateBrush(brush, Mouse.GetPosition(element));
            }
        }

        private void UpdateBrush(RadialGradientBrush brush, Point origin)
        {
            IInputElement element;
            if (IsUsingMouseOrStylus())
            {
                brush.GradientOrigin = origin;
                brush.Center = origin;
            }
            else
            {
                brush.Center = new Point(double.NegativeInfinity, double.NegativeInfinity);
            }
        }

        private RadialGradientBrush CreateRadialGradientBrush()
        {
            var brush = new RadialGradientBrush(Color, Colors.Transparent)
            {
                MappingMode = BrushMappingMode.Absolute,
                RadiusX = Radius,
                RadiusY = Radius,
                Opacity = Opacity,
                Transform = Transform,
                RelativeTransform = RelativeTransform,
                Center = new Point(double.NegativeInfinity, double.NegativeInfinity),
            };
            return brush;
        }

        private bool IsUsingMouseOrStylus()
        {
            var device = Stylus.CurrentStylusDevice;
            if (device is null)
            {
                return true;
            }

            if (device.TabletDevice.Type == TabletDeviceType.Stylus)
            {
                return true;
            }

            return false;
        }
    }
}

参考资料

本文会经常更新,请阅读原文: https://blog.walterlv.com/post/fluent-design-reveal-brush-in-wpf.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议

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

登录 GitHub 账号进行评论