Livetを使ったC#のWindowChromeプログラミング

高い表現力のWindowChromeを使うための準備を紹介します


はじめに


WindowChromeとは

通常のWindowの画面では最上部には、タイトルバーや閉じるボタンが配置されているプログラマーが触れない領域である非クライアント領域と、 その下側のクライアント領域があります。
画面にコントロールを配置する時は、このクライアント領域に配置します。
上部の非クライアント領域にContentの記述を拡張するための仕組みがWindowChromeです。
このがWindowChromeを使うことによって、画面全体を自由にデザインすることが可能となります。


応用 WindowChromeサンプル
タイトル 内容
1 DashboardSampleの移植 サイトで見つけたDashboardSampleのViewを移植
2 CustomChromeLibrary.dll WindowChrome作成で必要なdllを直接使ったプログラミング

参考にしたサイト

ここでの解説は、下記のCODE PROJECTのサイトより「WpfCustomChromeLibrary」をダウンロードして使用しています。

「C# Free Code - Download WpfCustomChromeLibrary - Java2s」

サイトの下の方の「Download」の下のwpfcustomchromelib.zipをクリックするとダウンロード出来ます。 解凍すると、wpfcustomchromelibというフォルダが出来上がります。この中には、
BuildProcessTemplates
Libraries
SampleApps
ThirdParty
というフォルダがあります。
wpfcustomchromelibごと、Dドライブ直下にコピー(又は移動)します。
Visual Studioを起動して、「プロジェクトを開く...」で、
D:\WpfCustomChromeLib\SampleApps\CustomChromeSample\CustomChromeSample.sln
を選択します。画面は下記のようになります。


※下記のurlからは簡単にプロジェクトのダウンロードができます。

WPF Custom Chrome Library - CodeProject


WpfCustomChromeLib (クリックすると拡大)

このプロジェクトは使いません。単に参考に表示しただけで、使用するのは、解凍した
wpfcustomchromelibの中のLibrariesフォルダの中のCustomChromeLibraryフォルダ
ThirdPartyフォルダの中のWPF Shell Integration Library v2フォルダです。

新しくLivetプロジェクトの作成

