fc2ブログ

VisualStateManagerで状態管理

 VisualStateManagerを使用するとコントロールの見た目を状態ごとに管理することができます。
 状態はグループ(VisualStateGroup)分けされて管理されています。
 例えば、共通項目のCommonStatesには通常状態を表す"Normal"やマウスオーバー時の"MouseOver"、コントロールが押されたときの"Pressed"等があります。
 コントロール毎に使用できるグループが違うので、コントロールのスタイルとテンプレートで確認してください。
 例として、ボタンのコントロールテンプレートをVisualStateManagerを使用して作成します。全部の状態を定義すると恐ろしく長くなるのでMouseOverとDisabledのみ定義します。
<Window x:Class="VisualStateManagerDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="150" Width="225">
    <Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border Name="Border" BorderBrush="Gray" BorderThickness="1" Background="AliceBlue">
                <ContentPresenter Name="Content" Margin="2" RecognizesAccessKey="True"
                                  HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                  VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                <VisualStateManager.VisualStateGroups>
                    <VisualStateGroup x:Name="CommonStates">
                        <VisualState x:Name="Normal"/>
                        <VisualState x:Name="MouseOver">
                            <Storyboard>
                                <ColorAnimationUsingKeyFrames
                                    Storyboard.TargetName="Border"
                                    Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
                                    <EasingColorKeyFrame KeyTime="0"
                                                         Value="Pink"/>
                                </ColorAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                        <VisualState x:Name="Disabled">
                            <Storyboard>
                                <ColorAnimationUsingKeyFrames
                                    Storyboard.TargetName="Border"
                                    Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
                                    <EasingColorKeyFrame KeyTime="0"
                                                         Value="DarkGray"/>
                                </ColorAnimationUsingKeyFrames>
                                <ColorAnimationUsingKeyFrames
                                    Storyboard.TargetName="Content"
                                    Storyboard.TargetProperty="(TextBlock.Foreground).(SolidColorBrush.Color)">
                                    <EasingColorKeyFrame KeyTime="0"
                                                         Value="White"/>
                                </ColorAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                    </VisualStateGroup>
                </VisualStateManager.VisualStateGroups>
            </Border>
        </ControlTemplate>
    </Window.Resources>
    
    <StackPanel>
        <Button Content="ボタン1" Template="{StaticResource ButtonTemplate}" Width="100" Height="30"/>
        <Button Content="ボタン2" Template="{StaticResource ButtonTemplate}" Width="100" Height="30"/>
        <Button Content="ボタン3" IsEnabled="False" Template="{StaticResource ButtonTemplate}" Width="100" Height="30"/>
    </StackPanel>
</Window>
 これを実行すると以下の様になります。
VisualStateManagerDemo1.png 解りにくいですが「ボタン2」をマウスオーバーしています。
 VisualStateManager.VisualStateGroups以下に状態を定義するVisualStateGroupを作成します。x:Nameでグループ名を指定します。
 VisualStateGroup以下にVisualState(状態)を定義していきます。状態名はx:Nameで指定します。
 この例では、通常の状態を表すNormal、マウスオーバー時の状態を表すMouseOver、無効時の状態を表すDisabledを定義しています。
 Normalでは特にやることがないので中身は空のままにしてあります。
 MouseOver時にはボタンの背景色をPinkにしています。
 Disabled時はボタンの背景色をDardGrayに、文字色をWhiteにしています。
 状態を変化させる場合はStoryboardを使用して見た目の変更を行います。
 アニメーションで定義する必要があるので、どうしてもめんどくさいコードになりがちですが、頑張って覚えるしかありません。
 あらかじめ用意されている状態(CommonStatesのMouseOver等)を使用する場合、状態遷移を行う際のトリガーの定義は必要ありません。自動で状態遷移が行われます。
 VisualStateManagerを使用すると、あらかじめ用意されている状態を名前付きで定義できるので、トリガーを使用して見た目をコントロールする時よりもわかりやすくなります。

 VisualStateManagerは独自の状態を定義して使用することもできます。
 その説明は次回行います。
スポンサーサイト



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

WPFのStringFormatによるフォーマット指定

 TextBlock等に値をバインディングする際にStringFormatを使用して表示形式をカスタマイズすることができます。
 いくつか例を紹介します。
public class MainWindowViewModel : ViewModel
{
    public MainWindowViewModel()
    {
        Value = 1234.567;
        Price = 12345;
        Text = "12345";
        Name = "DVD Box";
        Date = DateTime.Today;
    }

    // プロパティの定義は省略
}
 ViewModelではバインディングするプロパティを用意しているだけです。
 プロパティの定義は無駄に長くなるので省略します。
