WPFでWindowChromeを使いデザインを柔軟に変更できるウインドウを作る

2021/10/29

C# Prism WindowChrome

アイキャッチ

探せば多くの情報が出てきますが、自分なりにアレンジしたウインドウの作り方を紹介します。

また、同時にReactivePropertyを使った制御方法なども紹介します。

私の作成するサンプルソースファイルは

準備としてはNuGetでReactiveProperty.WPFをインストールしておいてください。

今回の記述には影響しませんが、Prismを使ったアプリケーションになっています。

MainWindow.xamlに追記

ファイル構成はViewsにデザインを記述した「BasicDesign.xaml」を追加した事を前提として解説を進めます。

尚タイトルはReactivePropertySlim<string>を使いますので「Binding Title」から「Binding Title.Value」に変更しています。

自分の管理しやすいファイル名などがあれば随時変更してくださ。

MainWindow.xamlの先頭にある<Window...内に追加するのは

Style="{DynamicResource WindowStyle}"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:ri="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"

1行目はウインドウスタイルの指定となります。

残り2行はウインドウの変化に関するイベントが発生した場合ViewModelにあるReactiveCommandを実行するために必要となる記述です。

次に追加するのはリソースの指定です。

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="BasicDesign.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>

4行目のファイル名は自分で作ったデザインが記述してあるファイル名となります。

この部分では変更があるかもしれないのはこの行だけです。

MainWindow.xamlの追記は下記が最後となります。

ウインドウが閉じられる時に発生するClosedイベントとウインドウサイズが変化する時に発生するSizeChangedイベントをキャッチして各ReactiveCommandに渡す記述です。

<i:Interaction.Triggers>
	<i:EventTrigger EventName="Closed">
		<ri:EventToReactiveCommand Command="{Binding ClosedCommand}"/>
	</i:EventTrigger>
	<i:EventTrigger EventName="SizeChanged">
		<ri:EventToReactiveCommand Command="{Binding SizeChangedCommand}"/>
	</i:EventTrigger>
</i:Interaction.Triggers>

今回のサンプルでは最低限必要なイベントしか記述していません。

これ以外でも必要とするイベントがあったら随時追加してください。

MainWindowViewModel.csに追記

余計な記述があると紛らわしいので最低限必要な記述を紹介します。

まずは使用する変数です。

// タイトルバーに表示する文字列
public ReactivePropertySlim<string> Title { get; } = new("Title");
// Disposeが必要な処理をまとめてやる
private CompositeDisposable Disposable { get; } = new();
// 最大化、通常サイズのボタンデザイン切り替え
public ReactivePropertySlim<string> ButtonStyle { get; } = new("1");
// 最大化、通常のツールチップ文字列
public ReactivePropertySlim<string> ToolTip { get; } = new("最大化");
// 最小化ボタンが押された時
public ReactiveCommand WindowMinimum { get; } = new();
// 最大化、通常サイズのボタンが押された時
public ReactiveCommand WindowSize { get; } = new();
// ウインドウを閉じるボタンが押された時
public ReactiveCommand WindowClose { get; } = new();
// ウインドウのサイズが変更されるイベントが発生した時(最大化と通常の変化)
public ReactiveCommand SizeChangedCommand { get; } = new();
// ウィンドウを閉じるイベントが発生した時
public ReactiveCommand ClosedCommand { get; } = new();

起動時は「最大化されてない」状態を前提としています。

もし起動時は最大化された状態を前提とするなら

1.変数名ButtonStyleは「2」とする。

2.変数名ToolTipは「元に戻す」としておけばいいです。

またContentRenderedイベントをキャッチしApplication.Current.MainWindow.WindowStateの状態を見て初期の文字列を決めてもいいかもしれません。

コンストラクタの内容は以下となりますが、コピペでも問題ないと思います。

// 最小化ボタンが押された
_ = WindowMinimum.Subscribe(_ => Application.Current.MainWindow.WindowState = WindowState.Minimized).AddTo(Disposable);
// 最大化、通常サイズボタンが押された
_ = WindowSize.Subscribe(_ => Application.Current.MainWindow.WindowState = Application.Current.MainWindow.WindowState == WindowState.Normal ? WindowState.Maximized : WindowState.Normal).AddTo(Disposable);
// 閉じるボタンが押された
_ = WindowClose.Subscribe(_ => Window.GetWindow(Application.Current.MainWindow).Close()).AddTo(Disposable);
// ウィンドウサイズが変わった
_ = SizeChangedCommand.Subscribe(_ =>
{
	ButtonStyle.Value = Application.Current.MainWindow.WindowState == WindowState.Normal ? "1" : "2";
	ToolTip.Value = Application.Current.MainWindow.WindowState == WindowState.Normal ? "最大化" : "元に戻す";
}).AddTo(Disposable);
// ウィンドウが閉じるイベント
_ = ClosedCommand.Subscribe(Close).AddTo(Disposable);

