fc2ブログ

INotifyDataErrorInfoを使用したデータ検証

 これまで例外を使用したデータ検証IDataErrorInfoを使用したデータ検証を行いましたが、今回はWPF4.5で加わったINotifyDataErrorInfoを使用したデータ検証を行ってみます。
 IDataErrorInfoを使用したデータ検証と似ていますが、INotifyDataErrorInfoを使用した場合以下の点が異なります。
・HasErrorプロパティを持っている。
・エラー情報の変更をイベントで通知する。
・1つのプロパティに対して複数のエラーメッセージを返すことができる。
・エラー情報はstring型以外でもOK。

 それでは、ViewModelのサンプルコードを見てみましょう。
using Livet;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Validation3.ViewModels
{
    public class MainWindowViewModel : ViewModel, INotifyDataErrorInfo
    {
        private Dictionary<string, string[]> Errors = new Dictionary<string, string[]>();

        public MainWindowViewModel()
        {
            Value1 = 0;
            Value2 = "";
        }

        #region Value1変更通知プロパティ
        private int _Value1;

        public int Value1
        {
            get
            { return _Value1; }
            set
            { 
                if (_Value1 == value)
                    return;
                _Value1 = value;

                // データ検証
                Errors["Value1"] = value < 0 ? new[] { "0以上の数値を入力してください。" } : null;
                RaiseErrorsChanged();   // エラー情報が変更されたことを通知

                RaisePropertyChanged();
            }
        }
        #endregion


        #region Value2変更通知プロパティ
        private string _Value2;

        public string Value2
        {
            get
            { return _Value2; }
            set
            { 
                if (_Value2 == value)
                    return;
                _Value2 = value;

                // データ検証
                Errors["Value2"] = string.IsNullOrWhiteSpace(value)
                    ? new[] { "値が入力されていません。", "何か入力してください。" }
                    : null;
                RaiseErrorsChanged();

                RaisePropertyChanged();
            }
        }
        #endregion


        // エラー情報が変更された際に発生するイベント
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        // ErrorsChangedイベントを発生させる
        protected virtual void RaiseErrorsChanged([CallerMemberName]string propertyName = "")
        {
            var h = this.ErrorsChanged;
            if(h != null)
            {
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

        // エラーメッセージを取得する
        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrWhiteSpace(propertyName)) return null;
            if (!Errors.ContainsKey(propertyName)) return null;
            return Errors[propertyName];
        }

        // エラーの有無
        public bool HasErrors
        {
            get { return Errors.Values.Any(x => x != null); }
        }
    }
}
 INotifyDataErrorInfoではErrorsChangedイベントでエラー情報の変更を通知します。
 このイベントではどのプロパティのエラー情報が変更されたかが通知されます。
 エラーメッセージはGetErrorsメソッドによって取得されます。このメソッドの戻り値はIEnumerable型なので、string以外のエラー情報を返すことも可能です。(今回はstring型で返していますが)
 IDataErrorInfoには無かったHasErrorプロパティも実装されています。このプロパティによってエラーの有無を取得することが可能になっています。
 エラー情報の受け取り方がIDataErrorInfoと異なっていますが、データ検証の方法はそれほど変わりません。
 今回のコードも、プロパティのsetterでデータ検証を行い、エラーメッセージをErrorsに格納しています。
 エラー情報が複数あっても良いのでErrorsのValueはstring配列にしています。
 そして、エラー情報が変更されたことを通知するためにErrorsChangedイベントを発生させています。
 今回はValue1では今までと同じように1つのエラーメッセージを返していますが、Value2では試しに2つのエラーメッセージを返しています。

 次にView側のコードです。
<Window x:Class="Validation3.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        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:v="clr-namespace:Validation3.Views"
        xmlns:vm="clr-namespace:Validation3.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <Window.Resources>
        <ControlTemplate x:Key="ErrorTemplate">
            <DockPanel>
                <ItemsControl DockPanel.Dock="Right" Margin="5,0"
                              ItemsSource="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
                
                <Border BorderBrush="Red" BorderThickness="1"
                        Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
                        Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
                    <AdornedElementPlaceholder Name="adornedElement"/>
                </Border>
            </DockPanel>
        </ControlTemplate>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="15"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        
        <TextBlock Grid.Row="0" Grid.Column="0" Text="数値:"/>
        <TextBox Grid.Row="0" Grid.Column="1"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"
                 Text="{Binding Value1, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/>
        
        <TextBlock Grid.Row="2" Grid.Column="0" Text="文字列:"/>
        <TextBox Grid.Row="2" Grid.Column="1"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"
                 Text="{Binding Value2, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Window>
 INotifyDataErrorInfoの場合もエラーメッセージをエラーテンプレートを使用して表示しますが、エラーメッセージが複数あることが前提なので前回のコードとは少し異なります。
 エラーメッセージが複数あるのでItemsControlを使ってエラーメッセージを表示しています。
 バインディングの際には「ValidatesOnNotifyDataErrors」をtrueにします。ただ、ValidatesOnNotifyDataErrorsはデフォルトでtrueになっているので省略することが可能です。(Value1では説明のために記載していますが、Value2では省略しています)
 このコードを実行し、エラーを発生させると以下の様になります。
データ検証3
 こんな感じで複数のエラーメッセージを表示できます。
 IDataErrorInfoとINotifyDataErrorInfoどちらを使うかは人それぞれだと思うので、使いやすい方を使えばよいかなと思います。

 WPFでデータ検証を行う方法は、これまでに説明した3種類のどれかを使用することになると思います。
 データ検証はModelで行ったほうが良いのではないかと考える人も多いと思います。ただWPFのデータ検証機能が見ての通りViewModelよりなので、若干の違和感を感じますがViewModelでデータ検証を行っています。
 次回はDataAnnotationsを利用して、もう少し楽にデータ検証を行う方法を紹介します。
スポンサーサイト



テーマ : プログラミング
ジャンル : コンピュータ

IDataErrorInfoを使用したデータ検証

 前回は例外を利用したデータ検証でしたが、今回はIDataErrorInfoを使用したデータ検証の方法です。
 まずはViewModel(データバインディングするクラス)にIDataErrorInfoを実装します。
using Livet;
using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace Validation2.ViewModels
{
    public class MainWindowViewModel : ViewModel, IDataErrorInfo
    {
        private Dictionary<string, string> Errors = new Dictionary<string, string>();


        #region Value1変更通知プロパティ
        private int _Value1;

        public int Value1
        {
            get
            { return _Value1; }
            set
            { 
                if (_Value1 == value)
                    return;
                _Value1 = value;
                
                // データ検証
                Errors["Value1"] = value < 0 ? "0以上の数値を入力してください。" : null;

                RaisePropertyChanged();
            }
        }
        #endregion

        public string Error
        {
            get { throw new NotImplementedException(); }
        }

        public string this[string columnName]
        {
            get 
            {
                return Errors.ContainsKey(columnName) ? Errors[columnName] : null;
            }
        }
    }
}
 IDataErrorInfoのメンバーには、Errorプロパティとインデクサがありますがデータ検証に使用するのはインデクサの方だけです。そのためこのサンプルコードではErrorプロパティは実装しません。
 インデクサでは指定したプロパティのエラーメッセージを返す必要があります。
 エラーがない場合はnullを返します。
 このサンプルコードではプロパティのsetterでデータ検証を行いエラーメッセージをErrorsに格納し、インデクサでErrorsから指定されたプロパティのエラーメッセージを返しています。
 インデクサで各プロパティのデータ検証を行い、その結果を返すようにしてもOKです。
 どのような方法でもよいのでデータ検証の結果をインデクサで返すようにすれば大丈夫です。

 View側のコードは前回のコードとほぼ同じです。
