Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WINAPI/DWMAPI Blur-behind window with irregular shape

Tags:

c#

winapi

dwm

NB: THIS IS NOT A QUESTION ABOUT A BORDERLESS WINDOW.

So, I stumbled upon this program while I was exploring my Start menu the other day on Windows 7:

Math input panel

It's a native Windows program, called "Math Input Panel." Now, I'm curious about the window shape. I know that it's not completely drawn by DWM, because the borders and Close button look fishy and the window has no drop shadow (I have drop shadows enabled). My first guess as to how this was made would be using DwmEnableBlurBehindWindow, but I can't imagine that works on irregular window shapes, does it? (Or is there another way to do this, or is it just completely Microsoft sorcery?)

like image 813
rookie1024 Avatar asked Jan 05 '15 21:01

rookie1024


2 Answers

Here's a quickly hacked together WPF solution. It uses the hRgnBlur of the DWM_BLURBEHIND structure, and some interop.

This example will apply an ellipse-shaped background blur on the window.

You can easily convert this to an attached property or behavior for MVVM-friendliness. It's also a good idea to listen to the WM_DWMCOMPOSITIONCHANGED message and reapply the blur if needed.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        WindowStyle = WindowStyle.None;
        AllowsTransparency = true;

        SourceInitialized += OnSourceInitialized;
    }

    private void OnSourceInitialized(object sender, EventArgs eventArgs)
    {
        if (!NativeMethods.DwmIsCompositionEnabled())
            return;

        var hwnd = new WindowInteropHelper(this).Handle;

        var hwndSource = HwndSource.FromHwnd(hwnd);
        var sizeFactor = hwndSource.CompositionTarget.TransformToDevice.Transform(new Vector(1.0, 1.0));

        Background = System.Windows.Media.Brushes.Transparent;
        hwndSource.CompositionTarget.BackgroundColor = Colors.Transparent;

        using (var path = new GraphicsPath())
        {
            path.AddEllipse(0, 0, (int)(ActualWidth * sizeFactor.X), (int)(ActualHeight * sizeFactor.Y));

            using (var region = new Region(path))
            using (var graphics = Graphics.FromHwnd(hwnd))
            {
                var hRgn = region.GetHrgn(graphics);

                var blur = new NativeMethods.DWM_BLURBEHIND
                {
                    dwFlags = NativeMethods.DWM_BB.DWM_BB_ENABLE | NativeMethods.DWM_BB.DWM_BB_BLURREGION | NativeMethods.DWM_BB.DWM_BB_TRANSITIONONMAXIMIZED,
                    fEnable = true,
                    hRgnBlur = hRgn,
                    fTransitionOnMaximized = true
                };

                NativeMethods.DwmEnableBlurBehindWindow(hwnd, ref blur);

                region.ReleaseHrgn(hRgn);
            }
        }
    }

    [SuppressUnmanagedCodeSecurity]
    private static class NativeMethods
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct DWM_BLURBEHIND
        {
            public DWM_BB dwFlags;
            public bool fEnable;
            public IntPtr hRgnBlur;
            public bool fTransitionOnMaximized;
        }

        [Flags]
        public enum DWM_BB
        {
            DWM_BB_ENABLE = 1,
            DWM_BB_BLURREGION = 2,
            DWM_BB_TRANSITIONONMAXIMIZED = 4
        }

        [DllImport("dwmapi.dll", PreserveSig = false)]
        public static extern bool DwmIsCompositionEnabled();

        [DllImport("dwmapi.dll", PreserveSig = false)]
        public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind);
    }
}

Used with the following XAML:

<Window x:Class="WpfTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="200" Width="300" WindowStartupLocation="CenterScreen">

    <Border Background="#800000FF" Margin="30">
        <TextBlock Text="Hello, world!" VerticalAlignment="Center" HorizontalAlignment="Center" />
    </Border>
</Window>

The result is:

blur example

like image 124
Lucas Trzesniewski Avatar answered Oct 19 '22 18:10

Lucas Trzesniewski


So, unbeknownst to me, hRgn can take an irregular shape (and DwmEnableBlurBehindWindow takes an hRgn, but I knew that). So, here's my solution that's (more or less) compatible with WPF:

My own custom-shaped glass window

...and source code:

MainWindow.xaml:

<Window x:Class="IrregularGlassWindow.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="500"
        Width="500"
        Background="#01FFFFFF"
        AllowsTransparency="True"
        WindowStyle="None"
        ResizeMode="NoResize">
  <Window.Clip>
    <PathGeometry>
      <PathFigure StartPoint="250,0">
        <ArcSegment Point="250,500"
                    RotationAngle="180"
                    Size="250,250"
                    SweepDirection="Clockwise" />
        <ArcSegment Point="250,0"
                    RotationAngle="180"
                    Size="250,250"
                    SweepDirection="Clockwise" />
      </PathFigure>
    </PathGeometry>
  </Window.Clip>
  <Grid>
    <Ellipse Margin="1"
             Width="498"
             Height="498"
             Stroke="#8FFF"
             StrokeThickness="1.25" />
    <Ellipse Width="500"
             Height="500"
             Stroke="#C000"
             StrokeThickness="1"/>
  </Grid>
</Window>

MainWindow.xaml.cs:

public partial class MainWindow : Window {
  public MainWindow() {
    InitializeComponent();

    this.SourceInitialized += MainWindow_SourceInitialized;
    this.KeyDown += MainWindow_KeyDown;
  }

  void MainWindow_KeyDown(object sender, KeyEventArgs e) {
    if (e.Key == Key.Escape) this.Close();
  }

  void MainWindow_SourceInitialized(object sender, EventArgs e) {
    var helper = new WindowInteropHelper(this);
    var hwnd = helper.Handle;
    var src = HwndSource.FromHwnd(hwnd);

    src.CompositionTarget.BackgroundColor = Colors.Transparent;

    WindowChrome.SetWindowChrome(this, new WindowChrome {
      CaptionHeight = 500,
      CornerRadius = new CornerRadius(0),
      GlassFrameThickness = new Thickness(0),
      NonClientFrameEdges = NonClientFrameEdges.None,
      ResizeBorderThickness = new Thickness(0),
      UseAeroCaptionButtons = false
    });

    GraphicsPath path = new GraphicsPath(FillMode.Alternate);
    path.StartFigure();
    path.AddArc(new RectangleF(0, 0, 500, 500), 0, 360);
    path.CloseFigure();

    var dbb = new DwmBlurBehind(true);
    dbb.SetRegion(Graphics.FromHwnd(hwnd), new Region(path));
    DwmApi.DwmEnableBlurBehindWindow(hwnd, ref dbb);
  }
}

I think somebody else beat me to it, but here's how my solution works:

When the window's SourceInitialized event is fired, that means that we have a handle for our window. So in the handler of this function, I get the window handle. Then I make a call to a function I imported from dwmapi.dll called DwmEnableBlurBehindWindow. This basically turns transparent areas of the window into glass for a certain region. The DwmBlurBehind struct I got from pinvoke.net, and it converts a GDI+ System.Drawing.Region into an hRgn. The hRgn is passed to DwmEnableBlurBehindWindow, and it clips the transparent parts to the Region. In this case, I used a circle. Then the XAML is just the accent borders. It's worth noting that, for some reason, setting Window.Background to Transparent doesn't enable hit-testing when AllowsTransparency is true here. No idea why, but it probably has something to do with the code-behind.

like image 4
rookie1024 Avatar answered Oct 19 '22 18:10

rookie1024