非クライアント領域に配置したボタンの動作とウインドウサイズ変更のイベントが発生した時の処理です。

最後にウインドウが閉じられる時のイベントで呼ばれるメソッドに破棄の処理を記述すれば完了です。

private void Close()
{
    Disposable.Dispose();
}

これは他に記述がなければメソッドにしなくてもコンストラクタの

_ = ClosedCommand.Subscribe(Close).AddTo(Disposable);

_ = ClosedCommand.Subscribe(_ => Disposable.Dispose()).AddTo(Disposable);

にしてもOKです。

ウインドウのデザイン

本題となるウインドウのデザインについて解説します。

全てのコードを一気に掲載すると分かりにくくなるので分割して解説しますので全体像はサンプルプログラムを参照してください。

まずはボタンの基本的なスタイルの設定です。

<Style x:Key="SystemButton" TargetType="Button">
    <Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="BorderBrush" Value="Transparent"/>
    <Setter Property="Foreground" Value="LightGray"/>
    <Setter Property="IsTabStop" Value="False"/>
    <!-- FontFamilyにMarlettとする事でシステムボタンの絵になる -->
    <Setter Property="FontFamily" Value="Marlett"/>
    <Setter Property="FontSize" Value="20"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Width" Value="35"/>
    <Setter Property="Height" Value="30"/>
</Style>

大事なのはフォントの設定です。

それ以外は自分でカスタマイズして好きなデザインにしてください。

下記コードはボタンのスタイルです。

<!-- 最大化、最小化のボタン用 -->
<Style x:Key="SystemMaxMiniButton" TargetType="Button" BasedOn="{StaticResource SystemButton}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border Name="HoverButtonBorder" BorderThickness="0">
                    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Border>
                <ControlTemplate.Triggers>
                    <!-- マウスカーソルがボタン上に来たら色を変える -->
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="HoverButtonBorder" Property="Background">
                            <Setter.Value>
                                <RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
                                    <RadialGradientBrush.GradientStops>
                                        <GradientStop Color="#7F0000FF" Offset="0"/>
                                        <GradientStop Color="#00000000" Offset="1"/>
                                    </RadialGradientBrush.GradientStops>
                                </RadialGradientBrush>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<!-- 閉じるボタン用 -->
<Style x:Key="SystemCloseButton" TargetType="Button" BasedOn="{StaticResource SystemButton}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border Name="HoverButtonBorder" BorderThickness="0">
                    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Border>
                <ControlTemplate.Triggers>
                    <!-- マウスカーソルがボタン上に来たら色を変える -->
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="HoverButtonBorder" Property="Background">
                            <Setter.Value>
                                <RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
                                    <RadialGradientBrush.GradientStops>
                                        <GradientStop Color="#7FFF0000" Offset="0"/>
                                        <GradientStop Color="#00000000" Offset="1"/>
                                    </RadialGradientBrush.GradientStops>
                                </RadialGradientBrush>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

基本的な内容は同じですが、マウスカーソルがボタンの上に来た時の色を閉じるボタンは赤ベースのグラデーション、それ以外は青ベースのグラデーションにしています。

ブラウザのChromeも閉じるボタンの上にマウスカーソルが来た時は赤、他のボタン上にマウスカーソルが来た時は灰色としており、それと似た感じにしています。

そのような違う色の変化が不要であれば1つに統一しても問題ありません。

その場合は11行目と39行目から始まる「 <Trigger Property="IsMouseOver" Value="True">」から「</Trigger>」までを削除してください。

以下はウインドウ全体のデザイン設定となります。