とりあえず、D:ドライブ直下にLivetWPFApplicationChrome1という名前でプロジェクト(ソリューション を作成します。


Chrome関連フォルダのコピー

次に、作ったプロジェクトをエックスプローラーで開いて、この中に、CustomChromeLibraryフォルダWPF Shell Integration Library v2フォルダをコピーします。


エックスプローラー (クリックすると拡大)

新しく作ったプロジェクトにプロジェクト(フォルダ)を追加

先ほど作ったプロジェクトを開いて、ソリューションエックスプローラーのソリューション名を選択して下記のようにショートカットメニューで2つのプロジェクトを取り込みます。


Visual Stusio ソリューションエックスプローラー (クリックすると拡大)

Chrome関連フォルダを追加したLivetプロジェクト

次に追加したプロジェクトを「参照設定」します。

Visual Stusio ソリューションエックスプローラー (クリックすると拡大)


「参照の追加」をクリックしますと下記の画面が表示されます。


2つのプロジェクトを選択して、OKをクリックします。


クラスの作成

CaptionButtonRectToMarginConverter.csとう名前で、LivetWPFApplicationChrome1の直下にクラスを作成します。

Visual Stusio ソリューションエックスプローラー (クリックすると拡大)
ViewModel
using System;
using System;
using System.Windows;
using System.Windows.Data;

using LivetWPFApplicationChrome1.ViewModels;
using LivetWPFApplicationChrome1.Views;

namespace LivetWPFApplicationChrome1
{
  public class CaptionButtonRectToMarginConverter : IValueConverter
  {
     public object Convert(object value, Type targetType, object parameter, 
         				System.Globalization.CultureInfo culture)
     {
        var captionLocation = (Rect)value;
        return new Thickness(0, captionLocation.Top + 6, -captionLocation.Right, 0);
     }
     public object ConvertBack(object value, Type targetType, object parameter, 
         			System.Globalization.CultureInfo culture)
     {
        throw new NotSupportedException();
     }
  }
}


MainWindow.xamlの書き換え

WindowChromeに書き換えます。その時、クラスがWondowからccl:CustomChromeWindowに変更しています。

ZAMLは下記の通りです。
ClosingとCloseの処理は全て、CustomChromeLibraryがおこなうので、ここでのEventTriggerは不要なのでコメントにしています。

View
<ccl:CustomChromeWindow 
        x:Class="LivetWPFApplicationChrome1.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:shell="http://schemas.microsoft.com/winfx/2006/xaml/presentation/shell"
        xmlns:ccl="clr-namespace:CustomChromeLibrary;assembly=CustomChromeLibrary"                        
	    xmlns:local="clr-namespace:LivetWPFApplicationChrome1"               
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:core="clr-namespace:Microsoft.Expression.Interactivity.
        	Core;assembly=Microsoft.Expression.Interactions"      
        xmlns:v="clr-namespace:LivetWPFApplicationChrome1.Views"
        xmlns:vm="clr-namespace:LivetWPFApplicationChrome1.ViewModels"    
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="714" Width="1035">

    <shell:WindowChrome.WindowChrome>
        <shell:WindowChrome
            ResizeBorderThickness="6"
            CaptionHeight="43"
            CornerRadius="0,0,0,0"
            GlassFrameThickness="0">
        </shell:WindowChrome>
    </shell:WindowChrome.WindowChrome>

    <Window.Resources>
        <ResourceDictionary>
            <local:CaptionButtonRectToMarginConverter 
            		x:Key="CaptionButtonMarginConverter"/>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Resources/Styles2.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <!--WindowのContentRenderedイベントのタイミングでViewModelの
        	Initializeメソッドが呼ばれます-->
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" 
			MethodName="Initialize"/>
        </i:EventTrigger>

        <l:InteractionMessageTrigger 
            MessageKey="MessageKey2" Messenger="{Binding Messenger}">
            <l:TransitionInteractionMessageAction
                WindowType="{x:Type v:Window2}" Mode="Modal"/>
        </l:InteractionMessageTrigger>

        <!-- 下記がないと、タスクバーが1つにならない -->
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding Path=Loaded}" 
               CommandParameter="{Binding Mode=OneTime,
               RelativeSource={RelativeSource Mode=FindAncestor,
               AncestorType={x:Type Window}}}"/>
        </i:EventTrigger>
<!--
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding Path=Closing}"
                CommandParameter="{Binding Mode=OneTime,RelativeSource=
                {RelativeSource Mode=FindAncestor,AncestorType={x:Type Window}}}"/>
        </i:EventTrigger>
        <l:InteractionMessageTrigger MessageKey="Close" 
                                        Messenger="{Binding Messenger}">
                 <l:WindowInteractionMessageAction/>
        </l:InteractionMessageTrigger>
-->
    </i:Interaction.Triggers>

    <!-- Grid-1 -->
    <Grid>

        <!-- WindowChrome Start  -->
        <Border  Grid.RowSpan="2" BorderThickness="3" BorderBrush="Black">
            <Border.Background>
                <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                    <GradientStop Color="Black" Offset="0" />
                    <GradientStop Color="Black" Offset="1" />
                </LinearGradientBrush>
            </Border.Background>
        </Border>

        <Border  BorderThickness="3,3,3,1" BorderBrush="Black" 
        		Margin="{Binding Path=CaptionButtonMargin}">
            <Border.Background>
                <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                    <GradientStop Color="Black" Offset="0" />
                    <GradientStop Color="Black" Offset="1" />
                </LinearGradientBrush>
            </Border.Background>
            <!--Window Icon and Title-->
            <StackPanel Orientation="Horizontal" Margin="0" VerticalAlignment="Top">
                <TextBlock Text="WindowChrome" FontFamily="Calibri" 
                	FontWeight="Bold" FontSize="26" Foreground="Blue" />
            </StackPanel>
        </Border>
        <ccl:CaptionButtons />
        <!-- WindowChrome End  -->

        <!-- Grid-2 -->
        <!--Content-->
        <Grid Grid.Row="1">

            <!-- ★★★ -->
            <Grid>
                <TextBox Text="{Binding Path=txt本日}" TextAlignment="Center" 
                    Foreground="White" Background="Black"
                    Height="17" HorizontalAlignment="Center"  Name="txt本日" 
                    VerticalAlignment="Top" Width="81" Margin="809,3,123,0" />
                <Button Command="{Binding Path=GotoCommand2}" 
		            Content="2.メニュー" FontSize="16" Height="35" 
		            	Margin="39,0,712,569"  
                    Foreground="White" Style="{StaticResource CommandTemplate}" 
                    	VerticalAlignment="Bottom" />

            </Grid>
            <!-- ★★★ -->

        </Grid>
    </Grid>