<Window x:Class="StringFormatDemo.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:StringFormatDemo.Views"
        xmlns:vm="clr-namespace:StringFormatDemo.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <StackPanel>
        <GroupBox Header="数値の表示">
            <StackPanel>
                <TextBlock Text="{Binding Value, StringFormat={}{0:N2}}"/>
                <TextBlock Text="{Binding Value, StringFormat={}{0:N3}}"/>
                <TextBlock Text="{Binding Value, StringFormat={}{0:00000}}"/>
            </StackPanel>
        </GroupBox>
        
        <GroupBox Header="価格の表示">
            <StackPanel>
                <TextBlock Text="{Binding Price, StringFormat={}{0:C}}"/>
                <TextBlock Text="{Binding Price, StringFormat={}{0:C}, ConverterCulture=ja-JP}"/>
                <TextBlock Text="{Binding Price, StringFormat=定価{0:N0}円}"/>
                <TextBlock Text="{Binding Text, StringFormat={}{0:N0}円}"/>
            </StackPanel>
        </GroupBox>
        
        <GroupBox Header="日付の表示">
            <StackPanel>
                <TextBlock Text="{Binding Date}"/>
                <TextBlock Text="{Binding Date, ConverterCulture=ja-JP}"/>
                <TextBlock Text="{Binding Date, StringFormat={}{0:yyyy年MM月dd日 HH:mm:ss}}"/>
            </StackPanel>
        </GroupBox>
        
        <GroupBox Header="マルチバインディング">
            <TextBlock>
                <TextBlock.Text>
                    <MultiBinding StringFormat="商品名:{0} 価格:{1:N0}円">
                        <Binding Path="Name"/>
                        <Binding Path="Price"/>
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </GroupBox>
    </StackPanel>
</Window>
 これを実行すると以下の様になります。
StringFormatDemo.png
 表示形式(StringFormat)にNを指定すると数値として表示されます。(XAMLコードの18, 19行目)
 Nを指定した場合は数値はカンマ区切りで表示されます。
 Nの後の数値は小数点以下を何桁まで表示するかを表しています。
 数値の桁数を0で合わせたい場合は、StringFormatで桁数分の0を指定します。(20行目)

 価格を表示したい場合はStringFormatにCを指定します。(26, 27行目)
 ただし、表示形式をCとしただけだと価格がドル表示になってしまいます。日本円として表示したい場合はカルチャーの指定を行う必要があります。(27行目)
 StringFormatは値の表示形式をカスタマイズするだけでなく、値の前後にテキストを挿入することもできます。(28行目)
 数値フォーマット(N)や価格フォーマット(C)はバインディングする値が数値の場合に有効になります。バインディングしている値が文字列の場合は機能しません。(29行目)

 日付の場合はStringFormatを使わなくても日付表示になります。(35, 36行目)
 日本人になじみのある形式にするには価格と同じでカルチャーの指定が必要です。
 もちろんStringFormatを使用して表示をカスタマイズすることもできます。(37行目)
 この例では時刻まで表示させましたが年月日だけ表示とかもできます。

 最後に、マルチバインディングを利用して複数のプロパティとバインディングする方法です。(41~50行目)
 MultiBinding要素のStringFormatで表示方法を指定します。なぜかMultiBinding要素はインテリセンスの一覧に表示されないので入力する際にちょっと不安になりますが、問題はありません。
 プロパティとのバインディングはMultiBinding要素内でBinding要素を使用して行います。
 

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

RelativeSourceによるバインディング

 RelativeSourceを使用することで、自分自身や自分の上位要素とバインディングすることができます。
<Window x:Class="RelativeSourceBinding.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="150" Width="225">
    
    <StackPanel>
        <!--自分自身のプロパティを参照する-->
        <TextBlock Background="Red" Margin="5"
                   Text="{Binding RelativeSource={RelativeSource Mode=Self}, Path=Background}"/>
        
        <!--親(Border)のプロパティを参照する-->
        <Border BorderBrush="Blue" BorderThickness="2" Margin="5">
            <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=BorderBrush}"/>
        </Border>
        
        <!--AncestorLevelで親の位置を指定する-->
        <Border BorderBrush="Blue" BorderThickness="2" Margin="5">
            <Border BorderBrush="Red" BorderThickness="2">
                <TextBlock Text="テキスト"
                           Foreground="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}, AncestorLevel=2}, Path=BorderBrush}"/>
            </Border>
        </Border>
    </StackPanel>
</Window>
 これを実行すると、以下のようなウインドウが表示されます。
RelativeSource.png
 8~9行目のコードは、自分自身のプロパティとバインドする際の例です。
 自分自身にバインディングする場合はRelativeSourceのModeをSelfにします。そして、Pathでバインディングするプロパティを指定します。(ここは通常のバインディングと同じです)

 12~14行目、17~21行目のコードは、自分の上位要素とバインディングする際の例です。
 上位要素とバインディングする場合はAncestorTypeでバインディングする対象の型を指定します。この例の場合はBorderをターゲットにしています。
 さらにAncestorLevelでバインドする上位要素の位置を指定できます。AncestorLevelを指定しない場合は最初に見つかった上位要素とバインドします。(AncestorLevel=1と同じです)
 AncestorLevelは自分に近い方から1, 2,…とインデックスが付けられます。AncestorLevel=1だと最初に見つかった要素と、AncestorLevel=2だと2番目に見つかった要素とバインディングします。
 17~21行目のコードではAncestorLevel=2としているので、青枠(外側)のBorderとバインディングされています。  

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