<Window x:Class="Validation2.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        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:v="clr-namespace:Validation2.Views"
        xmlns:vm="clr-namespace:Validation2.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <Window.Resources>
        <ControlTemplate x:Key="ErrorTemplate">
            <DockPanel>
                <TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="5,0"
                        Text="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"/>

                <Border BorderBrush="Red" BorderThickness="1"
                    Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
                    Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
                    <AdornedElementPlaceholder Name="adornedElement"/>
                </Border>
            </DockPanel>
        </ControlTemplate>
    </Window.Resources>    
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="50"/>
        </Grid.ColumnDefinitions>
        
        <TextBlock Grid.Column="0" Text="IDataErrorInfoを使用:"/>
        <TextBox Grid.Column="1"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"
                 Text="{Binding Value1, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Window>
 エラーメッセージの表示の方法(エラーテンプレート)は前回と同じです。
 違う点は、バインディング時に「ValidatesOnDataErrors」をtrueにすることです。
 IDataErrorInfoを使用したデータ検証を行う場合は必ずValidatesOnDataErrorsをtrueにする必要があります。
 結果は以下の様になります。
データ検証2

テーマ : プログラミング
ジャンル : コンピュータ

例外を利用したデータ検証

 WPFではデータの検証を行う方法が複数用意されています。
 今回はその中でも一番単純な例外を利用したデータ検証の方法を紹介します。
 例外を利用したデータ検証では、プロパティのset時に値のチェックを行い、問題がある場合は例外を発生させます。
 あとは、View側で例外メッセージを取得して、エラーメッセージとして表示します。
 View側のエラーメッセージ表示は他のデータ検証でもほぼ同じです。
 それでは、まずはViewModelのサンプルコードを見てみましょう。
using Livet;
using System;

namespace Validation1.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {

        #region Value1変更通知プロパティ
        private int _Value1;

        public int Value1
        {
            get
            { return _Value1; }
            set
            { 
                if (_Value1 == value)
                    return;
                _Value1 = value;

                // データ検証
                if(value < 0)
                {
                    throw new Exception("0以上の数値を入力してください。");
                }

                RaisePropertyChanged();
            }
        }
        #endregion


        #region Value2変更通知プロパティ
        private int _Value2;

        public int Value2
        {
            get
            { return _Value2; }
            set
            { 
                if (_Value2 == value)
                    return;
                _Value2 = value;

                // データ検証
                if (value < 0)
                {
                    throw new Exception("0以上の数値を入力してください。");
                }

                RaisePropertyChanged();
            }
        }
        #endregion


        #region Value3変更通知プロパティ
        private int _Value3;

        public int Value3
        {
            get
            { return _Value3; }
            set
            { 
                if (_Value3 == value)
                    return;
                _Value3 = value;

                // データ検証
                if (value < 0)
                {
                    throw new Exception("0以上の数値を入力してください。");
                }

                RaisePropertyChanged();
            }
        }
        #endregion

    }
}
 Value1、Value2、Value3と3つのプロパティを定義しています。
 どのプロパティも値をチェックして0未満の場合は例外を発生させています。
 同じ様なプロパティを複数作ったのは、View側で表示方法を複数紹介するためです。
 次に、Viewのサンプルコードを見てみましょう。
<Window x:Class="Validation1.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        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:v="clr-namespace:Validation1.Views"
        xmlns:vm="clr-namespace:Validation1.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <Window.Resources>
        <!--TextBoxの右側にエラーメッセージを表示-->
        <ControlTemplate x:Key="ErrorTemplate">
            <DockPanel>
                <TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="5,0"
                           Text="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"/>
                
                <!--AdornedElementPlaceholderはErrorTemplateが適用される要素のことです-->
                <Border BorderBrush="Red" BorderThickness="1"
                        Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
                        Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
                    <AdornedElementPlaceholder Name="adornedElement"/>
                </Border>
            </DockPanel>
        </ControlTemplate>
        
        <!--ツールチップにエラーメッセージを表示する-->
        <Style x:Key="ToolTipErrorStyle" TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip">
                        <Setter.Value>
                            <Binding RelativeSource="{RelativeSource Self}" Path="(Validation.Errors)[0].ErrorContent"/>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="0" Grid.Column="0" Text="エラーメッセージを表示:"/>
        <TextBox Grid.Row="0" Grid.Column="1"
                 Validation.ErrorTemplate="{StaticResource ErrorTemplate}"
                 Text="{Binding Value1, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}"/>
        
        <TextBlock Grid.Row="1" Grid.Column="0" Text="ツールチップに表示:"/>
        <TextBox Grid.Row="1" Grid.Column="1" Style="{StaticResource ToolTipErrorStyle}"
                 Text="{Binding Value2, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}"/>
        
        <TextBlock Grid.Row="2" Grid.Column="0" Text="メッセージを別要素に表示:"/>
        <TextBox Grid.Row="2" Grid.Column="1"
                 Text="{Binding Value3, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}"
                 Validation.ValidationAdornerSite="{Binding ElementName=ErrorMessageText}"/>
        <TextBlock Grid.Row="3" Grid.Column="2" Name="ErrorMessageText"
                   Text="{Binding RelativeSource={RelativeSource Self}, 
                    Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)[0].ErrorContent}"
                   Foreground="Red"/>
    </Grid>
</Window>
 このコードを実行し、設定した範囲外の数値を入力すると以下の様になります。
データ検証1
 例外を利用してデータ検証を行う場合は、検証する値をバインディングする際に「ValidatesOnExceptions」をtrueにする必要があります。
 このサンプルコードでは「UpdateSourceTrigger」をPropertyChangedにしていますが、これは値が変化した瞬間にデータ検証を行う為です。どのタイミングで検証を行うかはケースバイケースだと思うので、適したタイミングで更新するようにしてください。
 「ValidatesOnExceptions」をtrueにしただけでは、エラーメッセージを表示できません。
 エラーメッセージを表示するためには、ControlTemplateやStyleを使用して何らかの方法でエラーメッセージを表示させるような工夫が必要になります。
 最初の項目はエラーテンプレートを変更することで、エラーメッセージを表示させています。
 
<ControlTemplate x:Key="ErrorTemplate">
    <DockPanel>
        <TextBlock DockPanel.Dock="Right" Foreground="Red" Margin="5,0"
                Text="{Binding ElementName=adornedElement, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"/>

        <!--AdornedElementPlaceholderはErrorTemplateが適用される要素のことです-->
        <Border BorderBrush="Red" BorderThickness="1"
            Width="{Binding ElementName=adornedElement, Path=ActualWidth}"
            Height="{Binding ElementName=adornedElement, Path=ActualHeight}">
            <AdornedElementPlaceholder Name="adornedElement"/>
        </Border>
    </DockPanel>
</ControlTemplate>
 AdornedElementPlaceholderがエラーテンプレートが適用される要素を表しています。(このサンプルの場合はTextBoxになります)
 AdornedElementPlaceholderをBorderで囲い、赤枠表示にさせています。(標準のエラーテンプレートと同じような見た目にしています。)
 エラーメッセージの表示は、AdornedElementPlaceholderの右側にTextBlockで表示させています。
 バインディング対象がちょっとややこしいことになっていますが、定型文なのでなんとか覚えてください。

 エラーテンプレートを使わず、スタイルでエラーメッセージを表示することもできます。
 2つ目の項目は、スタイルを使用してツールチップにエラーメッセージを表示させています。
<Style x:Key="ToolTipErrorStyle" TargetType="{x:Type TextBox}">
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
            <Setter Property="ToolTip">
                <Setter.Value>
                    <Binding RelativeSource="{RelativeSource Self}" Path="(Validation.Errors)[0].ErrorContent"/>
                </Setter.Value>
            </Setter>
        </Trigger>
    </Style.Triggers>