</ccl:CustomChromeWindow>

MainWindow.xamlのコードビハインドファイルの継承の変更

MainWindow.xamlのクラスをWondowからccl:CustomChromeWindowに変更したので、MainWindow.xamlのコード・ビハインドの継承をWondowからCustomChromeLibrary.CustomChromeWindowに変更します。


コードビハインドファイル
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using Livet;
using Livet.EventListeners.WeakEvents;

namespace LivetWPFApplicationChrome1.Views
{
    public partial class MainWindow : CustomChromeLibrary.CustomChromeWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

リソースフォルダの作成

名前は、Resources。LivetWPFApplicationChrome1を選択して下記のようにします。

Visual Stusio ソリューションエックスプローラー (クリックすると拡大)

リソースディクショナリの作成

名前は、Styles2.xamlで、Resourcesフォルダを選択して下記のようにします。

Visual Stusio ソリューションエックスプローラー (クリックすると拡大)

リソースディクショナリのZAMLは下記の通りです。

View
<ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Style  x:Key="CommandTemplate" TargetType="{x:Type Button}">
        <Setter Property="Background" Value="Green" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Button}">
                    <Border Background="{TemplateBinding Background}">
                        <ContentPresenter HorizontalAlignment="Center" 
                        VerticalAlignment="Center" />
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="DarkGoldenrod" />
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>

ViewModelは下記の通りです。14,15,17,18,19,20行は自動作成に追加しています。
Closingの処理は全て、CustomChromeLibraryがおこなうので、ここでの記述は不要なので、コメントにしています。

ViewModel
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

using Livet;
using Livet.Commands;
using Livet.Messaging;
using Livet.Messaging.IO;
using Livet.EventListeners;
using Livet.Messaging.Windows;

//ICommand
using System.Windows.Input;

//Application
//Window
//MessageBox
using System.Windows;

using LivetWPFApplicationChrome1.Models;

namespace LivetWPFApplicationChrome1.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        #region 変更通知プロパティ
        //-----------------------------------------------
        public string txt本日 { get; set; }
        public bool IsEnabled { get; set; }
        //-----------------------------------------------
        #endregion
        Window win = null;
        public MainWindowViewModel()
        {            
            Loaded = new Livet.Commands.ListenerCommand<Window>((w) =>
            {
                if (NeedHideOwner && w.Owner != null && w.Owner.Visibility == 
                		Visibility.Visible)
                {
                    win = w;
                    //w.Owner.Hide();
                }
            });
/*
            Closing = new Livet.Commands.ListenerCommand<Window>((w) =>
            {
                if (NeedHideOwner && w.Owner != null)
                {
                    w.Owner.Show();
                }
            });
*/
            //Initialize()では表示されない
            txt本日 = Convert.ToString(DateTime.Today.ToShortDateString());
        }
        public bool NeedHideOwner { get; set; }
        public ICommand Loaded { get; private set; }
     // public ICommand Closing { get; private set; }

        public void Initialize()
        {
        	if (win != null) win.Owner.Hide();
        }

        #region GotoCommand2
        //--------------------------------------------------------------
        public ViewModelCommand GotoCommand2
        {
            get { return new Livet.Commands.ViewModelCommand(Goto2); }
        }
        public void Goto2()
        {
       //     Messenger.Raise(new TransitionMessage(new ViewModel2() 
       //		{ NeedHideOwner = true }, "MessageKey2"));
        }
        //--------------------------------------------------------------
        #endregion

        #region CloseCommand
        private ViewModelCommand _CloseCommand;
        public ViewModelCommand CloseCommand
        {
            get
            {
                if (_CloseCommand == null)
                {
                    _CloseCommand = new ViewModelCommand(Close);
                }
                return _CloseCommand;
            }
        }
        public void Close()
        {
            var window = Application.Current.Windows.OfType<Window>().
            			SingleOrDefault((w) => w.IsActive);
            window.Close();
        }
        #endregion
    }
}

