SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

Windows10 1903 insider previewでアクリル化のAPIがおかしい・・・

ちょっと前に、以下のようなIssueをもらったので、見ていました。

近々リリースされる予定のWindows10 1903環境で、FluentWPFのAcrylicWindowの動作がおかしいとのこと。
ウィンドウをドラッグすると、UIの応答が非常に悪くなり、ウィンドウの移動がマウス操作に追従しなくなるようです。

続きを読む

Lighty 0.3.2をリリースしました

以前、WPFでウィンドウ内にLightBox表示のようなダイアログ表示をするライブラリを作ったのですが、久々の更新をしました。

バグ修正のプルリクいただいたんで、マージしてv0.3.2としてリリースしました。

修正内容

修正内容は以下の1点のみです。

  • ShowAsyncメソッドでダイアログ表示する際、背景部分をクリックしてもダイアログを閉じれない問題を修正

最近、全然更新できてなかったけど、他のライブラリもIssuesなどたまってきているので、色々対応していきたいなぁ、、、というところ。

Visual Studio 2019をインストールしてみた

リリースされたんでさっそくインストールしてみました。
メモリ消費がだいぶ少なくなったり起動周りのパフォーマンス向上とか、地味にうれしい点が多いですね。

コンパイラの言語バージョンをC# 8.0(beta)とかにしておいて、
f:id:minami_SC:20190406183404p:plain
#nullable enable指定をつけておけば、null許容参照型の機能も使えます。
f:id:minami_SC:20190406182929p:plain

C#8.0はまだプレビュー扱いですが、今のうちに少しずつこういった機能にも慣れておこうかな、と思います。

あと、Xamarin.Formsもこんな風にプレビュー表示とプロパティ表示が出るようになってますね。
f:id:minami_SC:20190406185000p:plain
そろそろXamarinに本腰入れて取り組みたい、、、と思ったり。

FluentWPF 0.6.1をリリースしました

FluentWPF 0.6.0をリリースしました。
Release v0.6.1 · sourcechord/FluentWPF · GitHub

今回の変更内容は以下の通り。

  • 新機能
    • アクリル効果付きのメニュー・・・AcrylicMenuStyle
    • TextBox/PasswordBox用の添付プロパティ追加・・・Extensions.PlaceholderText, Extensions.Header
  • バグフィックス
    • AcrylicWindowのウィンドウ枠の色が、OSのテーマカラーに追従しない不具合を修正
    • ButtonRevealStyle使用時に起きるバインディングエラーを修正
続きを読む

WPFでアクリルなコンテキストメニューを作ってみた

この記事は、C# Advent Calendar 2018の4日目の記事です。

C#というかWPFXAMLに特化したネタで失礼します。
Windows10ではOSデザインの刷新として、FluentDesignSystemが段階的に導入されてきてます。
そんなFluentDesignのルック&フィールにマッチするような、半透明のアクリル効果が効いたコンテキストメニューWPF向けに作ってみました。

アクリルなコンテキストメニュー

まずは、出来上がったコンテキストメニューの動作を、以下のgifアニメで見てください。
f:id:minami_SC:20181204231238g:plain
ほんのりと、背景が透けたメニューになっていることがわかるかと思います。

あと、メニュー左側のアイコンとかチェック状態を表示する部分で、ドロップシャドウを使ってうっすらと立体感を出してるのがこだわりポイント!!

ContextMenu AcrylicContextMenu
f:id:minami_SC:20181204231348p:plain f:id:minami_SC:20181204231357p:plain

こういうコンテキストメニューを、WPFアプリで簡単に使えるようにライブラリ化しました。

1年ほど前から、WPF向けにFluent DesignXAML上で力技で再現したFluentWPFというライブラリを公開しているのですが、この中にAcrylicContextMenuというコントロールを追加してます。

使い方

まずは、このAcrylicContextMenuの使い方から説明します。

1. WPFアプリのプロジェクトを作り、nugetでFluentWPFと検索し、パッケージをインストールします。 f:id:minami_SC:20181204232409p:plain
2. XAML名前空間の宣言に、以下の記述を追加します。

xmlns:fw="clr-namespace:SourceChord.FluentWPF;assembly=FluentWPF"

