WPF无边框窗口的DropShadow效果

28

我有一个WPF窗口,将WindowStyle设置为none。有没有办法强制该窗口产生一个阴影(就像WindowStyle不为none时获得的那个)?我不想将AllowTransparency设置为true,因为它会影响性能。我也不想禁用硬件渲染(我在某个地方读到透明度使用禁用硬件渲染可以更好地执行)。

4个回答

39
我写了一个小型的实用程序类,可以完全做到你想要的效果:在没有边框的 Window 上投射标准阴影,但使 AllowsTransparency 属性设置为 false
你只需要调用 DropShadowToWindow(Window window) 方法。最好是在窗口的构造函数中的 InitializeComponent() 方法后立即调用此方法,但即使在窗口显示后调用它也可以正常工作。
using System;
using System.Drawing.Printing;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

public static class DwmDropShadow
{
    [DllImport("dwmapi.dll", PreserveSig = true)]
    private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);

    [DllImport("dwmapi.dll")]
    private static extern int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref Margins pMarInset);

    /// <summary>
    /// Drops a standard shadow to a WPF Window, even if the window is borderless. Only works with DWM (Windows Vista or newer).
    /// This method is much more efficient than setting AllowsTransparency to true and using the DropShadow effect,
    /// as AllowsTransparency involves a huge performance issue (hardware acceleration is turned off for all the window).
    /// </summary>
    /// <param name="window">Window to which the shadow will be applied</param>
    public static void DropShadowToWindow(Window window)
    {
        if (!DropShadow(window))
        {
            window.SourceInitialized += new EventHandler(window_SourceInitialized);
        }
    }

    private static void window_SourceInitialized(object sender, EventArgs e)
    {
        Window window = (Window)sender;

        DropShadow(window);

        window.SourceInitialized -= new EventHandler(window_SourceInitialized);
    }

    /// <summary>
    /// The actual method that makes API calls to drop the shadow to the window
    /// </summary>
    /// <param name="window">Window to which the shadow will be applied</param>
    /// <returns>True if the method succeeded, false if not</returns>
    private static bool DropShadow(Window window)
    {
        try
        {
            WindowInteropHelper helper = new WindowInteropHelper(window);
            int val = 2;
            int ret1 = DwmSetWindowAttribute(helper.Handle, 2, ref val, 4);

            if (ret1 == 0)
            {
                Margins m = new Margins { Bottom = 0, Left = 0, Right = 0, Top = 0 };
                int ret2 = DwmExtendFrameIntoClientArea(helper.Handle, ref m);
                return ret2 == 0;
            }
            else
            {
                return false;
            }
        }
        catch (Exception ex)
        {
            // Probably dwmapi.dll not found (incompatible OS)
            return false;
        }
    }
}

2
这很棒,但我遇到了一个问题,打开一个带有阴影的子窗口时,它会减少父窗口的阴影,然后当子窗口关闭时,完全删除它。奇怪的错误,尚未找到原因。当子窗口不使用阴影时,似乎也会出现这种情况。 - Stephen Price
2
注意:当我尝试在本地应用程序中使用此方法时,阴影直到我将边距设置为非零值后才出现。(对于无边框窗口,实际值似乎并不重要,只要它们与0不同即可)。 - Nikita Nemkin
我想补充一下:DWM似乎会进行一些图像处理,因此不能保证你会有一个阴影。主要是,如果你的窗口边缘很暗,它会给你一个弱的阴影(可能是因为暗色发光体+暗色矩形很丑)。我尝试将背景设置为白色,并用深色控件覆盖它,但没有帮助(可能是因为他们使用窗口的扁平化图像,而不是读取其背景颜色)。 - Warty
2
虽然这种方法很有前途,但它还存在另一个问题。如果您切换离开应用程序,然后再切回来,阴影就会消失。 - Gábor
2
不要使用System.Drawing.Printing.Margins,它会在某些机器上导致奇怪的图形故障。相反,像@Omer Ran建议的那样,在本地定义结构体。 - Kelly Elton
显示剩余5条评论

