WPFでアクリルなコンテキストメニューを作ってみた
この記事は、C# Advent Calendar 2018の4日目の記事です。
C#というかWPFやXAMLに特化したネタで失礼します。
Windows10ではOSデザインの刷新として、FluentDesignSystemが段階的に導入されてきてます。
そんなFluentDesignのルック&フィールにマッチするような、半透明のアクリル効果が効いたコンテキストメニューをWPF向けに作ってみました。
アクリルなコンテキストメニュー
まずは、出来上がったコンテキストメニューの動作を、以下のgifアニメで見てください。
ほんのりと、背景が透けたメニューになっていることがわかるかと思います。
あと、メニュー左側のアイコンとかチェック状態を表示する部分で、ドロップシャドウを使ってうっすらと立体感を出してるのがこだわりポイント!!
ContextMenu | AcrylicContextMenu |
---|---|
こういうコンテキストメニューを、WPFアプリで簡単に使えるようにライブラリ化しました。
1年ほど前から、WPF向けにFluent DesignをXAML上で力技で再現したFluentWPFというライブラリを公開しているのですが、この中にAcrylicContextMenuというコントロールを追加してます。
使い方
まずは、このAcrylicContextMenuの使い方から説明します。
1. WPFアプリのプロジェクトを作り、nugetでFluentWPF
と検索し、パッケージをインストールします。
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>
これで、以下のようなメニューが表示できます。
アクリルなコンテキストメニューの作り方
コンテキストメニューにアクリル効果を適用するために、ライブラリ内部でやっていることも説明したいと思います。
WPFのコンテキストメニューは、元のウィンドウとは別のウィンドウハンドルを持ったものとして画面に描画されています。
コンテキストメニューを半透過にするには、このメニュー用の別ウィンドウに対して、「ウィンドウのアクリル化」処理を行う、ということが基本的な方針となります。
ウィンドウのアクリル化
ウィンドウに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>
実際には、もうちょっとスタイルの作り込みなども必要ですが、メニューにアクリル効果を適用するための手順について、なんとなく雰囲気はつかめるのではないでしょうか。
サブメニューのアクリル化
先ほどの手順を行えば、コンテキストメニューを最初に開いたときの表示はアクリル化ができます。
しかし、コンテキストメニューでは、サブメニューを持つこともあります。
サブメニューの部分は、また別のウィンドウハンドルを持ったものとなっており、別途アクリル化をしなければなりません。
そこで、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>
AcrylicContextMenuのスタイル定義の中で、このAcrylicPopupクラスを標準Popupのかわりに用いることで、サブメニューまで含めてアクリル化しました。
最後に
今回、こんな感じでContextMenuから派生した、独自のコンテキストメニューを作ってみました。
WPFのContextMenuなどのコントロールは、メインのウィンドウとは異なるウィンドウハンドルを持った要素が出てくるため、コントロールのカスタマイズやスタイル定義などもちょっと面倒なコントロールと感じます。
ということで、こういう表示をお手軽に試してみたい場合には、記事冒頭で紹介した拙作のFluentWPFライブラリ、よかったら使ってみてくださいませ♪
XAML Islandsなんてのも出てきて、WPFからもUWPコントロールを使えるようになってきてますが、このライブラリも当面は開発も続けようと思ってます!!