WPF ウィンドウ全体にマスク処理

あるWindowの上にAeroGlassのWindowが来ると、下のWindowがボケた感じになります。そんなボケた感じを2つのWindowを使わずに作ろうとするとどうしたらいいかなぁと思ったのがキッカケの実用性無視したエントリ。

Window自体をAeroGlassにはできますが今回目的としているその上のElement群には影響を与えられないので、方針は今あるElementの上にぼかしフィルタを付けること。今ある画面をキャプチャしてぼかしてやろうということです。

 

1.ぼかす対象を作る

ぼかす対象としてはGridを想定してみます。

 MainWindow.xaml

<Window x:Class="WindowMask.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:me="clr-namespace:WindowMask"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="420"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Content="Mask/Unmask" Grid.Column="1" Grid.Row="1"/>
        <Grid x:Name="MaskTargetGrid" Grid.ColumnSpan="2" Background="Gray">
            <Calendar HorizontalAlignment="Left" Margin="38,95,0,0" VerticalAlignment="Top"/>
            <CheckBox Content="CheckBox" HorizontalAlignment="Left" Margin="226,38,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.135,-0.667"/>
            <DataGrid HorizontalAlignment="Left" Height="62" Margin="267,70,0,0" VerticalAlignment="Top" Width="125"/>
            <Ellipse Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="127" Margin="421,95,0,0" Stroke="Black" VerticalAlignment="Top" Width="67"/>
            <Label Content="Label" HorizontalAlignment="Left" Margin="392,29,0,0" VerticalAlignment="Top"/>
            <Button Content="Button" HorizontalAlignment="Left" Margin="36,33,0,0" VerticalAlignment="Top" Width="75"/>
            <Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="274,159,0,0" Stroke="Black" VerticalAlignment="Top" Width="118"/>
        </Grid>
    </Grid>
</Window>

mainwindow

Mask/Unmaskボタンを押すと、灰色背景部分(Grid)がまるっとマスクが掛かるという想定。ボタンに処理を載せているのでセオリー通りICommandを実装するコマンドとしてマスク処理を作ります。

 

2.ぼかす処理を作る

MaskCommand.cs

namespace WindowMask
{
    using System;
    using System.Windows;
    using System.Windows.Media;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media.Imaging;
    using System.IO;
    using System.Drawing;
    using System.Drawing.Imaging;
    public class MaskCommand : ICommand
    {
        private bool masking;
        private Grid maskingGrid = new Grid();

        public bool CanExecute(object parameter){
            return parameter is Window;
        }

        public event EventHandler CanExecuteChanged{
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter){
            var window = parameter as Window;
            if (masking){
                UnMask(window);
            }
            else{
                Mask(window);
            }
            masking = !masking;
        }

        private void Mask(Window window)
        {
            // Render対象のGridの見た目を保存
            var grid = GetRenderTarget(window);
            var bitmap = new RenderTargetBitmap(
                (int)grid.ActualWidth, (int)grid.ActualHeight, 96, 96, PixelFormats.Pbgra32);
            bitmap.Render(window);
            var encoder = new BmpBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(bitmap));
            using(var originalGridVisualStream = new MemoryStream())
            using (var maskedGridVisualStream = new MemoryStream())
            {
                encoder.Save(originalGridVisualStream);
                // マスク処理
                using (Bitmap image = new Bitmap(originalGridVisualStream))
                {
                    var bluredImage = MaskEffect.Blur(image);
                    bluredImage.Save(maskedGridVisualStream, ImageFormat.Bmp);
                }
                originalGridVisualStream.Close();
                // マスク処理した画像をGridに張り、最前面へ配置
                WriteableBitmap maskedBitmap = new WriteableBitmap(BitmapFrame.Create(maskedGridVisualStream));
                maskingGrid.Background = new ImageBrush(maskedBitmap);
                grid.Children.Add(maskingGrid);
            }
        }

        private void UnMask(Window window)
        {
            // 設定したマスクを除去するだけ
            var grid = GetRenderTarget(window);
            grid.Children.Remove(maskingGrid);
        }