<Style x:Key="WindowStyle" TargetType="Window">
    <Setter Property="WindowChrome.WindowChrome">
        <Setter.Value>
            <WindowChrome CornerRadius="0" GlassFrameThickness="0" ResizeBorderThickness="8"/>
        </Setter.Value>
    </Setter>
    <!-- クライアント領域の基本文字色 -->
    <Setter Property="Foreground" Value="LightGray"/>
    <!-- クライアント領域の基本フォントサイズ -->
    <Setter Property="FontSize" Value="15"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Window">
                <!-- 外枠の線色 -->
                <Border x:Name="border" BorderBrush="#202020" BorderThickness="1">
                    <!-- このGridが全体の領域 -->
                    <Grid>
                        <Grid.RowDefinitions>
                            <!-- 領域を非クライアント領域とクライアント領域に分ける -->
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <!-- 非クライアント領域 -->
                        <Grid Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top">
                            <!-- 非クライアント領域の色 これはグラデーション -->
                            <Rectangle>
                                <Rectangle.Fill>
                                    <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                                        <GradientStop Color="#808080"/>
                                        <GradientStop Color="#202020" Offset="1"/>
                                    </LinearGradientBrush>
                                </Rectangle.Fill>
                            </Rectangle>
                            <!-- 非クライアント領域左側のアイコンとタイトル -->
                            <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
                                <Image Source="/Resource/Dialog.ico" Stretch="Fill" HorizontalAlignment="Left" VerticalAlignment="Center" Width="20" Height="20" Margin="5,0,0,0"/>
                                <TextBlock Text="{Binding Title.Value}" FontSize="20" Foreground="LightGray" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10,0,0,0"/>
                            </StackPanel>
                            <!-- 非クライアント領域右側の最小化、最大化 閉じるボタン -->
                            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
                                <Button Content="0" ToolTip="最小化" Command="{Binding WindowMinimum}" Style="{StaticResource SystemMaxMiniButton}"/>
                                <!-- 「最大化」「元に戻す」ボタンのContentとツールチップはReactivePropertyで -->
                                <Button Content="{Binding ButtonStyle.Value}" ToolTip="{Binding ToolTip.Value}" Command="{Binding WindowSize}" Style="{StaticResource SystemMaxMiniButton}"/>
                                <Button Content="r" ToolTip="閉じる" Command="{Binding WindowClose}" Style="{StaticResource SystemCloseButton}"/>
                            </StackPanel>
                        </Grid>
                        <!-- ここがクライアント領域 -->
                        <Grid Grid.Row="1">
                            <!-- クライアント領域の基本色 -->
                            <Rectangle Fill="#202020"/>
                            <!-- これがないとMainWindow.xamlに記述したコントロールが表示されない -->
                            <ContentPresenter HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
                        </Grid>
                    </Grid>
                </Border>
                <!-- 最大化した時ウィンドウ枠が画面外にはみ出ないようにする -->
                <ControlTemplate.Triggers>
                    <Trigger Property="WindowState" Value="Maximized">
                        <Setter TargetName="border" Property="BorderThickness" Value="8"/>
                    </Trigger>
                    <Trigger Property="WindowState" Value="Normal">
                        <Setter TargetName="border" Property="BorderThickness" Value="1"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

各コードを別で解説するよりはコメントを見た方が分かりやすいと思いますので細かい解説はしませんが、全体像と注意点の解説をします。

全体的にはGridとなっており非クライアント領域とクライアント領域に分割しています。

今回のサンプルコードの非クライアント領域はグラデーションにして見た目重視としています。

非クライアント領域の左側にはアイコンとタイトルを表示させます。(35~38行目)

そして非クライアント領域の右側には最小化、最大化、閉じるボタンを配置します。(40~45行目)

最大化、元に戻すボタンはウインドウの状態によって変化しますのでContentとToolTipはViewModelを参照します。

クライアント領域は非クライアント領域のグラデーションと連続するような色にしています。

また52行目の記述がないとMainWindow.xamlに記述したコントロールが表示されないので注意してください。

最後に57~64行目ですが、ウインドウを最大化すると画面から少しはみ出るそうで、それを回避するための記述となります。

標準的なウインドウが

今回のプログラムで作成したウインドウが

最小化や最大化のボタンの上にマウスカーソルが来ると

閉じるボタンの上にマウスカーソルが来ると

こんな感じにほんのり色が変わるようになります。

自己紹介

自分の写真



新潟県のとある企業で働いてます。
【できる事】
電子回路設計
基板パターン設計
マイコンプログラム
C#(WinForms WPF)を使ったWindowsアプリケーション作成
PLCラダー
自動化装置アドバイザー
にほんブログ村 IT技術ブログ ソフトウェアへ

カテゴリ

このブログを検索

QooQ