XAML名前空間

 XAMLでBlend SDK等のライブラリや自分が作ったクラス等を使用したい場合は、XAML名前空間を宣言する必要があります。
 例えばLivetを使用してWindowを作成すると以下の様になります。
<Window x:Class="LivetWPFApplication1.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:LivetWPFApplication1.Views"
        xmlns:vm="clr-namespace:LivetWPFApplication1.ViewModels"
        Title="MainWindow" Height="350" Width="525">

</Window>
 名前空間の宣言は、
xmlns:プレフィックス="名前空間の識別子(URI)" となります。
 プレフィックスは他のプレフィックスと重ならなければ自由に指定できます。
 名前空間の識別子の入力はエディターがサポートしてくれるので難しくありません。
 この例の場合、7行目(プレフィックスv)は、このプロジェクト内で作成したLivetWPFApplication1.Views名前空間に定義されたクラスを使用するための宣言です。同様に8行目(プレフィックスvm)はLivetWPFApplication1.ViewModels名前空間のクラスを使用するためです。
 C#で作成したクラスを使用する場合はViewsやViewModelsの様に、使用したいクラスが存在するCRL名前空間をそれぞれ宣言する必要があります。
 これは別プロジェクトで自作ビヘイビア等を作成した場合も同様です。
 では、ビヘイビア等のライブラリを使用する際は、使用したい機能に合わせてXAML名前空間をいくつも宣言しなくてはならないのでしょうか?
 もちろん違います。
 上記のXAMLコードの6行目を見てください。この行はLivetの各種機能をXAMLで使用するための宣言です。
 XAML名前空間を1つ宣言するだけで、Livetの様々な機能が使用できるようになります。これは、複数のCLR名前空間に1つのXAML名前空間を割り当てているからです。
 例えば、アクションとそのアクションで使用するメッセージクラスを以下の様に作成したとします。 XAML名前空間
 ここには2つのCLR名前空間がありますが、これを1つのXAML名前空間に集約するには、AssemblyInfo.cs(Properties内にあります)に以下のコードを追記します。
using System.Windows.Markup;

[assembly: XmlnsDefinition("http://sample/", "ClassLibrary1.Messages")]
[assembly: XmlnsDefinition("http://sample/", "ClassLibrary1.Actions")]
 System.Windows.Markup.XmlnsDefinitionAttributeを使用して、XAML名前空間の割り当てができます。
 第1引数に割り当てたいXAML名前空間を指定し、第2引数にCLR名前空間を指定します。
 こんな感じで、複数のCLR名前空間を1つのXAML名前空間に集約できます。
 これをXAMLで使用すると以下の様になります。
<Window x:Class="LivetWPFApplication1.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:LivetWPFApplication1.Views"
        xmlns:vm="clr-namespace:LivetWPFApplication1.ViewModels"
        xmlns:s="http://sample/"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    
    <Grid>
        <Button Content="ボタン">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <s:XxxAction>
                        <l:DirectInteractionMessage>
                            <s:XxxMessage/>
                        </l:DirectInteractionMessage>
                    </s:XxxAction>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </Grid>
</Window>
 XAML用ライブラリを作成するときは名前空間の割り当てをしておくと非常に使いやすくなります。
 サンプルなのでかなり適当なURIにしましたが、もっとわかりやすい名前を付けることをお勧めします。
 わかりやすい名前にしておけば、XAMLエディターで名前空間を宣言する際に入力が楽になります。

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