        private Grid GetRenderTarget(Window window)
        {
            // ここでは、Windowの2階層下にあるGridを前提とする
            var grid = window.Content as Grid;
            foreach (var element in grid.Children)
            {
                if (element is Grid){
                    return element as Grid;
                }
            }
            throw new InvalidProgramException("Render対象のGridがない");
        }
    }
}

Mask処理がポイントです。今の見た目をBitmapとして保存→画像処理してぼかす→Gridに張り付けて最前面配置という作用をしています。

具体的なマスク処理は適当に書くと↓な感じ。ポインタ操作をしているのでプロジェクトのプロパティのビルドタブでアンセーフコードの実行を許可する必要があります。

MaskEffect.cs

namespace WindowMask
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Linq;
    using System.Text;

    public class MaskEffect
    {
        public unsafe static Bitmap Blur(Bitmap srcImage)
        {
            Bitmap dstImage = new Bitmap(srcImage);
            var srcData = srcImage.LockBits(new Rectangle(Point.Empty, srcImage.Size),
                ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb);
            var dstData = dstImage.LockBits(new Rectangle(Point.Empty, dstImage.Size),
                ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb);
            byte* pbySrc = (byte*)srcData.Scan0;
            byte* pbyDst = (byte*)dstData.Scan0;

            // ぼかし処理(例)
            int operatorSize = 10;
            for (int y = operatorSize; y < srcImage.Height - operatorSize; y++){
                for (int x = operatorSize; x < srcImage.Width - operatorSize; x++){
                    for (int i = 0; i < 4; i++){
                        int sum = 0;
                        for (int offsety = -operatorSize; offsety <= operatorSize; offsety++){
                            for(int offsetx = -operatorSize; offsetx <= operatorSize; offsetx++){
                                sum += pbySrc[(y+offsety) * srcData.Stride + 4 * (x+offsetx) + i];
                            }
                        }
                        pbyDst[y*dstData.Stride+4*x+i] = (byte)( sum / (2*operatorSize+1) / (2*operatorSize+1) );
                    }
                }
            }

            srcImage.UnlockBits(srcData);
            dstImage.UnlockBits(dstData);
            return dstImage;
        }
    }
}

コマンドとして作成したのでこれを元のWindowのMask/Unmaskボタンに割り当てます。

 MainWindow.xaml(改)

<Window x:Class="WindowMask.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:me="clr-namespace:WindowMask"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <me:MaskCommand x:Key="maskCommand"/>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="420"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Content="Mask/Unmask" Grid.Column="1" Grid.Row="1"
                Command="{StaticResource maskCommand}"
                CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"/>
        <Grid x:Name="MaskTargetGrid" Grid.ColumnSpan="2" Background="Gray">
            <Calendar HorizontalAlignment="Left" Margin="38,95,0,0" VerticalAlignment="Top"/>
            <CheckBox Content="CheckBox" HorizontalAlignment="Left" Margin="226,38,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.135,-0.667"/>
            <DataGrid HorizontalAlignment="Left" Height="62" Margin="267,70,0,0" VerticalAlignment="Top" Width="125"/>
            <Ellipse Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="127" Margin="421,95,0,0" Stroke="Black" VerticalAlignment="Top" Width="67"/>
            <Label Content="Label" HorizontalAlignment="Left" Margin="392,29,0,0" VerticalAlignment="Top"/>
            <Button Content="Button" HorizontalAlignment="Left" Margin="36,33,0,0" VerticalAlignment="Top" Width="75"/>
            <Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="274,159,0,0" Stroke="Black" VerticalAlignment="Top" Width="118"/>
        </Grid>
    </Grid>
</Window>

これで実行してMask/Unmaskを押すと、以下のような感じ。

maskedwindow

WPFは表現力が優れているのはわかるけどElementの境界を越えて見た目を変更する・・・というのはこんな方法しかないんじゃないかな。だから何?という突っ込みは禁止です。

11/11追記
ぼかすだけならGrid.EffectにBlurEffectをかければもっとスマートにできますね。今回のテクニックは、Element領域を超えた描画をしたいとか、要素全体に対してイベントハンドラを無効化したいとかいった場合に使い道がある。。。かも、なさそう・・・。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中