</Style>
 エラーがあるかどうかはValidation.HasErrorの値を使って見分けることができます。この値をトリガーとしてエラーがある場合はツールチップにエラーメッセージを表示させています。
 エラーメッセージの場所は、これまたちょっとややこしいことになっていますが、これも定型文なので頑張って覚えてください。

 3つ目の項目はエラーメッセージを別の要素に表示させる方法です。
<TextBox Grid.Row="2" Grid.Column="1"
            Text="{Binding Value3, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}"
            Validation.ValidationAdornerSite="{Binding ElementName=ErrorMessageText}"/>
<TextBlock Grid.Row="3" Grid.Column="2" Name="ErrorMessageText"
            Text="{Binding RelativeSource={RelativeSource Self}, 
            Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)[0].ErrorContent}"
            Foreground="Red"/>
 値を入力するTextBoxに値をバインディングする際にValidation.ValidationAdornerSiteの設定も行います。
 Validation.ValidationAdornerSiteにはエラーメッセージを表示する要素をバインディングします。
 エラーメッセージを表示する側ではValidation.ValidationAdornerSiteForからエラーメッセージを取得します。
 これもややこしいですが定型文なので覚えるしかありません。

 どの方法を使用しても良いですが、個人的にはエラーテンプレートを指定する方法が一番わかりやすいかなと思います。
 次回はIDataErrorInfoを使用する場合のデータ検証について紹介します。

テーマ : プログラミング
ジャンル : コンピュータ

WebBrowserコントロールでXButtonを押した際に「戻る」「進む」を行う

 WebBrowserコントロールを使うことで簡易型のブラウザを簡単に作ることができます。
 「戻る」や「進む」といった機能もWebBrowserコントロールから提供されているので、あとは戻るボタン等を作るだけでブラウザを作ることができます。
 ショートカットも定義済みの様で、「Alt + Left」で「戻る」、「Alt + Right」で「進む」が実行されます。
 色々やってくれて気が利いてると思うでしょうが、落とし穴もあります。
 WebBrowserコントロールではマウスボタンが押された等のイベントを補足できません。また、WebBrowserでイベントが終了するのでWebBrowser以下の場所でイベントを補足することもできません。
 ウェブブラウザを使う方ならマウスの横ボタン(XButton)で「戻る」や「進む」を行うことが多いと思います。
 このXButtonはマウスによってあったりなかったりするので標準機能ではありません。そのため、WebBrowserのショートカットにも組み込まれていません。
 通常ならMouseUpやMouseDownイベントでXButtonが押されたかどうかをチェックして処理を行うところですが、WebBrowserコントロールではイベントを補足できないのでこの方法が使えません。
 そこで、HotKeyを設定する方法で使用した方法を利用してXButtonが押されていることを把握してみようと思います。
 以下のコードは、XButton1で「戻る」、XButton2で「進む」を行うビヘイビアのコードです。
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Interop;

namespace WpfExtensions.WebBrowsing.Behaviors
{
    /// <summary>
    /// WebBrowserでマウスのXButtonを使って「戻る」、「進む」を実行するためのビヘイビア
    /// </summary>
    public class WebBrowserXButtonNavigationBehaviors : Behavior<WebBrowser>
    {
        /// <summary>
        ///  XButton1又はXButton2を離した際のメッセージ
        /// </summary>
        private const int WM_XBUTTONUP = 0x020C;

        /// <summary>
        /// XButton1を押した場合の値
        /// </summary>
        private const int XBUTTON1 = 0x10000;

        /// <summary>
        /// XButton2を押した場合の値
        /// </summary>
        private const int XBUTTON2 = 0x20000;

        protected override void OnAttached()
        {
            base.OnAttached();
            ComponentDispatcher.ThreadPreprocessMessage += ComponentDispatcher_ThreadPreprocessMessage;
        }

        protected override void OnDetaching()
        {
            ComponentDispatcher.ThreadPreprocessMessage -= ComponentDispatcher_ThreadPreprocessMessage;
            base.OnDetaching();
        }

        void ComponentDispatcher_ThreadPreprocessMessage(ref MSG msg, ref bool handled)
        {
            if(msg.message == WM_XBUTTONUP)
            {
                var wParam = msg.wParam.ToInt32();

                // 戻る
                if(AssociatedObject.CanGoBack && wParam == XBUTTON1)
                {
                    AssociatedObject.GoBack();
                }

                // 進む
                if(AssociatedObject.CanGoForward && wParam == XBUTTON2)
                {
                    AssociatedObject.GoForward();
                }
            }
        }
    }
}
 HotKeyの時と同じ方法でXButtonが押されたかどうかを把握しています。
 このページによると、XButtonがUpした際のメッセージは[0x020C]となります。
 どのXButtonが押されたかはwParamの値で判断できますが、このページの値を使用してもうまくいきませんでした。
 XButton1、XButton2が押された際のwParamの値をMessageBoxで表示させて調べたところXButton1が押されたときは[0x10000]、XButton2が押されたときは[0x20000]になるようです。
 あとは、押されたボタンに合わせて「戻る」又は「進む」の処理を行うだけです。
 「戻る」が可能かどうかはWebBrowserコントロールのCanGoBackプロパティで確認できます。必ず、この値を確認し「戻る」が可能な場合のみGoBackメソッドを呼ぶようにしてください。戻れないときにGoBackメソッドを実行すると例外が発生します。
 これは「進む」の場合も同様です。

 このビヘイビアを使用する場合は以下の様にWebBrowserにアタッチするだけです。
<WebBrowser Source="https://www.google.co.jp/?gws_rd=ssl">
    <i:Interaction.Behaviors>
        <ex:WebBrowserXButtonNavigationBehaviors/>
    </i:Interaction.Behaviors>
</WebBrowser>
 これで、XButtonを使って「戻る」「進む」が可能なブラウザになります。

テーマ : プログラミング
ジャンル : コンピュータ

WPFでHotKeyを設定する方法

 WPFアプリケーションにHotKeyを設定するにはWinAPIを使用するしかありません。
 使用するのはuser32.dllのRegisterHotKey関数とUnregisterHotKey関数です。
 以下のコードはHotKeyが押された際にMessageBoxを表示するサンプルコードです。
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;