Close処理

CustomChromeLibraryの下記のクラスでClose処理をしています。

CloseButton.cs

クリックすると拡大します


実行

LivetWPFApplicationChrome1プロジェクトをビルドして、ソリューションをビルドして実行します。

LivetWPFApplicationChrome1 (クリックすると拡大)

2つ目の画面を下記の通り作ります。MainWindow.xamlのコード・ビハインドと同様に継承も修正します。

View

<ccl:CustomChromeWindow 
        x:Class="LivetWPFApplicationChrome1.Views.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:shell="http://schemas.microsoft.com/winfx/2006/xaml/presentation/shell"
        xmlns:ccl="clr-namespace:CustomChromeLibrary;assembly=CustomChromeLibrary"                        
	    xmlns:local="clr-namespace:LivetWPFApplicationChrome1"               
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:core="clr-namespace:Microsoft.Expression.Interactivity.
        		Core;assembly=Microsoft.Expression.Interactions"      
        xmlns:v="clr-namespace:LivetWPFApplicationChrome1.Views"
        xmlns:vm="clr-namespace:LivetWPFApplicationChrome1.ViewModels"
     
        WindowStartupLocation="CenterScreen"
        Title="Window2" 
        Height="714" Width="1035">

    <shell:WindowChrome.WindowChrome>
        <shell:WindowChrome
            ResizeBorderThickness="6"
            CaptionHeight="43"
            CornerRadius="0,0,0,0"
            GlassFrameThickness="0">
        </shell:WindowChrome>
    </shell:WindowChrome.WindowChrome>



    <Window.Resources>
        <ResourceDictionary>
            <local:CaptionButtonRectToMarginConverter x:Key=
            		"CaptionButtonMarginConverter"/>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Resources/Styles2.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    <i:Interaction.Triggers>
        <!--WindowのContentRenderedイベントのタイミングでViewModelの
        		Initializeメソッドが呼ばれます-->
        <i:EventTrigger EventName="ContentRendered">
        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>

        <!-- 下記がないと、タスクバーが1つにならない -->
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding Path=Loaded}" 
               CommandParameter="{Binding Mode=OneTime,RelativeSource=
               {RelativeSource Mode=FindAncestor,AncestorType={x:Type Window}}}"/>
        </i:EventTrigger>
<!--
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding Path=Closing}"
               CommandParameter="{Binding Mode=OneTime,RelativeSource=
               {RelativeSource Mode=FindAncestor,AncestorType={x:Type Window}}}"/>
        </i:EventTrigger>

        <l:InteractionMessageTrigger MessageKey="Close" Messenger=
        	"{Binding Messenger}">
            <l:WindowInteractionMessageAction/>
        </l:InteractionMessageTrigger>