ファイル名のソート

 System.IOのDirectoryクラス等を使用すると、指定したディレクトリ以下にあるファイル名やディレクトリ名を取得することができますが、その時得られるファイル名の順番は保障されていません。
 取得したファイル名を名前順に並べ替えたいと思うことは結構あることだと思います。この時、普通にソートを行うと自分が期待していた順番にならないことがあります。
 それはファイル名が文字列+数値で構成されている場合です。この場合、通常のソートでは数値も文字列として扱うので、数値順に並んでくれません。(file1, file2, file20, file3みたいな感じでfile2の次にfile20がきたりします)
 これを数値を考慮して並べ替える方法を紹介します。タイトルはファイル名のソートとなっていますが、ファイル名に限らず、文字列をソートする場合にも勿論使えます。

 まず、文字列を数値を考慮して並べ替える処理ですが、shlwapi.dllのStrCmpLogicalWを使用します。
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace FileNameSort
{
    public class LogicalComparer : IComparer<string>
    {
        // 並び順は昇順になる
        public int Compare(string x, string y)
        {
            return NativveMethods.StrCmpLogicalW(x ?? "", y ?? "");
        }

        internal static class NativveMethods
        {
            [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
            public static extern int StrCmpLogicalW(string psz1, string psz2);
        }
    }
}
 shlwapi.dllのStrCmpLogicalWを使用して、2つの文字列を比較しているだけです。
 StrCmpLogicalWに文字列を渡す時にx ?? ""としていますが、これは引数がnullの場合は空文字にしているだけです。
 この例では文字列は昇順で並べ替えられます。(並び順を降順にしたい場合は、StrCompLogicalWの結果に-1を掛けて反転させればOKです。)

 では、適当な文字列を並び替えてみましょう。以下のコードでは、通常のソートの場合とここで作成したLogicalComparerを使用した場合とを比較しています。
using System;
using System.Linq;

namespace FileNameSort
{
    class Program
    {
        static void Main(string[] args)
        {
            var fileNames = new[] { "file2", "file1", "file10", "file21", "file3" };

            Console.WriteLine("通常のソート");
            var nomalSorted = fileNames.OrderBy(x => x);
            foreach(var name in nomalSorted)
            {
                Console.WriteLine(name);
            }

            Console.WriteLine();
            Console.WriteLine("LogicalComparerを使用した場合");
            var fileNameSorted = fileNames.OrderBy(x => x, new LogicalComparer());
            foreach(var name in fileNameSorted)
            {
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}
 この結果は以下の様になります。
ソート結果

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

Rxでイベントを扱う

 RxではイベントもLINQとして扱えます。
 ただし、Rxでイベントを扱うためにはイベントをIObservable<T>に変換する必要があります。イベントをIObservable<T>に変換するたには、FromEventメソッドかFromEventPatternメソッドのどちらかを使用します。
 どちらを使用しても良いのですが、この2つのイベントは名前も使い方も似ていて非常に紛らわしいので、どちらか片方だけを覚えておけば良いと思います。
 私はFromEventメソッドの方で覚えているので、この記事ではFromEventメソッドを使用してイベントをIObservable<T>に変換します。
 これから紹介するコードでは、Modelクラスに作成したイベントをViewModelでObservable化して扱います。
using System;
using Livet;

namespace RxEvent.Models
{
    // TextChangedイベントのイベント変数
    public class TextChangedEventArgs : EventArgs
    {
        public TextChangedEventArgs(string text)
        {
            Text = text;
        }

        public string Text { get; private set; }
    }

    public class Item : NotificationObject
    {
        // 変更されたことだけを通知するイベント
        public event EventHandler Changed;

        // Textが変更された際に起きるイベント。
        public event EventHandler<TextChangedEventArgs> TextChanged;

        #region Text変更通知プロパティ
        private string _Text;

        public string Text
        {
            get
            { return _Text; }
            set
            { 
                if (_Text == value)
                    return;
                _Text = value;

                RaiseChanged();
                RaiseTextChanged(value);
                RaisePropertyChanged();
            }
        }
        #endregion

        // Changedイベントを発生させる。
        protected virtual void RaiseChanged()
        {
            var h = this.Changed;
            if(h != null)
            {
                h(this, new EventArgs());
            }
        }

        // TextChangedイベントを発生させる。
        protected virtual void RaiseTextChanged(string text)
        {
            var h = this.TextChanged;
            if(h != null)
            {
                h(this, new TextChangedEventArgs(text));
            }
        }
    }
}
 Modelでは適当なプロパティを作成し、その値が変更された際に各種イベントを発生させているだけです。これらのイベントをViewModelでObservable化して、Rxで使用します。
 イベントのObservable化を1回しかやらないのであれば、使用するときにObservable化すれば良いのですが、複数個所で使用する可能性があるのであれば拡張メソッドとして定義することをお勧めします。
using System;
using System.Reactive.Linq;
using System.ComponentModel;

namespace RxEvent.Models
{
    public static class ItemExtensions
    {
        // Changed(EventHandler)イベントのObservable化する
        public static IObservable<EventArgs> ChangedAsObservable(this Item source)
        {
            return Observable.FromEvent<EventHandler, EventArgs>(
                h => (sender, e) => h(e),
                h => source.Changed += h,
                h => source.Changed -= h);
        }

        // TextChanged(EventHandler<T>)イベントのObservable化する
        public static IObservable<TextChangedEventArgs> TextChangedAsObservable(this Item source)
        {
            return Observable.FromEvent<EventHandler<TextChangedEventArgs>, TextChangedEventArgs>(
                h => (sender, e) => h(e),
                h => source.TextChanged += h,
                h => source.TextChanged -= h);
        }

        // WPFでよく使うPropertyChangedをObservable化する
        public static IObservable<PropertyChangedEventArgs> PropertyChangedAsObservable(this INotifyPropertyChanged source)
        {
            return Observable.FromEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>(
                h => (sender, e) => h(e),
                h => source.PropertyChanged += h,
                h => source.PropertyChanged -= h);
        }
    }
}
 ItemクラスのChangedイベント(EventHandler)とTextChangedイベント(EventHandler<T>)のObservable化とPropertyChangedイベントのObservable化を行っています。
 FromEventメソッドでObservable化する際には、FromEventメソッドにイベントハンドラーの型とイベント変数の型を指定する必要があります。
 FromEventメソッドの第1引数で変換を行い、第2、第3引数で目的のイベントへの追加と削除を行っています。
 見ての通り、イベントハンドラーやイベント変数の型が違っても、FromEventメソッドの引数は同じような形になっています。なので、この使い方を1つ覚えるだけで様々なイベントをObservable化することができます。

 次にViewModel側でこれらのイベントをRxで利用します。
using System;
using Livet;
using Livet.EventListeners;
using System.Reactive.Linq;
using RxEvent.Models;

namespace RxEvent.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        private Item _Item;

        public  MainWindowViewModel()
        {
            ChangedCount = 0;
            TextChangedCount = 0;
            PropertyChangedCount = 0;
            LatestText = "";

            _Item = new Item();

            // _ItemのイベントをObservable化してRxで使用する
            var changed = _Item.ChangedAsObservable()
                .Subscribe(_ => ChangedCount++);

            var textChanged = _Item.TextChangedAsObservable()
                .Subscribe(e =>
                {
                    TextChangedCount++;
                    LatestText = e.Text;
                });

            var propertyChanged = _Item.PropertyChangedAsObservable()
                .Where(e => e.PropertyName == "Text")   // Textプロパティについてのみ
                .Subscribe(_ => PropertyChangedCount++);

            CompositeDisposable.Add(new PropertyChangedEventListener(_Item)
            {
                (sender, e) => RaisePropertyChanged(e.PropertyName)
            });
            CompositeDisposable.Add(changed);
            CompositeDisposable.Add(textChanged);
            CompositeDisposable.Add(propertyChanged);
        }

        public string Text
        {
            get { return _Item.Text; }
            set { _Item.Text = value; }
        }

        #region ChangedCount変更通知プロパティ
        private int _ChangedCount;
        // Changedイベント発生回数
        public int ChangedCount
        {
            get
            { return _ChangedCount; }
            private set
            { 
                if (_ChangedCount == value)
                    return;
                _ChangedCount = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region TextChangedCount変更通知プロパティ
        private int _TextChangedCount;
        // TextChangedイベント発生回数
        public int TextChangedCount
        {
            get
            { return _TextChangedCount; }
            private set
            { 
                if (_TextChangedCount == value)
                    return;
                _TextChangedCount = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region LatestText変更通知プロパティ
        private string _LatestText;
        /// <summary>
        /// TextChangedから取得する最新Text
        /// </summary>
        public string LatestText
        {
            get
            { return _LatestText; }
            private set
            { 
                if (_LatestText == value)
                    return;
                _LatestText = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region PropertyChangedCount変更通知プロパティ
        private int _PropertyChangedCount;
        // ItemのPropertyChangedイベント発生回数
        public int PropertyChangedCount
        {
            get
            { return _PropertyChangedCount; }
            private set
            { 
                if (_PropertyChangedCount == value)
                    return;
                _PropertyChangedCount = value;
                RaisePropertyChanged();
            }
        }
        #endregion
    }
}
 ModelのイベントをRx化して利用している部分は23~35行目の部分です。拡張メソッドXxxAsObservableを使用して、目的のイベントをObservable化します。あとはRxの使い方と同じで、フィルタリング等の処理を行い最後にSubscribeメソッド使用します。
 Subscribeメソッドの戻り値はIDisposableインスタンスです。このインスタンスのDisposeメソッドを呼ぶことで、イベントの監視を停止させることができます。(ここでは、Livetの機能を使ってViewModelがDisposeされた際に一緒にDisposeさせています。)

 最後にViewを作ります。
<Window x:Class="RxEvent.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:RxEvent.Views"
        xmlns:vm="clr-namespace:RxEvent.ViewModels"
        Title="MainWindow" Height="120" Width="325">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <TextBox Grid.Row="0" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}"/>
        
        <TextBlock Grid.Row="1" Text="{Binding ChangedCount, StringFormat=Changed回数:{0}}"/>
        <TextBlock Grid.Row="2" Text="{Binding TextChangedCount, StringFormat=TextChanged回数:{0}}"/>
        <TextBlock Grid.Row="3" Text="{Binding LatestText, StringFormat=最新Text:{0}}" Margin="10,0,0,0"/>
        <TextBlock Grid.Row="4" Text="{Binding PropertyChangedCount, StringFormat=PropertyChanged回数:{0}}"/>
    </Grid>
</Window>
 テキストボックスの値が変更される度に、各イベントのカウントが増加します。
RxEvent.png  

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

Rxを使ってクリップボードの監視を行う

 C#にはクリップボードの操作を行う為のClipboardクラスがありますが、残念ながらこのクラスにはクリップボードの変更を通知する機能はありません。
 そこで、Rx(Reactive Extensions)を使用してクリップボードの監視を行ってみます。
using System;
using System.Windows;
using System.Reactive.Linq;
using System.Reactive.Concurrency;

using Livet;

namespace ClipboardWatcher.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        public MainWindowViewModel()
        {
            // 起動時にClipboardの値をクリアする
            Clipboard.Clear();

            var watcher = Observable
                .Timer(TimeSpan.FromMilliseconds(200))  // 200ms毎に               
                .Where(_ => Clipboard.ContainsText())   // クリップボードにテキストデータがあるかを確認
                .ObserveOnDispatcher()                  // 操作スレッドをUIスレッドに変更(これを忘れると例外発生)
                .Select(_ => Clipboard.GetText())       // クリップボードのテキストを取得               
                .ObserveOn(Scheduler.Default)           // 操作スレッドを規定スレッドに変更
                .Repeat()                               // 上記の監視を何度も繰り返す     
                .DistinctUntilChanged()                 // 以前の結果と違う場合のみ
                .Subscribe(x => ClipboardText = x);
            
            // Livetを使用するならCompositeDisposableに追加しておけば
            // ViewModelがDisposeされる際にクリップボードの監視も停止されます。
            // Livetを使用しないのであれば、しかるべき場所でwatcher.Dispose();を実行してください。
            CompositeDisposable.Add(watcher);
        }

        #region ClipboardText変更通知プロパティ
        private string _ClipboardText;
        /// <summary>
        /// クリップボードにコピーされたテキスト
        /// </summary>
        public string ClipboardText
        {
            get
            { return _ClipboardText; }
            set
            { 
                if (_ClipboardText == value)
                    return;
                _ClipboardText = value;
                RaisePropertyChanged();
            }
        }
        #endregion
    }
}
 大体コメントに書いてあるので、特に説明は必要ないと思いますが、要点のみ説明します。
 Clipboard.GetText()メソッドはSTAモードじゃないと例外が発生します。そのため、GetTextメソッドを呼ぶ前に処理スレッドをUIスレッドに切り替えています。
 その後、UIスレッドの負荷を下げるため再度別スレッドに処理を移し、DistinctUntilChanged() メソッドを使用することで前回の値と違う場合のみ値を通しています。
 Repeat()メソッドは、この一連の監視処理を何度も続けるために必須です。これを忘れるとうまく動きません。
 Subscribe()メソッドの戻り値はIDisposableインスタンスです。このインスタンスのDispose()メソッドを呼ぶと、クリップボードの監視は停止します。
 Livetを使用しているので、CompositeDisposableにwatcherを追加していますが、Livetを使用しない場合は、しかるべき場所でwatcher.Dispose()を実行し監視を停止させてください。
   あとはView側で適当にバインドするだけです。
    <Grid>
        <TextBlock Text="{Binding ClipboardText}" />
    </Grid>
 これで、コピー(Ctrl + C)した瞬間に、Viewにコピーしたテキストが表示されます。

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

asyncとIProgressを使ってプログレスバーを操作する

 今現在、C#で非同期処理をするのであればasync awaitを使用することが多いと思います。(読み書きしやすいですし)
 また、.NET 4.5からIProgress<T>が実装され、状況通知を行う際のインターフェイスもできました。
 これらを使って処理状況をプログレスバーで通知してみます。ついでに、キャンセル処理も組み込んでみました。
 (以下のサンプルコードにはLivet ver1.2を使用しています。)
 まずは、モデルに重たい処理を行うメソッドを作成します。
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using Livet;

namespace AsyncProgress.Models
{
    // 通知内容
    public class ProgressInfo
    {
        public ProgressInfo(int value, string message)
        {
            Value = value;
            Message = message;
        }

        public int Value { get; private set; }
        public string Message { get; private set; }
    }

    public class Model : NotificationObject
    {
        private CancellationTokenSource _CancellationTokenSource;

        // 時間がかかる処理
        public async Task HeavyWork(IProgress<ProgressInfo> progress)
        {
            using(_CancellationTokenSource = new CancellationTokenSource())
            {
                try
                {
                    await Task.Run(() =>
                    {
                        foreach (var v in Enumerable.Range(1, 100))
                        {
                            // キャンセル処理
                            _CancellationTokenSource.Token.ThrowIfCancellationRequested();

                            // 重たい処理
                            Task.Delay(100).Wait();

                            // 状況通知
                            progress.Report(new ProgressInfo(v, string.Format("処理中{0}/{1}", v, 100)));
                        }
                    }, _CancellationTokenSource.Token);

                    // 作業終了後の処理
                    progress.Report(new ProgressInfo(0, "作業終了"));
                }
                catch(OperationCanceledException)
                {
                    // キャンセルされた場合
                    progress.Report(new ProgressInfo(0, "キャンセルされました"));
                    return ;
                }
            }   
        }

        // 処理をキャンセルする
        public void Cancel()
        {
            _CancellationTokenSource.Cancel();
        }
    }
}
 ProgressInfoは処理状況を伝えるための入れ物です。プログレスバー等で状況を通知する際に必要な情報を持たせます。
 ModelクラスのHeavyWorkメソッドが重たい処理を行うメソッドです。このメソッドの役割は、1)非同期で重たい処理を行う。2)処理状況を通知する。3)キャンセルされた場合は処理を中止する。の3つです。
 非同期処理はTaskを使用して行います。単純に、重たい処理のみを行うのであればasync awaitは必要ありません。(Taskを返せばOKです。)
 ですが、今回は作業が終わった後に終了処理(作業が終了したことを通知したい)を行いたいので、重たい処理(Task)をawaitしています。こうすることで、awaitが付いたTaskの処理が開始された時点で、一旦メソッドから抜けます。その後、Taskの処理が終了したら、続きの処理(この例の場合は、作業終了の通知を行う)が実行されます。
 非同期処理を同期処理と同じような感覚でコードを書けるので読み書きしやすいコードになります。
 async awaitを使用する場合は、基本的にTask又はTask<T>を返すようにしましょう。async voidにしてしまうと処理を待機することができなくなります。(投げっぱなしになります。イベントならそれでも良いのですが、普段使う場合はいつ終わるのかがわからなくなるのでやめたほうがよいです。)

 状況通知ですが、IProgress<T>は状況通知を行うためのReportメソッドを持っています。このメソッドに現在の処理状況を格納したインスタンスを渡すだけでOKです。
 ViewModel側でこの情報を受け取って、プログレスバー等で表示させます。

 キャンセル処理は、Taskによるキャンセルと同様にCancellationTokenSourceを使用します。
 _CancellationTokenSource.Cancel()メソッドが呼ばれた場合、_CancellationTokenSource.Token.ThrowIfCancellationRequested();は例外(OperationCanceledException)を返すため、処理が中止されます。あとは、try-catchでOperationCanceledExceptionをキャッチして例外処理を行えばOKです。

 次に、ViewModelを作成します。
using System;
using Livet;
using AsyncProgress.Models;

namespace AsyncProgress.ViewModels
{
    public class MainWindowViewModel : ViewModel
    {
        private Model _Model;
        private Progress<ProgressInfo> _Progress;

        public MainWindowViewModel()
        {
            _Model = new Model();

            // 状況通知処理を定義
            _Progress = new Progress<ProgressInfo>(e =>
            {
                ProgressValue = e.Value;
                Message = e.Message;
            });

            ProgressValue = 0;
            CanHeavyWork = true;
            CanCancel = false;
        }

        #region ProgressValue変更通知プロパティ
        private double _ProgressValue;

        // 処理状況を示す数値
        public double ProgressValue
        {
            get
            { return _ProgressValue; }
            set
            { 
                if (_ProgressValue == value)
                    return;
                _ProgressValue = value;
                RaisePropertyChanged();
            }
        }
        #endregion


        #region Message変更通知プロパティ
        private string _Message;

        // 状況を示すメッセージ
        public string Message
        {
            get
            { return _Message; }
            private set
            { 
                if (_Message == value)
                    return;
                _Message = value;
                RaisePropertyChanged();
            }
        }
        #endregion


        #region CanHeavyWork変更通知プロパティ
        private bool _CanHeavyWork;

        // 処理が可能かどうか
        public bool CanHeavyWork
        {
            get
            { return _CanHeavyWork; }
            private set
            { 
                if (_CanHeavyWork == value)
                    return;
                _CanHeavyWork = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        #region CanCancel変更通知プロパティ
        private bool _CanCancel;

        // キャンセル可能かどうか
        public bool CanCancel
        {
            get
            { return _CanCancel; }
            private set
            { 
                if (_CanCancel == value)
                    return;
                _CanCancel = value;
                RaisePropertyChanged();
            }
        }
        #endregion

        // 重い処理開始
        public async void RunHeavyWork()
        {
            CanHeavyWork = false;
            CanCancel = true;

            await _Model.HeavyWork(_Progress);

            CanHeavyWork = true;
            CanCancel = false;
        }

        // 処理をキャンセルする
        public void Cancel()
        {
            CanCancel = false;
            _Model.Cancel(); 
        }
    }
}
 ViewModelでは状況通知用の変数や処理を行うためのメソッド等を作ります。
 ここで、通知された処理状況を受け取ってどう処理をするかを定義します。
 Progress<T>に、Reportメソッドから通知があった場合の処理を指定します。ここでは、ProgressValueとMessageの値を更新し、Viewの表示を更新させています。
 RunHeavyWorkメソッドは処理開始ボタンにバインドさせるメソッドです。処理中に再度RunHeavyWorkメソッドが実行されないように、また、処理中のみキャンセルボタンが押せるようにするために、CanHeavyWork, CanCancelの値を制御しています。
 RunHeavyWorkがasync voidになっているのは、メソッドバインディングする場合は戻り値がvoidの必要があるためです。(実質、このメソッドはクリックイベントと同じと見なせますので、async voidでも問題ありません。)

 最後にViewを作ります。
<Window x:Class="AsyncProgress.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:AsyncProgress.Views"
        xmlns:vm="clr-namespace:AsyncProgress.ViewModels"
        Title="MainWindow" Height="80" Width="225">
    
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    
    <i:Interaction.Triggers>
        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        
        <ProgressBar Grid.Row="0" Minimum="0" Maximum="100" Value="{Binding ProgressValue}"/>
        
        <StackPanel Grid.Row="1" HorizontalAlignment="Left" Orientation="Horizontal">
            <Button Content="処理開始" IsEnabled="{Binding CanHeavyWork}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="RunHeavyWork"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
            <Button Content="キャンセル" IsEnabled="{Binding CanCancel}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Cancel"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>
            <TextBlock Text="{Binding Message}"/>
        </StackPanel>
    </Grid>
</Window>
 これを実行すると、以下の様になります。
処理中 処理中
処理終了 処理が終了した場合
キャンセルした場合 キャンセルした場合

ASeekerの紹介

 コミックスの新刊やお気に入りのアーティストの新譜等の発売日を一々チェックするのが面倒だったので作ったのがこれから紹介するASeekerです。
 このソフトは、登録した検索条件をアマゾンで検索し、見つかった商品を一覧表示します。
 新刊・新譜のチェックや、定期的に購入する消耗品の購入を目的にしています。
 逆に、パソコンのパーツ等の様な商品を検索するのには向いていません。
 登録した検索条件は、ソフトウェア起動時にスケジュールに従って検索が行われます。
ASeekerメイン
 起動する度に全項目を検索させることも可能ですが、API規約により1秒につきリクエストは1回までと決まっているので、非常に時間がかかります。(例えば、10件の検索を行う場合は、1件につき1リクエストだった場合でも10秒かかります。)
 そのため、検索スケジュールを設定し、検索するタイミングをずらすことで分散させています。
 検索により見つかった商品は、「商品情報」タブに表示されます。
ASeeker商品情報
 上部のリストには見つかった商品情報が表示されます。
 下部には、選択した商品の詳細情報が表示されます。
 購入可能な商品の場合は「カートに追加」ボタンを押すことで、カートに追加できます。
 「カート」タブで「購入する」ボタンを押すと、使用しているウェブブラウザで購入ページが開きます。
ASeekerカート
 その後の購入手続きはウェブブラウザで行ってください。
 ASeekerでは購入処理は行いません。これは、こういったお金が絡むソフトにアカウントIDとパスワードを登録するのを嫌がる人もいると思ったからです。(私もその一人です)

 Kindleの商品検索も行えますが、Kindle等のデジタルコンテンツは通常のカート操作で購入できません。
 そのため、商品が見つかっても常に「購入不可」の状態になります(価格も常に0円表示です)
 デジタルコンテンツを購入する場合は、「詳細ページへ」ボタンを押し、ウェブブラウザでアマゾンの商品詳細ページを開いて購入してください。また、購入後は商品情報の「購入済み」にチェックを入れてください。
 カートから購入した場合は自動で「購入済み」にチェックが入りますが、詳細ページから購入した場合はチェックが入りません。

 商品情報欄にある「定期購入する」にチェックを付け、更新期間を設定すると、定期的に商品リストにその商品が表示されます。
 電動歯ブラシの替えブラシや空気清浄機等のフィルターなど、定期的に購入する消耗品の購入に便利です。

 ASeekerのダウンロードは右フレームの「リンク」にある「ASeekerダウンロード(Vector)」から行えます。
 

Web Novel Bookshelfの紹介

 小説投稿サイトはいろいろありますが、そのサービス内容は様々です。中にはお気に入り小説の管理ができるサイトもありますが、すべてのサイトで行えるわけではありません。
 掲示板タイプの投稿サイトの場合はこういった小説管理機能がない場合も多いですし、更新日時で並べ替えが行われたりするので、目的の小説をみつけるのも面倒だったりします。
 何よりも、実際の本と違って栞を挟めないのがとても不便です。
 読んでいる小説の数が増えると栞機能なしでどこまで読んだかを管理するのはかなり面倒になります。
 そういった煩わしさを解消するために作成したのが、Web Novel Bookshelfです。
 Web Novel Bookshelfは、Webに掲載されている小説情報の管理と閲覧を目的にしたソフトウェアです。
 このソフトは、登録した小説にタグや評価などの情報を付加することができます。
 また、小説毎に栞を挟むこともできます。

WNBメイン
 メイン画面では、登録した小説が一覧表示されます。左側のパネルを操作することで、リストから小説を検索したり、特定の条件を満たす小説のみを抽出したりできます。
 よく使うフィルター条件は、設定を保存し、簡単にフィルターを切り替えることも可能です。

 小説の保存フォームは以下の様になっています。
WNB小説登録
 タイトルとURLは必須項目です。
 タイトルとURLには入力サポート機能がついています。 タイトルまたはURLを範囲選択してコピー(Ctrl + C)した際に自動で入力されます。(ペーストする必要はありません)。
 コピーした文字列がhttpで始まっていればURL、それ以外ならタイトルと判断して自動入力されます。ただし、タイトル欄、URL欄に文字列が入力されている場合は、自動入力されません。
 登録するURLは目次ページがある場合はそのページを、掲示板形式の場合は最初のページを登録します。
 残りの項目はオプションです。
 フィルター機能を使うために、できるだけ情報を付加することを推奨します。

 メイン画面のリストに表示される情報の小説タイトルが表示されているボタンを押すと、登録したURLのページが表示されます。その下にある「栞頁へ」ボタンを押した場合は、栞を挟んだページが表示されます。
WNB閲覧ページ
 閲覧ページは上部にツールバー、下部にブラウザ(IEコンポーネントです)が表示されます。普通のブラウザと同じような感じで小説を読み進めることができます。
 栞を挟みたい場合は、ツールバーにある「栞を挟む」ボタンを押してください。

 こんな感じで小説情報の管理と閲覧ができます。
 Web小説を読んでいて栞などの管理に困っている方は一度使ってみてください。
 ソフトウェアのダウンロードは、右フレーム内にある「リンク」の「Web Novel Bookshelfダウンロード(Vector)」から行えます。






 
カレンダー
08 | 2014/09 | 10
- 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 - - - -
全記事表示リンク

全ての記事を表示する

カテゴリ
タグリスト

月別アーカイブ
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