namespace HotKeySample.Views
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        /*
         * HotKey登録時に指定するID。
         * アプリケーションの場合は、0x0000~0xbfffの間で指定すること。
         * (共有DLLの場合は、0xc000~0xffffの間を使用する。)
         */
        private const int HOTKEY_ID1 = 0x0001;
        private const int HOTKEY_ID2 = 0x0002;

        // HotKey Message ID
        private const int WM_HOTKEY = 0x0312;

        private IntPtr WindowHandle;

        /* 
         * HotKey登録を行う関数。
         * 失敗の場合は0(他で使用されている)、成功の場合は0以外の数値が返される。
         */
        [DllImport("user32.dll")]
        private static extern int RegisterHotKey(IntPtr hWnd, int id, int modKey, int vKey);

        /*
         * HotKey解除を行う関数。
         * 失敗の場合は0、成功の場合は0以外の数値が返される。
         */
        [DllImport("user32.dll")]
        private static extern int UnregisterHotKey(IntPtr hWnd, int id);

        public MainWindow()
        {
            InitializeComponent();

            // WindowHandleを取得
            var host = new WindowInteropHelper(this);
            WindowHandle = host.Handle;

            // HotKeyを設定
            SetUpHotKey();
            ComponentDispatcher.ThreadPreprocessMessage += ComponentDispatcher_ThreadPreprocessMessage;
        }

        // HotKey登録。
        private void SetUpHotKey()
        {
            // Alt + 1 をHotKeyとして登録。
            var result1 = RegisterHotKey(WindowHandle, HOTKEY_ID1, (int)ModifierKeys.Alt, KeyInterop.VirtualKeyFromKey(Key.D1));
            if(result1 == 0)
            {
                MessageBox.Show("HotKey1の登録に失敗しました。");
            }

            // Alt + テンキーの1 をHotKeyとして登録。
            var result2 = RegisterHotKey(WindowHandle, HOTKEY_ID2, (int)ModifierKeys.Alt, KeyInterop.VirtualKeyFromKey(Key.NumPad1));
            if(result2 == 0)
            {
                MessageBox.Show("HotKey2の登録に失敗しました。");
            }
        }

        // ここでHotKeyが押された際の挙動を設定する。
        void ComponentDispatcher_ThreadPreprocessMessage(ref MSG msg, ref bool handled)
        {
            // HotKeyが押されたかどうかを判定。
            if (msg.message != WM_HOTKEY) return;

            switch(msg.wParam.ToInt32())
            {
                case HOTKEY_ID1:
                    MessageBox.Show("HotKey1(Alt + D1)");
                    break;
                case HOTKEY_ID2:
                    MessageBox.Show("HotKey2(Alt + NumPad1)");
                    break;
                default: 
                    break;
            }
        }

        // Windowを閉じる際にHotKeyの解除を行う。
        private void Window_Closed(object sender, EventArgs e)
        {
            UnregisterHotKey(WindowHandle, HOTKEY_ID1);
            UnregisterHotKey(WindowHandle, HOTKEY_ID2);
            ComponentDispatcher.ThreadPreprocessMessage -= ComponentDispatcher_ThreadPreprocessMessage;
        }
    }
}
 HotKeyの登録にはuser32.dllのRegisterHotKey関数を使用します。逆に、HotKeyを解除するにはUnregisterHotKey関数を使用します。
 これらの関数の引数にはウインドウハンドルが必要なので、まずはウインドウハンドルを取得します。
 ウインドウハンドルを取得するにはWindowInteropHelperを使用します。

 次にHotKey登録を行いますが、ここではキーストロークを登録するだけで、HotKeyが押された際の処理は後で定義します。
 RegisterHotKey関数に、ウインドウハンドル、HotKeyのID、HotKeyのキーストロークを指定します。
 キーストロークはModifierKey(AltとかCtrlとか)とキーをそれぞれ指定します。ModifierKeyの場合はModifierKeysの値をint型にキャストすればOKですが、キーの方はKeyの値をintにキャストする方法だとうまくいきません。
 これは、RegisterHotKeyが求めるキーは仮想キーコードなのですが、Key列挙体は仮想キーコードとは別の値が割り振られているためです。そのためKey列挙体の値を仮想キーコードに変換する必要があります。
 Keyの値を仮想キーコードに変換するには、KeyInterop.VirtualKeyFromKeyメソッドを使用します。
 HotKeyの登録が成功した場合はRegisterHotKeyの戻り値に0以外の数値が返されます。必ず、戻り値をチェックしてHotKeyの登録が成功したかどうかを確認してください。
 ここでは2つのHotKeyを指定しています。見ての通り、「キーボートの1」と「テンキーの1」は別々のキーとして判断されます。HotKeyに数値を使用する場合は注意してください。

 次に、HotKeyが押された際の処理を指定します。
 HotKeyが押されたかどうかはComponentDispatcher.ThreadPreprocessMessageイベントで送られてくるメッセージを見て判断します。
 登録したHotKeyが押された場合、msg.messageにはWM_HOTKEYの値が入っています。
 そして、msg.wParamに押されたHotKeyのIDが入っています。
 これらの値をチェックしてどのHotKeyが押されたかを判断し、それぞれのHotKeyに合わせた処理を実行します。

 HotKeyは必ず解除してください。このサンプルではウインドウが閉じられたときに解除を行っています。
 HotKeyの解除はUnregisterHotKey関数にウインドウハンドルと解除するHotKey IDを指定するだけです。

テーマ : プログラミング
ジャンル : コンピュータ

ドラッグでListBoxItemを移動させる

 コレクションをListBoxにバインドし、データテンプレートを使用してデータを図で表示したと思うことがあります。しかも、ユーザーが見やすいように図の位置を微調整したい(図を動かして位置調整したい)こともあります。
 データテンプレートで設定した内容はListBoxItemというコンテナに入れられて表示されます。そのため、表示されている図をドラッグで移動させるにはListBoxItemを移動させる必要があります。
 ListBoxItemにビヘイビアを設定することはできません。そのため、Blend SDKのMouseDragElementBehaviorや前回作成したビヘイビアは使えません。
 そんなわけで、ListBoxItemをドラッグで移動させたいのならば別のビヘイビアを作成する必要があります。
 今回作成するビヘイビアの要点を上げると以下の様になります。
・ListBox用のビヘイビアを作成し、そのListBoxに含まれるListBoxItemをドラッグで移動できるようにする。
・せっかくなのでListBoxのパネルをビヘイビア側でCanvasに変更する。
・前回と同じようにX軸方向、Y軸方向の移動制限を行えるようにする。

 以下は、ListBoxItemを移動させるためのビヘイビアのコードです。(Blend SDKを使用しています)
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;

namespace WpfExtensions.Behaviors.Actions
{
    /// <summary>
    /// ListBoxItemをドラッグで移動させる為のビヘイビア
    /// </summary>
    public class MouseDragListBoxItemBehavior : Behavior<ListBox>
    {
        private bool IsDrag = false;
        private ListBoxItem TargetListBoxItem;
        private Point MouseStartPosition;
        private Point ListBoxItemStartPosition;
        private Canvas MoveArea;


        #region LockX
        /// <summary>
        /// X軸方向への移動を固定するかどうかを設定、又は取得する。
        /// </summary>
        public bool LockX
        {
            get { return (bool)GetValue(LockXProperty); }
            set { SetValue(LockXProperty, value); }
        }

        public static readonly DependencyProperty LockXProperty =
            DependencyProperty.Register("LockX", typeof(bool), typeof(MouseDragListBoxItemBehavior), new PropertyMetadata(false));
        #endregion

        #region LockY
        /// <summary>
        /// Y軸方向への移動を固定するかどうかを設定、又は取得する。
        /// </summary>

        public bool LockY
        {
            get { return (bool)GetValue(LockYProperty); }
            set { SetValue(LockYProperty, value); }
        }

        public static readonly DependencyProperty LockYProperty =
            DependencyProperty.Register("LockY", typeof(bool), typeof(MouseDragListBoxItemBehavior), new PropertyMetadata(false));
        #endregion

        protected override void OnAttached()
        {
            // ListBoxのItemsPanelにCanvasを指定する
            SetCanvas();

            AssociatedObject.Loaded += AssociatedObject_Loaded;
            AssociatedObject.PreviewMouseLeftButtonDown += AssociatedObject_PreviewMouseLeftButtonDown;
            base.OnAttached();
        }        

        protected override void OnDetaching()
        {
            AssociatedObject.Loaded -= AssociatedObject_Loaded;
            AssociatedObject.PreviewMouseLeftButtonDown -= AssociatedObject_PreviewMouseLeftButtonDown;
            base.OnDetaching();
        }

        // ListBoxのItemsPanelTemplateにCanvasを設定する。
        private void SetCanvas()
        {
            var canvasFactory = new FrameworkElementFactory(typeof(Canvas));
            var itemsPanelTemplate = new ItemsPanelTemplate();
            itemsPanelTemplate.VisualTree = canvasFactory;
            itemsPanelTemplate.Seal();
            AssociatedObject.ItemsPanel = itemsPanelTemplate;
        }

