其实我是希望能够找到为 Win32 桌面程序实现 Fluent Design System 效果的,不过一直没找到。倒是发现了一个可以让 Win32 桌面程序做出类似 Windows 10 开始菜单和操作中心那种模糊效果的方法。
写这篇文章并不意味着我推荐大家这么去做,只是希望将方法总结出来,作为一个研究点而已。
本文提供了一个完整的用于在 Windows 10 上实现模糊特效的 C# 类,没有放到 GitHub 也没有其他类型的开源。如果需要直接拿走就好。
为什么不推荐使用?
当初 Windows Vista 推出 Aero 特效后惊艳了世人。然而那还是个 30 帧动画大行其道的年代,即便是后来的 Windows 7 也是如此。这个特效不能使用更高帧率就在于对资源的消耗量太感人。然而 Windows 8/8.1 的推出,动画是其中的一个重要部分——那全屏的感人的流畅的动画,那丝般的顺滑,让人难忘。然而这么流畅是有代价的——需要 60 帧满速运行,而且不能占用太多资源,不然依然卡顿。于是微软只好砍掉了背景高斯模糊功能……充满遗憾……被世人唾骂……
忍受不了世人的咒骂,微软只好再把高斯模糊效果带回 Windows 10。可是,在算法没有从根本上得到改进的情况下,大量的资源消耗依然是不可忽视的问题。所以微软现在只好在少数几个地方先用用,满足大家曾经对于 Aero 的呼声,适当提升一点点审美。
既然微软能用,那么我们也理应能用。然而事实情况是——微软没有任何文档来说明如何实现这样的效果。足以说明微软也不希望他们担心的性能问题大量出现在用户的电脑上。(对于移动设备如 Surface 来说,带来的就是电池可用时间的缩短。)
叛逆者 说,他们终于在特效的算法上有了质的突破,创意来源于平时小组言谈中一点点想法。这就是 Fluent Design System!终于只需要非常少量的计算资源就能达到非常炫酷的现代效果。让人印象深刻的可以替代 Aero 的就属亚克力(Acrylic)了。这效果是在 DWM 进程上运行的(与 Aero 特效一样),所以也不会额外占用应用程序本身的计算资源。
然而,本文探究的方法并不是 Fluent Design System 中的任何部分。依然是微软不期望大家使用的方法,所以,本文并不推荐大家作为真实项目使用,而是作为一种探究学习的途径。
我封装的 API
为了方便大家使用,我封装了一个小的 API。于是大家可以非常方便地使用。
如果你想在 XAML 里用,直接在 MainWindow
上加上以下两行:
1
2
xmlns:interop="clr-namespace:Walterlv.Demo.Interop"
interop:WindowBlur.IsEnabled="True"
如果你希望直接在 cs 文件里面写,则这样就好了:
1
WindowBlur.SetIsEnabled(this, true);
注意这里的 this
指的是 MainWindow
。
事实上,当你用了上面的 API 试图看一看效果的时候,你会发现其实并不如本文一开始的图片那样。而是一个非常丑陋的窗口:
你需要做两件事情才能变得好看一些:
- 设置窗口背景色为透明(
Transparent
)/半透明(#A0FFFFFF
),以便去掉默认的白色背景。 - 为窗口设置
WindowChrome
属性,以便去掉标题栏颜色的不同,并修复周围阴影几个像素的半透明偏差。
完整的代码可以看这里:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<Window x:Class="Walterlv.Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:interop="clr-namespace:Walterlv.Demo.Interop"
mc:Ignorable="d" Title="Blur Demo" Height="350" Width="525"
interop:WindowBlur.IsEnabled="True"
Background="Transparent">
<WindowChrome.WindowChrome>
<WindowChrome GlassFrameThickness="-1" />
</WindowChrome.WindowChrome>
<Grid Background="#A0FFFFFF">
<TextBlock Foreground="White"
FontSize="20" FontWeight="Light" TextAlignment="Center"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Run Text="Hello World" FontSize="48"/>
<LineBreak/>
<Run Text="白底效果" />
<LineBreak/>
<Run Text="walterlv.github.io"/>
</TextBlock>
</Grid>
</Window>
实现原理——SetWindowCompositionAttribute
WindowBlur
类内部用到了微软从未开放的 API,叛逆者 也已经证实这就是微软在开始菜单和操作中心中用到的 API。这个 API 就是 SetWindowCompositionAttribute
。
事实上此类中的代码来源也是多个地方找到的,最开始是 C 语言的版本,而后从 Nukepayload2/sample-win10-aeroglass 找到了 C# 的版本,最终基于它改造成了现在这个样子。
代码见本文最后,因为我想把参考资料放到前面来,以感谢前人的努力。
参考资料
- 如何评价微软在 Build 2017 上提出的 Fluent Design System? - 知乎
- windows - Mimicking Acrylic in a Win32 app - Stack Overflow
- winapi - How do you set the glass blend colour on Windows 10? - Stack Overflow
- 调用未公开API SetWindowCompositionAttribute 在Win10下开启Aero - CSDN博客
- Windows 10 开始菜单的高斯模糊效果是如何实现的? - 知乎
- 从编程的角度来说,Windows 的开始菜单是如何实现的? - 知乎
- Windows 10 Creators Update 新功能——画中画模式和窗口高斯模糊 - yinyue200 - 博客园
- Nukepayload2/sample-win10-aeroglass
附:封装好的 API 代码
using System; | |
using System.Runtime.InteropServices; | |
using System.Windows; | |
using System.Windows.Interop; | |
using Walterlv.Demo.Interop.Native; | |
namespace Walterlv.Demo.Interop | |
{ | |
public class WindowBlur | |
{ | |
public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached( | |
"IsEnabled", typeof(bool), typeof(WindowBlur), | |
new PropertyMetadata(false, OnIsEnabledChanged)); | |
public static void SetIsEnabled(DependencyObject element, bool value) | |
{ | |
element.SetValue(IsEnabledProperty, value); | |
} | |
public static bool GetIsEnabled(DependencyObject element) | |
{ | |
return (bool) element.GetValue(IsEnabledProperty); | |
} | |
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) | |
{ | |
if (d is Window window) | |
{ | |
if (true.Equals(e.OldValue)) | |
{ | |
GetWindowBlur(window)?.Detach(); | |
window.ClearValue(WindowBlurProperty); | |
} | |
if (true.Equals(e.NewValue)) | |
{ | |
var blur = new WindowBlur(); | |
blur.Attach(window); | |
window.SetValue(WindowBlurProperty, blur); | |
} | |
} | |
} | |
public static readonly DependencyProperty WindowBlurProperty = DependencyProperty.RegisterAttached( | |
"WindowBlur", typeof(WindowBlur), typeof(WindowBlur), | |
new PropertyMetadata(null, OnWindowBlurChanged)); | |
public static void SetWindowBlur(DependencyObject element, WindowBlur value) | |
{ | |
element.SetValue(WindowBlurProperty, value); | |
} | |
public static WindowBlur GetWindowBlur(DependencyObject element) | |
{ | |
return (WindowBlur) element.GetValue(WindowBlurProperty); | |
} | |
private static void OnWindowBlurChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) | |
{ | |
if (d is Window window) | |
{ | |
(e.OldValue as WindowBlur)?.Detach(); | |
(e.NewValue as WindowBlur)?.Attach(window); | |
} | |
} | |
private Window _window; | |
private void Attach(Window window) | |
{ | |
_window = window; | |
var source = (HwndSource) PresentationSource.FromVisual(window); | |
if (source == null) | |
{ | |
window.SourceInitialized += OnSourceInitialized; | |
} | |
else | |
{ | |
AttachCore(); | |
} | |
} | |
private void Detach() | |
{ | |
try | |
{ | |
DetachCore(); | |
} | |
finally | |
{ | |
_window = null; | |
} | |
} | |
private void OnSourceInitialized(object sender, EventArgs e) | |
{ | |
((Window) sender).SourceInitialized -= OnSourceInitialized; | |
AttachCore(); | |
} | |
private void AttachCore() | |
{ | |
EnableBlur(_window); | |
} | |
private void DetachCore() | |
{ | |
_window.SourceInitialized += OnSourceInitialized; | |
} | |
private static void EnableBlur(Window window) | |
{ | |
var windowHelper = new WindowInteropHelper(window); | |
var accent = new AccentPolicy | |
{ | |
AccentState = AccentState.ACCENT_ENABLE_BLURBEHIND | |
}; | |
var accentStructSize = Marshal.SizeOf(accent); | |
var accentPtr = Marshal.AllocHGlobal(accentStructSize); | |
Marshal.StructureToPtr(accent, accentPtr, false); | |
var data = new WindowCompositionAttributeData | |
{ | |
Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY, | |
SizeOfData = accentStructSize, | |
Data = accentPtr | |
}; | |
SetWindowCompositionAttribute(windowHelper.Handle, ref data); | |
Marshal.FreeHGlobal(accentPtr); | |
} | |
[DllImport("user32.dll")] | |
internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); | |
} | |
namespace Native | |
{ | |
internal enum AccentState | |
{ | |
ACCENT_DISABLED, | |
ACCENT_ENABLE_GRADIENT, | |
ACCENT_ENABLE_TRANSPARENTGRADIENT, | |
ACCENT_ENABLE_BLURBEHIND, | |
ACCENT_INVALID_STATE, | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
internal struct AccentPolicy | |
{ | |
public AccentState AccentState; | |
public int AccentFlags; | |
public int GradientColor; | |
public int AnimationId; | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
internal struct WindowCompositionAttributeData | |
{ | |
public WindowCompositionAttribute Attribute; | |
public IntPtr Data; | |
public int SizeOfData; | |
} | |
internal enum WindowCompositionAttribute | |
{ | |
// 省略其他未使用的字段 | |
WCA_ACCENT_POLICY = 19, | |
// 省略其他未使用的字段 | |
} | |
} | |
} |
本文会经常更新,请阅读原文: https://blog.walterlv.com/post/win10/2017/10/02/wpf-transparent-blur-in-windows-10.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com) 。