-->
    </i:Interaction.Triggers>

    <Window.DataContext>
        <vm:ViewModel2/>
    </Window.DataContext>

    <!-- Grid-1 -->
    <Grid>
        <!-- WindowChrome Start  -->
        <Border  Grid.RowSpan="2" BorderThickness="3" BorderBrush="Black">
            <Border.Background>
                <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                    <GradientStop Color="Black" Offset="0" />
                    <GradientStop Color="Black" Offset="1" />
                </LinearGradientBrush>
            </Border.Background>
        </Border>

        <!--title bar-->
        <Border  BorderThickness="3,3,3,1" BorderBrush="Black" Margin=
        	"{Binding Path=CaptionButtonMargin}">
            <Border.Background>
                <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                    <GradientStop Color="Black" Offset="0" />
                    <GradientStop Color="Black" Offset="1" />
                </LinearGradientBrush>
            </Border.Background>
           <StackPanel Orientation="Horizontal" Margin="0" VerticalAlignment="Top">
             <TextBlock Text="WindowChrome" FontFamily="Calibri" FontWeight=
             	"Bold" FontSize="26" Foreground="Blue" />
            </StackPanel>
        </Border>
        <ccl:CaptionButtons />
        <!-- WindowChrome End  -->

        <!-- Grid-2 -->
        <!--Content-->
        <Grid Grid.Row="1">

            <!-- ★★★ -->
            <Grid>
                <TextBox Text="{Binding Path=txt本日}" TextAlignment="Center" 
                    Foreground="White" Background="Black"
                    Height="17" HorizontalAlignment="Center"  Name="txt本日" 
                    VerticalAlignment="Top" Width="81" Margin="809,3,123,0" />
            </Grid>
            <!-- ★★★ -->

        </Grid>
    </Grid>
</ccl:CustomChromeWindow>

画面は下記のようになります。

Window2.zaml

クリックすると拡大します


ViewModelは下記の通りです。


ViewModel
using System;

// ObservableCollection
using System.Collections.Generic;
using System.Linq;

//INotifyPropertyChanged
//PropertyChanged
using System.ComponentModel;

//参照設定が必要
//using System.Configuration;

using Livet;
using Livet.Commands;
using Livet.Messaging;
//CloseCommand
using Livet.Messaging.Windows;

//ICommand
using System.Windows.Input;

//MessageBox
using System.Windows;


namespace LivetWPFApplicationChrome1.ViewModels
{
    public class ViewModel2 : ViewModel
    {
        #region 変更通知プロパティ
        //-----------------------------------------------
        public string txtMessage { get; set; }
        public string txt本日 { get; set; }
        public bool IsEnabled { get; set; }
        //-----------------------------------------------
        #endregion

        Window win = null;
        public ViewModel2()
        {            
            Loaded = new Livet.Commands.ListenerCommand<Window>((w) =>
            {
                if (NeedHideOwner && w.Owner != null && w.Owner.Visibility == Visibility.Visible)
                {
                    win = w;
                    //w.Owner.Hide();
                }
            });
/*
            Closing = new Livet.Commands.ListenerCommand<Window>((w) =>
            {
                if (NeedHideOwner && w.Owner != null)
                {
                   w.Owner.Show();
                }
            });
*/
            //Initialize()では表示されない
            txt本日 = Convert.ToString(DateTime.Today.ToShortDateString());  
        }
        public bool NeedHideOwner { get; set; }
        public ICommand Loaded { get; private set; }
    //  public ICommand Closing { get; private set; }

        public void Initialize()
        {
            if (win != null) win.Owner.Hide();
        }

        #region GotoCommand
        //---------------------------------------------------------------
        public ViewModelCommand GotoCommand3
        {
            get { return new Livet.Commands.ViewModelCommand(Goto3); }
        }
        public void Goto3()
        {
            Messenger.Raise(new TransitionMessage(new ViewModel3() { NeedHideOwner = true }, "MessageKey3"));
        }
        //----------------------------------------------------------------
        #endregion

        #region CloseCommand
        private ViewModelCommand _CloseCommand;
        public ViewModelCommand CloseCommand
        {
            get
            {
                if (_CloseCommand == null)
                {
                    _CloseCommand = new ViewModelCommand(Close);
                }
                return _CloseCommand;
            }
        }
        public void Close()
        {
            var window = Application.Current.Windows.OfType<Window>().SingleOrDefault((w) => w.IsActive);
            window.Close();
        }
        #endregion


    }
}


はじめに