        // Canvasインスタンスを取得する。
        // OnAttachedではインスタンスの取得ができなかったのでLoadedイベントで取得
        void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            MoveArea = (Canvas)FindControl(AssociatedObject, typeof(Canvas));
        }

        // ドラッグ開始
        void AssociatedObject_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (MoveArea == null) return;

            // マウスの位置を取得
            MouseStartPosition = e.GetPosition(AssociatedObject);

            // ドラッグ対象のListBoxItemを取得
            TargetListBoxItem = GetListBoxItem(MouseStartPosition);
            if (TargetListBoxItem == null) return;

            // ListBoxItemの位置を取得する
            ListBoxItemStartPosition = new Point(Canvas.GetLeft(TargetListBoxItem), Canvas.GetTop(TargetListBoxItem));
            if (double.IsNaN(ListBoxItemStartPosition.X)) ListBoxItemStartPosition.X = 0.0;
            if (double.IsNaN(ListBoxItemStartPosition.Y)) ListBoxItemStartPosition.Y = 0.0;

            // ListBoxItemのMouseMove, MouseLeftButtonUpイベントを登録
            TargetListBoxItem.MouseMove += TargetListBoxItem_MouseMove;
            TargetListBoxItem.MouseLeftButtonUp += TargetListBoxItem_MouseLeftButtonUp;

            // ドラッグ開始
            IsDrag = true;
            TargetListBoxItem.CaptureMouse();
        }

        // ListBoxItemのドラッグ移動処理
        void TargetListBoxItem_MouseMove(object sender, MouseEventArgs e)
        {
            if (MoveArea == null || TargetListBoxItem == null || !IsDrag) return;

            // 座標計算
            var currentMousePosition = e.GetPosition(AssociatedObject);
            var currentListBoxItemPosition = ListBoxItemStartPosition + (currentMousePosition - MouseStartPosition);

            if(!LockX)
            {
                var maxX = MoveArea.ActualWidth - TargetListBoxItem.ActualWidth;
                if(currentListBoxItemPosition.X < 0)
                {
                    currentListBoxItemPosition.X = 0.0;
                }
                else if(maxX < currentListBoxItemPosition.X)
                {
                    currentListBoxItemPosition.X = maxX;
                }

                Canvas.SetLeft(TargetListBoxItem, currentListBoxItemPosition.X);
            }

            if(!LockY)
            {
                var maxY = MoveArea.ActualHeight - TargetListBoxItem.ActualHeight;
                if(currentListBoxItemPosition.Y < 0)
                {
                    currentListBoxItemPosition.Y = 0.0;
                }
                else if(maxY < currentListBoxItemPosition.Y)
                {
                    currentListBoxItemPosition.Y = maxY;
                }

                Canvas.SetTop(TargetListBoxItem, currentListBoxItemPosition.Y);
            }

            e.Handled = true;
        }

        // ドラッグ終了(後始末)
        void TargetListBoxItem_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (TargetListBoxItem == null) return;

            IsDrag = false;
            TargetListBoxItem.ReleaseMouseCapture();
            TargetListBoxItem.MouseMove -= TargetListBoxItem_MouseMove;
            TargetListBoxItem.MouseLeftButtonUp -= TargetListBoxItem_MouseLeftButtonUp;
            TargetListBoxItem = null;

            e.Handled = true;
        }

        // 最初に見つかった目的のコントロールを返す
        private DependencyObject FindControl(DependencyObject obj, Type controlType)
        {
            if (obj == null) return null;
            if (obj.GetType() == controlType) return obj;

            var childrenCount = VisualTreeHelper.GetChildrenCount(obj);
            for(var i = 0; i < childrenCount; i++)
            {
                var child = VisualTreeHelper.GetChild(obj, i);
                var descendant = FindControl(child, controlType);
                if (descendant != null && descendant.GetType() == controlType) return descendant;
            }

            return null;
        }

        // マウスカーソル位置にあるListBoxItemを取得する。
        // ビヘイビアがアタッチされたListBoxのItemのみ返す。
        private ListBoxItem GetListBoxItem(Point position)
        {
            var result = VisualTreeHelper.HitTest(AssociatedObject, position);
            if (result == null) return null;

            // 目的のListBoxItemまで辿っていく
            var item = result.VisualHit;
            while(item != null)
            {
                if(item is ListBoxItem)
                {
                    // ビヘイビアがアタッチされたListBoxのListBoxItemなら終了
                    var index = AssociatedObject.ItemContainerGenerator.IndexFromContainer(item, true);
                    if (index >= 0) break;
                }

                // 次の調査項目を取得する
                item = VisualTreeHelper.GetParent(item);
            }

            return item != null ? (ListBoxItem)item : null;
        }
    }
}
 LockX, LockYについては前回と同じなので解説は省略します。
 SetCanvasメソッドの処理でListBoxのパネルをCanvasに置き換えています。
 C#でテンプレートをいじるなんて今回が初めてだったので、あまり詳しいことは解りませんが、テンプレートを操作するときにはFrameworkElementFactoryを使用して行うようです。
 FrameworkElementFactoryのインスタンスで作成したいコントロールの型を指定します。このfactoryインスタンスをItemPanelTemplateインスタンスのVisualTreeプロパティにセットします。
 あとはListBoxのItemsPanelにItemPanelTemplateインスタンスをセットするだけです。
 SetCanvasメソッドはOnAttachedメソッドで呼んでいますが、この時点ではまだCanvasは作成されていません。そのため、ここでCanvasを取得しようとすると失敗します。
 Canvasインスタンスの取得はLoadedイベントで行っています。
 コントロールを取得するにはVisualTreeHelperを使用して目的のコントロールの探索を行います。
 VisualTreeHelperで子要素を取得し、目的のコントロールと同じかどうかを調べています。

 ListBoxItemの移動ですが、座標計算等の処理は前回と同じなのですが、このビヘイビアはListBoxにアタッチされているので、まずは対象のListBoxItemを取得するところから始める必要があります。
 ListBoxのPreviewMouseLeftButtonDownイベントがドラッグ開始処理のトリガーとなります。
 ここでマウスの位置を元にドラッグの対象となるListBoxItemを取得します。
 ListBoxItem取得処理はGetListBoxItemメソッドで行っています。
 GetListBoxItemメソッドでは、ビヘイビアをアタッチしたListBoxのListBoxItemのみをターゲットにしています。
 これは、子要素にListBoxがある場合、子要素のListBoxのアイテムだけが移動してしまい、意図したとおりに移動できなくなることを避けるためです。
 そのため、ListBoxItemかどうかとAssociatedObject(アタッチしたListBox)に含まれるかどうかをチェックしています。

 ドラッグするのはListBoxItemなので、取得したListBoxItemのMouseMoveとMouseLeftButtonUpにイベントハンドラを追加します。
 MouseMoveの処理は前回とほぼ同じです。
 最後にe.Handledをtrueにしていますが、これを忘れると処理が重たくなり、移動がスムーズに行えなくなります。
 MouseLeftButtonUpでドラッグ終了時の処理を行っています。

 このビヘイビアをListBoxにアタッチさせれば、ListBoxItemをドラッグで移動できるようになります。