3. 通常のContextMenuのかわりにAcrylicContextMenuを配置します。

            <Grid Background="Transparent">
                <Grid.ContextMenu>
                    <fw:AcrylicContextMenu>
                        <MenuItem Header="MenuItem1"/>
                        <MenuItem Header="MenuItem2"/>
                        <MenuItem Header="MenuItem3" IsEnabled="False"/>
                        <MenuItem Header="MenuItem4"/>
                        <Separator />
                        <MenuItem Header="MenuItem5" IsCheckable="True" IsChecked="True"/>
                        <MenuItem Header="MenuItem6">
                            <MenuItem.Icon>
                                <Rectangle Margin="2" Fill="DarkRed"/>
                            </MenuItem.Icon>
                        </MenuItem>
                        <Separator />
                        <MenuItem Header="MenuItem7"/>
                        <MenuItem Header="Sub" >
                            <MenuItem Header="SubMenuItem1"/>
                            <MenuItem Header="SubMenuItem2"/>
                            <Separator />
                            <MenuItem Header="SubMenuItem3" IsCheckable="True" IsChecked="True"/>
                            <Separator />
                            <MenuItem Header="Sub" >
                                <MenuItem Header="Item1"/>
                                <MenuItem Header="Item2"/>
                            </MenuItem>
                        </MenuItem>
                    </fw:AcrylicContextMenu>
                </Grid.ContextMenu>
            </Grid>

これで、以下のようなメニューが表示できます。
f:id:minami_SC:20181204231427p:plain

アクリルなコンテキストメニューの作り方

コンテキストメニューにアクリル効果を適用するために、ライブラリ内部でやっていることも説明したいと思います。

WPFコンテキストメニューは、元のウィンドウとは別のウィンドウハンドルを持ったものとして画面に描画されています。
f:id:minami_SC:20181204231617p:plain

コンテキストメニューを半透過にするには、このメニュー用の別ウィンドウに対して、「ウィンドウのアクリル化」処理を行う、ということが基本的な方針となります。

ウィンドウのアクリル化

ウィンドウにFluentDesignのアクリル効果を適用するには、Win32APIの非公開APIのSetWindowCompositionAttributeという関数を使用します。

このAPIでウィンドウを半透明にしたら、あとはウィンドウのAllowsTransparencyプロパティをTrueにしたり、背景色をTransparentにするなどして、WPFのUIレイヤーで描画する背景を透明にすれば出来上がり!!

細かい説明は、以前書いた↓の記事をご参照ください。
WPFでFluent Design Systemのアクリルっぽいウィンドウを作ってみる - SourceChord

コンテキストメニューのアクリル化

先ほど書いた通り、WPFコンテキストメニューは、WPFで描画している元のウィンドウとは別のウィンドウが新たに作られ、そのウィンドウ上にメニューの内容が表示されます。
WPFのメニューでアクリルな表示をするには、このメニューのウィンドウハンドルを用いて、SetWindowCompositionAttribute関数を呼び出せば良い、、、ということになります。

ContextMenu用のウィンドウが作られるタイミングは、OnOpenedというイベントで捕らえることができます。
このメソッドをオーバーライドして、SetWindowCompositionAttribute関数を呼び出すことで、コンテキストメニューをアクリル化します。

以下のようなContextMenu派生クラスを作り、、、

    public class AcrylicContextMenu : ContextMenu
    {
        [StructLayout(LayoutKind.Sequential)]
        internal struct WindowCompositionAttributeData
        {
            public WindowCompositionAttribute Attribute;
            public IntPtr Data;
            public int SizeOfData;
        }

        internal enum WindowCompositionAttribute
        {
            WCA_ACCENT_POLICY = 19
        }

        internal enum AccentState
        {
            ACCENT_DISABLED = 0,
            ACCENT_ENABLE_GRADIENT = 1,
            ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
            ACCENT_ENABLE_BLURBEHIND = 3,
            ACCENT_INVALID_STATE = 4
        }

        [StructLayout(LayoutKind.Sequential)]
        internal struct AccentPolicy
        {
            public AccentState AccentState;
            public int AccentFlags;
            public uint GradientColor;
            public int AnimationId;
        }

        protected override void OnOpened(RoutedEventArgs e)
        {
            base.OnOpened(e);

            var hwnd = (HwndSource)HwndSource.FromVisual(this);
            // ウィンドウハンドルを指定して、ウィンドウのアクリル化を行う。
            EnableBlur(hwnd.Handle);
        }

        [DllImport("user32.dll")]
        internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data);

        internal static void EnableBlur(IntPtr hwnd)
        {
            var accent = new AccentPolicy();
            var accentStructSize = Marshal.SizeOf(accent);
            accent.AccentState = AccentState.ACCENT_ENABLE_BLURBEHIND;
            accent.AccentFlags = 2;
            //               ↓の色はAABBGGRRの順番で設定する
            accent.GradientColor = 0x99FFFFFF;  // 60%の透明度が基本

            var accentPtr = Marshal.AllocHGlobal(accentStructSize);
            Marshal.StructureToPtr(accent, accentPtr, false);

            var data = new WindowCompositionAttributeData();
            data.Attribute = WindowCompositionAttribute.WCA_ACCENT_POLICY;
            data.SizeOfData = accentStructSize;
            data.Data = accentPtr;

            SetWindowCompositionAttribute(hwnd, ref data);

            Marshal.FreeHGlobal(accentPtr);
        }
    }