7

Patrick的答案很好,但当一个win32窗口被托管时会出现问题。 当发生这种情况时,你会注意到托管的窗口被“洗脸”了(看起来像Windows将“玻璃板”效果应用于整个托管窗口)。 当在本地定义结构时,可以解决这种奇怪的行为,例如:

[StructLayout(LayoutKind.Sequential)]
public struct Margins
{
    public int Left;
    public int Right;
    public int Top;
    public int Bottom;
}  

2
这是一个非常好的观点。值得注意的是,这个问题只在少数机器上可重现,所以它特别棘手。 - Kelly Elton
到目前为止,我注意到在Intel HD 520显卡上出现了这个问题,以及其他笔记本电脑上的其他Intel HD显卡。 - Kelly Elton
另外,如果您将“Margins”转换为“class”,那么问题就会再次出现。这必须与“System.Drawing.Printing”也是一个“class”有关。 - Kelly Elton

3
如果您允许窗口具有调整大小边框,可以通过将ResizeMode设置为CanResize来实现,那么您将获得操作系统的阴影效果。然后,您可以将MaxWidthMinWidthMaxHeightMinHeight设置为防止调整大小的值。
如果您有一个没有样式的无边框窗口,您将需要在自己的可视树中提供窗口的所有外观,包括阴影,因为这种组合设置与说您不希望操作系统提供的一样。
编辑:
从那个点开始,如果您的窗口大小是固定的,只需添加阴影,可能作为<Canvas/>内容中的第一个元素,就像这样:
<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" AllowsTransparency="True" Background="Transparent" WindowStyle="None">
    <Canvas>
        <Rectangle Fill="#33000000" Width="100"  Height="100"/>
        <Rectangle Fill="#FFFF0000" Width="95"  Height="95" />
    </Canvas>
</Window>

请注意,第一个RectangleFill属性是部分透明的,您也可以使用RectangleOpacity属性来实现。您可以使用自己的图形或不同的形状来自定义投影的外观。
请注意,这违反了要求将AllowsTransparency设置为False的要求,但您别无选择:如果您想要透明度,您必须允许它。

关于编辑:我确实尝试过类似的方法,但它会严重影响性能。我是在Windows XP上进行操作的,不知道Vista/7会怎样。 - TripShock
你不必在设置“AllowsTransparency”为“False”和能够投影阴影之间做出选择。你可以将“AllowsTransparency”设置为“True”,只需在主窗口边缘的4个窗口中设置,这些窗口将负责投影阴影。这样,你的主窗口性能将保持完好。 - cprcrack

0
为什么不使用相同的对象,但创建一个更大、位于其后面的阴影来代替“窗口”呢?
<Window x:Class="WPF_Custom_Look.ShadowWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="ShadowWindow" Height="400" Width="450" ResizeMode="NoResize" Background="Transparent" AllowsTransparency="True" WindowStyle="None">
<Grid>
    <Rectangle Fill="Black" Width="330" Opacity="0.5" Height="279">
        <Rectangle.Effect>
            <BlurEffect Radius="30"/>
        </Rectangle.Effect>
    </Rectangle>
    <Rectangle Fill="#FFFDFDFD" Width="312"  Height="260"/>

</Grid>

如果您需要一个透明的标题栏,可以用<Border>替换它

<Canvas>
    <Border BorderBrush="Black" BorderThickness="7" Height="195" Width="304" Canvas.Left="53" Canvas.Top="25">
        <Border.Effect>
            <BlurEffect Radius="20"/>
        </Border.Effect>
    </Border>
    <Rectangle Fill="#FF86B0F9" Width="285"  Height="177" Opacity="0.7" Canvas.Left="62" Canvas.Top="34" MouseDown="Border_MouseDown"/>
    <Rectangle Fill="#FFFDFDFD" Width="285"  Height="143" Canvas.Left="62" Canvas.Top="68"/>
</Canvas>

编辑:我刚注意到OP想要AllowsTransparency设置为False。 我看不到一个阴影可以在没有它被设置为“True”的情况下工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接