<Window x:Class="MouseDragDemo2.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        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:v="clr-namespace:MouseDragDemo2.Views"
        xmlns:vm="clr-namespace:MouseDragDemo2.ViewModels"
        xmlns:ex="http://wpf-extensions.net/2014/wpf"
        Title="MainWindow" Height="300" Width="225">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
        
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <ListBox Grid.Row="0" ItemsSource="{Binding Items}">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Canvas.Left" Value="{Binding X, Mode=TwoWay}"/>
                    <Setter Property="Canvas.Top" Value="{Binding Y, Mode=TwoWay}"/>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemTemplate>
                <DataTemplate DataType="{x:Type vm:ItemViewModel}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition/>
                        </Grid.RowDefinitions>
                        <TextBlock Grid.Row="0" Text="{Binding Name}" Background="Gray" Foreground="White"/>
                        <ListBox Grid.Row="1" ItemsSource="{Binding Colors}">
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <Ellipse Stroke="Black" Fill="{Binding}" Width="20" Height="20"/>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
            <i:Interaction.Behaviors>
                <ex:MouseDragListBoxItemBehavior/>
            </i:Interaction.Behaviors>
        </ListBox>
        
        <ListView Grid.Row="1" ItemsSource="{Binding Items}">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="名前" DisplayMemberBinding="{Binding Name}"/>
                    <GridViewColumn Header="X" DisplayMemberBinding="{Binding X}"/>
                    <GridViewColumn Header="Y" DisplayMemberBinding="{Binding Y}"/>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</Window>
 ViewModelではバインドするデータを用意しているだけなので、コードは省略します。
 ListBoxにMouseDragListBoxItemBehaviorをアタッチすれば、ListBoxItemの移動ができるようになります。
 アイテムの見た目はデータテンプレートで変更しています。見ての通り、内部にListBoxを持っています。
 ListBoxItemの座標とバインディングしたい場合は、ListBox.ItemContainerStyleでバインディングを行います。
 これを実行すると以下の様になります。
ListBoxItemDragMove.png
 アタッチしたListBoxのアイテムのみが対象になっているので、ListBoxItemの中にある子ListBoxのアイテム(色つきの○)をドラッグしても、問題なく移動できます。

テーマ : プログラミング
ジャンル : コンピュータ

ドラッグで要素を移動させるビヘイビアを作る

 前回はBlend SDKのMouseDragElementBehaviorを使用して要素を移動させる方法を紹介しました。この方法だと要素の座標が常にルートパネル上の位置になるため扱いにくいという話をしました。
 今回は、要素の位置を指定したCanvas上の位置で指定できる要素をドラッグで移動させるビヘイビアを作ります。
 ただ移動させるだけではもったいないので、X軸方向への移動をロック、Y軸方向への移動をロックさせる機能もついでにつけます。
 このビヘイビアの要点をまとめると、
1)指定したCanvas内で要素を移動させる。
2)要素の座標は指定したCanvas内の座標になる。
3)X軸方向への移動ロック機能(おまけ)
4)Y軸方向への移動ロック機能(おまけ)
5)このビヘイビアはBlend SDKを利用して作成する。
となります。
 まずはビヘイビアのコードを紹介します。
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace WpfExtensions.Behaviors.Actions
{
    /// <summary>
    /// 要素をドラッグで移動可能にするビヘイビア
    /// </summary>
    public class MouseDragElementBehaviorEx : Behavior<FrameworkElement>
    {
        private bool IsDrag = false;
        private Point MouseStartPosition;
        private Point ElementStartPosition;


        #region ParentCanvas
        /// <summary>
        /// 移動させる要素の親Canvasを設定、又は取得する。
        /// </summary>
        public Canvas ParentCanvas
        {
            get { return (Canvas)GetValue(ParentCanvasProperty); }
            set { SetValue(ParentCanvasProperty, value); }
        }

        public static readonly DependencyProperty ParentCanvasProperty =
            DependencyProperty.Register("ParentCanvas", typeof(Canvas), typeof(MouseDragElementBehaviorEx), new PropertyMetadata(null));
        #endregion

        #region LockX
        /// <summary>
        /// X軸方向への移動を固定するかどうかを設定、又は取得する。
        /// </summary>
        public bool LockX
        {
            get { return (bool)GetValue(LockXProperty); }
            set { SetValue(LockXProperty, value); }
        }

        public static readonly DependencyProperty LockXProperty =
            DependencyProperty.Register("LockX", typeof(bool), typeof(MouseDragElementBehaviorEx), new PropertyMetadata(false));
        #endregion

        #region LockY
        /// <summary>
        /// Y軸方向への移動を固定するかどうかを設定、又は取得する。
        /// </summary>
        public bool LockY
        {
            get { return (bool)GetValue(LockYProperty); }
            set { SetValue(LockYProperty, value); }
        }

        public static readonly DependencyProperty LockYProperty =
            DependencyProperty.Register("LockY", typeof(bool), typeof(MouseDragElementBehaviorEx), new PropertyMetadata(false));
        #endregion

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
            AssociatedObject.MouseMove += AssociatedObject_MouseMove;
            AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
        }

        protected override void OnDetaching()
        {
            AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
            AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
            AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
            base.OnDetaching();
        }

        // ドラッグ開始時の位置を取得し、ドラッグ処理を開始する
        void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (ParentCanvas == null) return;

            // マウス位置取得
            MouseStartPosition = e.GetPosition(ParentCanvas);

            // 要素の位置取得
            ElementStartPosition = new Point(Canvas.GetLeft(AssociatedObject), Canvas.GetTop(AssociatedObject));
            if (double.IsNaN(ElementStartPosition.X)) ElementStartPosition.X = 0.0;
            if (double.IsNaN(ElementStartPosition.Y)) ElementStartPosition.Y = 0.0;

            // ドラッグ開始
            IsDrag = true;
            AssociatedObject.CaptureMouse();
        }

        // ドラッグ中の移動処理
        void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
        {
            if (ParentCanvas == null) return;
            if (!IsDrag) return;

            // 移動後の要素の位置を計算する
            var currentMousePosition = e.GetPosition(ParentCanvas);
            var currentElementPosition = ElementStartPosition + (currentMousePosition - MouseStartPosition);

            // ParentCanvas内に収まる様に修正し移動させる
            if (!LockX)
            {
                var maxX = ParentCanvas.ActualWidth - AssociatedObject.ActualWidth;
                if (currentElementPosition.X < 0)
                {
                    currentElementPosition.X = 0.0;
                }
                else if (maxX < currentElementPosition.X)
                {
                    currentElementPosition.X = maxX;
                }

                // X軸方向の移動
                Canvas.SetLeft(AssociatedObject, currentElementPosition.X);
            }

            if (!LockY)
            {
                var maxY = ParentCanvas.ActualHeight - AssociatedObject.ActualHeight;
                if (currentElementPosition.Y < 0)
                {
                    currentElementPosition.Y = 0.0;
                }
                else if (maxY < currentElementPosition.Y)
                {
                    currentElementPosition.Y = maxY;
                }

                // Y軸方向の移動
                Canvas.SetTop(AssociatedObject, currentElementPosition.Y);
            }
        }

        // ドラッグ解除
        void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (IsDrag)
            {
                IsDrag = false;
                AssociatedObject.ReleaseMouseCapture();
            }
        }
    }
}
 ParentCanvasは要素の移動範囲(Canvas)を指定させるためのプロパティです。
 LockXとLockYは要素の移動方向に制限を付けるかどうかのプロパティです。
 これらのプロパティは依存関係プロパティで定義します。(コードスニペットpropdpを使うと簡単に定義できます)
 通常のCLRプロパティでもエラーにはなりませんが、その場合データバインディングはできなくなります。データバインディング可能なプロパティにしたい場合は依存関係プロパティにする必要があります。
 このビヘイビアにXやYといった要素の位置を示すプロパティは作っていません。
 要素の位置はCanvas.Top、Canvas.Leftで指定させるので、ビヘイビアに位置情報を作成する必要がないためです。

 OnAttachedメソッドをオーバーライドしてビヘイビアの機能を付加します。
 OnDetachingメソッドではOnAttachedでアタッチしたイベントハンドラの解除を行います。

 ビヘイビアの機能ですが、単純なドラッグ操作です。そのため、