XAML側でこんな風に使えば、アクリルな半透過効果がかかったコンテキストメニューを表示できます。

    <Grid Background="Transparent">
        <Grid.ContextMenu>
            <local:AcrylicContextMenu Background="Transparent" HasDropShadow="False">
                <MenuItem Header="MenuItem1" />
                <MenuItem Header="MenuItem2" />
                <MenuItem Header="MenuItem3" />
                <MenuItem Header="MenuItem4" />
                <Separator Background="#44000000" Foreground="#CC000000" />
                <MenuItem Header="MenuItem5" />
                <MenuItem Header="MenuItem6" />
                <Separator Background="#44000000" Foreground="#CC000000" />
                <MenuItem Header="MenuItem7" />
            </local:AcrylicContextMenu>
        </Grid.ContextMenu>
    </Grid>

f:id:minami_SC:20181204231813p:plain
実際には、もうちょっとスタイルの作り込みなども必要ですが、メニューにアクリル効果を適用するための手順について、なんとなく雰囲気はつかめるのではないでしょうか。

サブメニューのアクリル化

先ほどの手順を行えば、コンテキストメニューを最初に開いたときの表示はアクリル化ができます。
しかし、コンテキストメニューでは、サブメニューを持つこともあります。
サブメニューの部分は、また別のウィンドウハンドルを持ったものとなっており、別途アクリル化をしなければなりません。
f:id:minami_SC:20181204231836p:plain

そこで、WPF標準のMenuやContextMenuなどのスタイルを見てみると、このサブメニュー部分はPopupコントロールとして描画されていることがわかります。