1)MouseLeftButtonDownイベントでドラッグ開始処理とドラッグ開始時の位置を取得。
2)MouseMoveイベントで、要素の位置を計算し移動。
3)MouseLeftButtonUpイベントで、ドラッグ処理終了。
といった王道パターンで処理を行います。
 まず、MouseLeftButtonDownイベントでドラッグ開始時のマウスと要素の位置を取得します。
 マウスの位置はMouseButtonEventArgs(e)のGetPositionメソッドから取得できます。
 要素の位置はCanvasクラスのGetLeft、GetTopメソッドで位置を取得できます。
 ここでは、念のため値のチェックを行いdouble.NaNの場合は0.0を入力しています。
 あとはドラッグ処理を開始するためのフラグ(IsDrag)を立て、対象の要素にマウスをキャプチャーさせます。

 次に、MouseMoveイベントでドラッグしている際の要素の移動処理を行います。
 現在のマウスの位置を取得し、ドラッグ開始時の位置から移動量を計算し、ドラッグ開始時の要素の位置にマウスの移動量を加えれば現在の要素の位置が計算できます。
 あとは要素の位置を移動するだけなのですが、要素がキャンバスからはみ出ないように補正を加えています。
 また、LockX、LockYの値をチェックしtrueの場合はその方向の移動を行わないようにしています。
 要素の移動はCanvasクラスのSetLeft、SetTopメソッドを使用して行います。

 最後に、MouseLeftButtonUpイベントでドラッグの終了処理を行います。

 このビヘイビアを使って要素を動かしてみましょう。
<Window x:Class="MouseDragElementBehaviorExDemo.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        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:v="clr-namespace:MouseDragElementBehaviorExDemo.Views"
        xmlns:vm="clr-namespace:MouseDragElementBehaviorExDemo.ViewModels"
        xmlns:ex="http://wpf-extensions.net/2014/wpf"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
        
    <Grid ShowGridLines="True">
        <Grid.RowDefinitions>
            <RowDefinition Height="100"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        
        <StackPanel Grid.Row="0" Grid.Column="1">
            <TextBlock Text="{Binding XPosition, StringFormat=X:{0}}"/>
            <TextBlock Text="{Binding YPosition, StringFormat=Y:{0}}"/>
        </StackPanel>
        
        <Canvas Grid.Row="1" Grid.Column="1" Name="MoveArea">
            <Rectangle Width="20" Height="20" Fill="Red"
                       Canvas.Top="{Binding YPosition, Mode=TwoWay}" 
                       Canvas.Left="{Binding XPosition, Mode=TwoWay}">
                <i:Interaction.Behaviors>
                    <ex:MouseDragElementBehaviorEx ParentCanvas="{Binding ElementName=MoveArea}"/>
                </i:Interaction.Behaviors>
            </Rectangle>
        </Canvas>
    </Grid>
</Window>
 MouseDragElementBehaviorExを使うためにXAML名前空間を追加しています(プレフィックスはexとしました)。
 このビヘイビアでは移動させたい要素は必ずCanvas内に設置されている必要があります。
 また、要素の位置はCanvas.TopとCanvas.Leftで行います。データバインディングしたい場合はModeをTwoWayにする必要があります。
 あとは、移動させたい要素にMouseDragElementBehaviorExをアタッチし、ParentCanvasにCanvasを指定すればOKです。
 このコードを実行すると、以下のようになります。
MouseDragElementBehaviorDemo2.png
 要素は指定したCanvas内でのみ移動できます。また、要素の位置はそのCanvas上の位置になります。
 Canvasの左上角に要素を押し付けた時の画像ですが、XとYの値が0になっているのが確認できます。

テーマ : プログラミング
ジャンル : コンピュータ

ドラッグで要素を移動させる

 WPFで要素(ボタンとか)をドラッグで移動させるだけなら、動かしたい要素にBlend SDKのMouseDragElementBehaviorを指定するだけでOKです。
 親パネルがCanvasである必要すらありません。
<Window x:Class="MouseDragElementBehabiorDemo.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        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:v="clr-namespace:MouseDragElementBehabiorDemo.Views"
        xmlns:vm="clr-namespace:MouseDragElementBehabiorDemo.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
        
    <Grid ShowGridLines="True">
        <Grid.RowDefinitions>
            <RowDefinition Height="100"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        
        <StackPanel Grid.Row="0" Grid.Column="1">
            <TextBlock Text="{Binding XPosition, StringFormat=X:{0}}"/>
            <TextBlock Text="{Binding YPosition, StringFormat=Y:{0}}"/>
        </StackPanel>
        
        <StackPanel Grid.Row="1" Grid.Column="1">
            <Rectangle Width="20" Height="20" Fill="Red">
                <i:Interaction.Behaviors>
                    <ei:MouseDragElementBehavior ConstrainToParentBounds="True"
                                                 X="{Binding XPosition, Mode=TwoWay}"
                                                 Y="{Binding YPosition, Mode=TwoWay}"/>
                </i:Interaction.Behaviors>
            </Rectangle>
        </StackPanel>      
    </Grid>
</Window>
 赤い四角をドラッグで移動させることができます。
 MouseDragElementBehaviorのXプロパティとYプロパティを利用することで、要素の位置を取得することもできます。
 ConstrainToParentBoundsをTrueにすると、親パネルの範囲内でのみ移動可能になります。(デフォルトではFalseになっています。)
 この場合はGrid.Row=1, Grid.Column=1にあるStackPanel内でのみ移動できます。それ以外の範囲には移動できません。

 要素をただ移動させるだけならこのビヘイビアを使えば良いのですが、座標を使って何か処理を行う場合は少し問題があります。
 赤い四角をStackPanelの左上角に押し付けた場合、以下の様になります。
MouseDragElementBehaviorDemo1.png
 座標を確認してほしいのですが、X=100, Y=100となっています。
 このように親パネル上の位置ではなく、常にルートパネルの位置で座標が返されます。
 親パネルの座標が欲しい場合は、残念ながら自分でビヘイビアを作るしかありません。
 そんなわけで、次回は親パネル上の位置を取得できるMouseDragElementBehaviorを自作してみます。    

テーマ : プログラミング
ジャンル : コンピュータ

WPFで要素を画像として保存する

 WPFで要素(ボタンとかパネルとか)を画像として保存したいと思うことがあります。
 画像として保存したい要素をビットマップにレンダリングし画像として保存することができますが、画像として保存する要素の位置に注意する必要があります。
 以下のサンプルコードで要素の位置の違いで画像がどのように変化するかを見てみましょう。
<Window x:Class="SaveImage.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        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:v="clr-namespace:SaveImage.Views"
        xmlns:vm="clr-namespace:SaveImage.ViewModels"
        Title="MainWindow" Height="150" Width="150">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <Grid ShowGridLines="True">
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="30"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        
        <Rectangle Grid.Row="0" Grid.Column="0" Name="Rect1"
                   HorizontalAlignment="Center" VerticalAlignment="Center"
                   Width="20" Height="20" Fill="Red"/>
        
        <Rectangle Grid.Row="0" Grid.Column="1" Name="Rect2"
                   HorizontalAlignment="Left" VerticalAlignment="Top"
                   Width="30" Height="30" Fill="Blue"/>
        
        <Grid Grid.Row="1" Grid.Column="1" Name="Panel1">
            <Grid Name="Panel2">
                <Rectangle HorizontalAlignment="Center" VerticalAlignment="Center"
                           Width="50" Height="50" Fill="Green"/>
            </Grid>
        </Grid>
        
        <Button Grid.Row="3" Grid.Column="1" Content="画像として保存" Click="Button_Click"/>
    </Grid>
</Window>
 このコードを実行すると以下の様な画面が表示されます。
SaveImageWindow.png
 「画像として保存」ボタンを押すと、いくつかの要素をJPEG画像で保存します。
 画像として保存する際のコードは以下の通りです。
using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace SaveImage.Views
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            var rect1ImagePath = System.IO.Path.Combine(desktopPath, "Rect1.jpg");
            var rect2ImagePath = System.IO.Path.Combine(desktopPath, "Rect2.jpg");
            var panel1ImagePath = System.IO.Path.Combine(desktopPath, "Panel1.jpg");
            var panel2ImagePath = System.IO.Path.Combine(desktopPath, "Panel2.jpg");

            SaveImage(this.Rect1, rect1ImagePath);
            SaveImage(this.Rect2, rect2ImagePath);
            SaveImage(this.Panel1, panel1ImagePath);
            SaveImage(this.Panel2, panel2ImagePath);
        }

        // 要素を画像として保存する
        private void SaveImage(FrameworkElement target, string path)
        {
            if (target == null) throw new ArgumentNullException("target");
            if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("pathが未設定");

            // レンダリング
            var bmp = new RenderTargetBitmap(
                (int)target.ActualWidth,
                (int)target.ActualHeight,
                96, 96, // DPI
                PixelFormats.Pbgra32);
            bmp.Render(target);

            // jpegで保存
            var encoder = new JpegBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(bmp));
            using(var fs = File.Open(path, FileMode.Create))
            {
                encoder.Save(fs);
            }
        }
    }
}
 RenderTargetBitmapのコンストラクタではレンダリングするサイズ等を指定する必要があります。
 サイズは画像として保存したい要素のActualWidth、ActualHeightプロパティを用いることで描画されている要素のサイズを取得しています。
 DPIはWindowsでは一般的な96としています。
 コンストラクタでは入れ物を用意しただけでまだ要素のレンダリングはされていません。
 Renderメソッドに画像として保存した要素を指定し、レンダリングを行います。
 次に、レンダリングした内容を画像ファイルとして保存します。
 画像ファイルに保存するにはXxxBitmapEncoderクラスを使用します。今回はJPEGで保存させるのでJpegBitmapEncoderを使用します。(他の画像フォーマットでも保存の仕方は同じです。)
 ボタンをクリックするとRect1(赤い四角)、Rect2(青い四角)、Panel1(Penel2を持つGrid)、Panel2(緑糸の四角を持つGrid)の4つの要素をそれぞれ画像として保存します。
 では、ボタンを押すと、どのような画像が作成されるのかを見ていきます。
 まず、Rect1(赤い四角)を保存した結果は以下の様になります。
Rect1.jpg
 パネルの背景色が透過色になっているためJPEGだと黒くなっています。(PNG等の透過色をサポートする画像フォーマットの場合は透過色になります)
 Rect1を保存したはずなのに上と左に余計なスペース(黒い部分)が入っています。また、赤い四角も一部見切れています。
 これは、レンダリングする際に、親パネルの左上の角が基準となっているためです。
 Rect1のサイズは幅、高さ共に20です。そのため、Gridの左上角の位置から幅20、高さ20の領域がレンダリングされます。

 次に、Rect2(青い四角)を保存した結果は以下の様になります。
Rect2.jpg
 全て黒くなっており、青い四角はありません。
 青い四角はセル(Row=0, Column=1)の上と左に寄せられているため余計な余白は含んでいません。そのため、うまくいくように思えますが、起点となるのはセルの左上角ではなくパネルの左上角です。
 そのため、青色の四角がある領域までレンダリング範囲が届かなかったため真っ黒になっています。このレンダリング領域には赤い四角がありますが、この四角はレンダリング対象ではないので描画されません。
 Panel1を保存した場合もこれと同じことが起きます。そのため、以下の様に緑の四角が一部しか表示されません。
Panel1.jpg

 最後に、Panel2を保存した結果は以下の様になります。
Panel2.jpg
 レンダリングの基準点は指定した要素の1つ上のパネルの左上角になります。
 Penel2を指定した場合、基準となるパネルはPanel1となります。そのため、Panel1の左上角からPanel2のサイズの領域を画像として保存します。

 画像をイメージ通りに保存する場合は、対象となる要素の上に1つパネルを用意してあげるとうまくいきます。
 XAMLコードだけ見ると無駄なパネルに見えますが、画像保存する際の位置合わせのために必須です。  

テーマ : プログラミング
ジャンル : コンピュータ

VisualStateManagerで独自状態に遷移する

 前回は、VisualStateManagerでMouseOver等のあらかじめ用意されている状態の見た目をカスタマイズする方法を紹介しました。今回は、独自に作成した状態に遷移する方法を紹介します。
 以下の例では、StackPanelに「Red」と「White」という独自の状態を作成し、ボタンを押すことで「Red」に遷移したり、「White」に遷移させたりします。
<Window x:Class="VisualStateManagerDemo2.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        Title="MainWindow" Height="350" Width="525">

    <!--StackPanelでBackgroundを指定しないと実行時にエラーになる-->
    <StackPanel Name="LayoutRoot" Background="White">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="Original">
                <VisualState x:Name="White"/>
                <VisualState x:Name="Red">
                    <Storyboard>
                        <ColorAnimationUsingKeyFrames
                            Storyboard.TargetName="LayoutRoot"
                            Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
                            <EasingColorKeyFrame KeyTime="0"
                                                 Value="Red"/>
                        </ColorAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        
        <!--GoToStateActionで状態遷移する場合-->
        <Button Content="赤くする" Width="100">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <ei:GoToStateAction StateName="Red"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        
        <!--コードビハインドで状態遷移する場合-->
        <Button Content="戻す" Width="100" Click="Button_Click"/>
    </StackPanel>
</Window>
 今回、見た目を変更するのはStackPanelなので、StackPanel以下でVisualStateManagerの定義を行います。
 作成するのは独自の状態なので、VisualStateGroupの名前は「Original」としました。(どんな名前でもOKです。)
 あとは、VisualStateに状態の名前を付けて見た目の定義をするだけです。ここら辺は前回と同じなので説明は省略します。
 「Red」や「White」は独自の状態なので、状態が遷移するタイミングを指定する必要があります。
 VisualStateManagerの状態を遷移させる方法は2種類あり、1つがBlend SDKのGoToStateActionを使用する方法。もう1つが、コードビハインドでVisualStateManager.GoToElementStateメソッドを使用する方法です。
 このサンプルコードでは、状態「Red」への遷移はGoToStateActionを使用し、「White」への遷移はVisualStateManager.GoToElementStateを使用しています。
 GoToStateActionを使用する場合は、GoToStateActionのStateNameで遷移したい状態名を指定するだけです。
 VisualStateManager.GoToElementStateメソッドで状態遷移を行う場合は以下の様にします。
using System.Windows;

namespace VisualStateManagerDemo2.Views
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            VisualStateManager.GoToElementState(this.LayoutRoot, "White", false);
        }
    }
}
 GoToElementStateメソッドの第1引数にはVisualStateManagerを含んでいるルート要素を指定します。
 第2引数には遷移する状態名を指定します。
 第3引数にはVisualTransitionオブジェクトを使用して遷移するかどうかを指定します。今回は使っていないのでfalseです。
 これを実行し、ボタンを押すとStackPanelの背景色が以下の様に変わります。
VisualStateManagerDemo2.png

テーマ : プログラミング
ジャンル : コンピュータ

カレンダー
09 | 2014/10 | 11
- - - 1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31 -
全記事表示リンク

全ての記事を表示する

カテゴリ
タグリスト

月別アーカイブ
04  10  11  09  08  07  06  05  04  03  02  01  12  11  10  09  08  07  06  04  03  02  01  12  11  10  09  08  07  06  05  04  03  02  01  12  11  10  09 
最新記事
リンク
最新コメント
検索フォーム
Amazon