Menuコントロール内で、サブメニュー表示を行う部分のスタイル定義を抜粋
            <Border x:Name="templateRoot" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
                <Grid Margin="-1">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition MinWidth="22" SharedSizeGroup="MenuItemIconColumnGroup" Width="Auto"/>
                        <ColumnDefinition Width="13"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="30"/>
                        <ColumnDefinition SharedSizeGroup="MenuItemIGTColumnGroup" Width="Auto"/>
                        <ColumnDefinition Width="20"/>
                    </Grid.ColumnDefinitions>
                    <ContentPresenter x:Name="Icon" Content="{TemplateBinding Icon}" ContentSource="Icon" HorizontalAlignment="Center" Height="16" Margin="3" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center" Width="16"/>
                    <Border x:Name="GlyphPanel" BorderBrush="#FF26A0DA" BorderThickness="1" Background="#3D26A0DA" Height="22" Margin="-1,0,0,0" Visibility="Hidden" VerticalAlignment="Center" Width="22">
                        <Path x:Name="Glyph" Data="F1M10,1.2L4.7,9.1 4.5,9.1 0,5.2 1.3,3.5 4.3,6.1 8.3,0 10,1.2z" Fill="#FF212121" FlowDirection="LeftToRight" Height="11" Width="9"/>
                    </Border>
                    <ContentPresenter ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" Grid.Column="2" ContentStringFormat="{TemplateBinding HeaderStringFormat}" ContentSource="Header" HorizontalAlignment="Left" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="Center"/>
                    <TextBlock Grid.Column="4" Margin="{TemplateBinding Padding}" Opacity="0.7" Text="{TemplateBinding InputGestureText}" VerticalAlignment="Center"/>
                    <Path x:Name="RightArrow" Grid.Column="5" Data="M0,0L4,3.5 0,7z" Fill="#FF212121" HorizontalAlignment="Left" Margin="10,0,0,0" VerticalAlignment="Center"/>
                    <Popup x:Name="PART_Popup" AllowsTransparency="True" Focusable="False" HorizontalOffset="-2" IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}" PopupAnimation="{DynamicResource {x:Static SystemParameters.MenuPopupAnimationKey}}" Placement="Right" VerticalOffset="-3">
                        <Border x:Name="SubMenuBorder" BorderBrush="#FF999999" BorderThickness="1" Background="#FFF0F0F0" Padding="2">
                            <ScrollViewer x:Name="SubMenuScrollViewer" Style="{DynamicResource {ComponentResourceKey ResourceId=MenuScrollViewer, TypeInTargetAssembly={x:Type FrameworkElement}}}">
                                <Grid RenderOptions.ClearTypeHint="Enabled">
                                    <Canvas HorizontalAlignment="Left" Height="0" VerticalAlignment="Top" Width="0">
                                        <Rectangle x:Name="OpaqueRect" Fill="{Binding Background, ElementName=SubMenuBorder}" Height="{Binding ActualHeight, ElementName=SubMenuBorder}" Width="{Binding ActualWidth, ElementName=SubMenuBorder}"/>
                                    </Canvas>
                                    <Rectangle Fill="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" HorizontalAlignment="Left" Margin="29,2,0,2" Width="1"/>
                                    <ItemsPresenter x:Name="ItemsPresenter" KeyboardNavigation.DirectionalNavigation="Cycle" Grid.IsSharedSizeScope="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" KeyboardNavigation.TabNavigation="Cycle"/>
                                </Grid>
                            </ScrollViewer>
                        </Border>
                    </Popup>
                </Grid>
            </Border>

しかし、標準のPopupコントロールはもちろんアクリルな半透過表示には対応してません。
ということで、アクリル効果の効いたサブメニューを作るには、別途アクリル効果に対応したPopupコントロールが必要になります。
詳細なコードについては省略しますが、今回AcrylicPopupという、アクリルな表示をするPopup派生コントロールを作ってみました。

こんな感じのコントロールです。

                <fw:AcrylicPopup x:Name="popupAcrylic" PopupAnimation="None" Placement="Bottom" PlacementTarget="{Binding ElementName=toggleAcrylicPopup}">
                    <Border Width="200" Height="75"
                            BorderBrush="LightGray"
                            BorderThickness="1"
                            Background="{DynamicResource SystemAltMediumLowColorBrush}">
                        <TextBlock Text="This is AcrylicPopup" />
                    </Border>
                </fw:AcrylicPopup>

f:id:minami_SC:20181204231856p:plain
AcrylicContextMenuのスタイル定義の中で、このAcrylicPopupクラスを標準Popupのかわりに用いることで、サブメニューまで含めてアクリル化しました。

最後に

今回、こんな感じでContextMenuから派生した、独自のコンテキストメニューを作ってみました。
WPFのContextMenuなどのコントロールは、メインのウィンドウとは異なるウィンドウハンドルを持った要素が出てくるため、コントロールのカスタマイズやスタイル定義などもちょっと面倒なコントロールと感じます。

ということで、こういう表示をお手軽に試してみたい場合には、記事冒頭で紹介した拙作のFluentWPFライブラリ、よかったら使ってみてくださいませ♪
XAML Islandsなんてのも出てきて、WPFからもUWPコントロールを使えるようになってきてますが、このライブラリも当面は開発も続けようと思ってます!!

FluentWPF 0.4.1をリリースしました

今回はちょっとしたバグフィックスなど、軽めの更新です。

Release v0.4.1 · sourcechord/FluentWPF · GitHub

変更内容

  • AcrylicWindow

    • Windows7や8.x環境でも、ウィンドウのタイトルバー右側のアイコンが表示されるよう対応
  • Button

    • RevealButtonStyle/RevealAccentButtonStyle適用時の、ボタンDisable時の表示を改善
    • IsEnabeld="False"としても、UIの無効表示がわかりにくかった点を修正
続きを読む