WPF で読みがなを取得(2)

前回、"YomiganaWPFTextBox" コントロールを用いた読みがな取得について、コード ビハインドで利用するパターンを書いたので、今回は MVVM パターンを利用して分離するものを書いてみます。

コード ビハインドで書くことになった理由は、次の2つになります。

  • YomiganaWPFTextBox コントロールは、TextCaptured プロパティを依存関係プロパティとして提供していない。
  • YomiganaWPFTextBox コントロールは、ユーザー入力による変換操作確定時の読みがな取得の契機を YomiganaChanged イベントで提供している。

このことから、Interaction.Triggers でイベントを補足し、添付プロパティで読みがなを通知するようにすることで、分離を行います(TextCaptured プロパティが依存関係プロパティとして提供されていれば、こんな苦労は不要なんですが。。。)。

動作テストのプログラムは、氏名を入力すると読みがなを表示し、「登録ボタン」をクリックすると下段に入力した内容を表示するものを作っています。

YomiganaTextBox2 の画面
YomiganaTextBox2 の画面

それでは、プログラムです。
「WPF アプリケーション」なプロジェクトを作成します(プロジェクト名は YomiganaTextBox2 としています)。

最初に、コントロールへの参照を追加します。Visual Studio のメニューの「プロジェクト」から「参照の追加」を選択します。参照マネージャーが表示されるので、「参照」ボタンをクリックして YomiganaWPFTextBox.dll を選択して「追加」ボタンをクリックします(インストーラーのデフォルトの格納先は "C:\Program Files\Microsoft Visual Studio International Feature Pack 2.0\YomiganaFramework" です)。
YomiganaWPFTextBox の使い方は、インストール フォルダにあるヘルプ ファイル("YomiganaFramework.chm")を参照してください。
また、トリガとアクションを利用するので System.Windows.Interactivity と Microsoft.Expression.Interactions への参照も追加します(Visual Studio 2013 Update 1 では Blend SDK を入れなくても入っているはず)。

次にビヘイビアを作成します。プロジェクトに「Behaviors」フォルダを追加します。
Behaviors フォルダに「YomiganaTextBoxBehaviors」クラスを作成します。

using System.Windows;
using Microsoft.International.Windows.Controls;

namespace YomiganaTextBox2.Behaviors
{
    public static class YomiganaTextBoxBehaviors
    {
        public static readonly DependencyProperty YomiganaProperty = DependencyProperty.RegisterAttached(
            "Yomigana", typeof(string), typeof(YomiganaTextBoxBehaviors),
            new FrameworkPropertyMetadata
            {
                DefaultValue = string.Empty,
                BindsTwoWayByDefault = true
            });

        public static string GetYomigana(DependencyObject obj)
        {
            return (string)obj.GetValue(YomiganaProperty);
        }
        public static void SetYomigana(DependencyObject obj, string value)
        {
            obj.SetValue(YomiganaProperty, value);
        }

        public static readonly DependencyProperty KickYomiganaProperty = DependencyProperty.RegisterAttached(
            "KickYomigana", typeof(bool), typeof(YomiganaTextBoxBehaviors),
            new FrameworkPropertyMetadata
            {
                DefaultValue = false,
                PropertyChangedCallback = KickYomiganaChanged,
                BindsTwoWayByDefault = true
            });

        public static bool GetKickYomigana(DependencyObject obj)
        {
            return (bool)obj.GetValue(KickYomiganaProperty);
        }
        public static void SetKickYomigana(DependencyObject obj, bool value)
        {
            obj.SetValue(KickYomiganaProperty, value);
        }

        private static void KickYomiganaChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var yomiganaTextBox = sender as YomiganaWPFTextBox;
            if (yomiganaTextBox == null) return;
            if (!(bool)e.NewValue) return;

            // 添付プロパティ Yomigana に YomiganaWPFTextBox.TextCaptured の値をセットする
            SetYomigana(yomiganaTextBox, yomiganaTextBox.TextCaptured);

            SetKickYomigana(yomiganaTextBox, false); // プロパティ値を false に戻す
        }
    }
}

bool 型の添付プロパティ KickYomigana は、true をセットされると、string 型の添付プロパティ Yomigana へ YomiganaWPFTextBox コントロールの TextCaptured プロパティの値をセットするようにしています(PropertyChangedCallback にセットした KickYomiganaChanged イベントハンドラで行っています)。
添付プロパティ KickYomigana への値の設定は XAML 側で行います。

次に入力内容の保存用のモデルです。
プロジェクトに「Models」フォルダを追加し、Models フォルダに「Sample」クラスを作成します。

namespace YomiganaTextBox2.Models
{
    class Sample
    {
        public string Name { get; set; }
        public string Yomigana { get; set; }
    }
}

次にビューモデルですが、MVVM パターンなので、お決まりのコマンド処理とプロパティチェンジイベントの発火を提供する ViewModelBase クラスは別記事の ViewModelBase で書いたものを使っています。別プロジェクトにして作成した DLL を参照設定で追加するなり、プロジェクト内にソースを取り入れるなりしてください(次のコードは DLL を参照する前提で書いています)。

プロジェクトに「ViewModels」フォルダを追加し、ViewModels フォルダに「MainViewModel」クラスを作成します。

using System;
using System.Collections.Generic;
using System.Windows.Input;

using MakCraft.ViewModels;

using YomiganaTextBox2.Models;

namespace YomiganaTextBox2.ViewModels
{
    class MainViewModel : ViewModelBase
    {
        private List<Sample> _samples = new List<Sample>();

        public MainViewModel() { }

        private bool _kickYomigana = false;
        public bool KickYomigana
        {
            get { return _kickYomigana; }
            set
            {
                _kickYomigana = value;
                base.RaisePropertyChanged(() => KickYomigana);
            }
        }

        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                _name = value;
                base.RaisePropertyChanged(() => Name);
            }
        }

        private string _yomigana;
        public string Yomigana
        {
            get { return _yomigana; }
            set
            {
                _yomigana = value;
                base.RaisePropertyChanged(() => Yomigana);
            }
        }

        private string _lists = string.Empty;
        public string Lists
        {
            get { return _lists; }
            set
            {
                _lists = value;
                base.RaisePropertyChanged(() => Lists);
            }
        }

        private void addNameExecute()
        {
            var sample = new Sample { Name = Name, Yomigana = Yomigana };
            _samples.Add(sample);

            Name = Yomigana = string.Empty;

            var temp = new string[_samples.Count];
            for (var i = 0; i < _samples.Count; ++i)
            {
                temp[i] = string.Format("Name: {0}, Yomigana: {1}", _samples[i].Name, _samples[i].Yomigana);
            }

            Lists = string.Join(Environment.NewLine, temp);
        }
        private bool addNameCanExecute(object param)
        {
            return !string.IsNullOrEmpty(Name) && !string.IsNullOrEmpty(Yomigana);
        }
        private ICommand _addNameCommand;
        public ICommand AddNameCommand
        {
            get
            {
                if (_addNameCommand == null)
                {
                    _addNameCommand = new RelayCommand(addNameExecute, addNameCanExecute);
                }
                return _addNameCommand;
            }
        }
    }
}

最後にビューです。

<Window x:Class="YomiganaTextBox2.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:b="clr-namespace:YomiganaTextBox2.Behaviors"
        xmlns:yomi="clr-namespace:Microsoft.International.Windows.Controls;assembly=YomiganaWPFTextBox"
        xmlns:vm="clr-namespace:YomiganaTextBox2.ViewModels"
        Name="Window"
        Title="MainWindow" Width="525" SizeToContent="Height">
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>
    
    <StackPanel>
        <TextBlock
            Text="Microsoft 製よみがな拡張 TextBox のテスト"
            FontSize="18" HorizontalAlignment="Center" Margin="8" />

        <StackPanel Orientation="Horizontal" Margin="8" HorizontalAlignment="Center">
            <Label Content="氏名" />
            <yomi:YomiganaWPFTextBox
                TextCapturedPreferredConversion="ToHiragana" MinWidth="150"
                Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
                b:YomiganaTextBoxBehaviors.Yomigana="{Binding ElementName=yomigana, Path=Text}"
                b:YomiganaTextBoxBehaviors.KickYomigana="{Binding KickYomigana}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="YomiganaChanged">
                        <ei:ChangePropertyAction
                            TargetObject="{Binding ElementName=Window, Path=DataContext}"
                            PropertyName="KickYomigana"
                            Value="True" />
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </yomi:YomiganaWPFTextBox>
        </StackPanel>

        <StackPanel Orientation="Horizontal" Margin="8" HorizontalAlignment="Center">
            <Label Content="読み" />
            <TextBox
                Name="yomigana" MinWidth="150"
                Text="{Binding Yomigana, UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
        
        <Button
            Content="登録" MinWidth="100" Margin="8" Padding="8 4" HorizontalAlignment="Right"
            Command="{Binding AddNameCommand}" />
        
        <TextBox
            MinWidth="300" IsReadOnly="True" MinHeight="40" HorizontalAlignment="Center"
            Text="{Binding Lists}" />
    </StackPanel>
</Window>

YomiganaWPFTextBox の設定で添付プロパティ YomiganaTextBoxBehaviors.Yomigana を読みがなの TextBox の Text プロパティへバインドし、取得した読みがなを表示するようにしています。また、添付プロパティ YomiganaTextBoxBehaviors.KickYomigana は、ビューモデルの KickYomigana プロパティへバインドしています(後述する ChangePropertyAction でプロパティを変更するため、ビューモデルを経由させています(添付プロパティの値を直接変更できなさそうなので :mrgreen: ))。
YomiganaWPFTextBox の YomiganaChanged イベントの補足は Interaction.Triggers の子要素 EventTrigger で行っていて、イベント発生時には ChangePropertyAction でビューモデルの KickYomigana プロパティに true をセットするようにしています(前述のとおり、 KickYomigana プロパティは添付プロパティ YomiganaTextBoxBehaviors.KickYomigana へバインドされています)。

YomiganaTextBoxBehaviors.Yomigana からバインドされている、読みがな用の TextBox の Text プロパティは、ビューモデルの Yomigana プロパティにバインドしています(これにより、「登録」ボタンがクリックされた時点の読みがなを下段に表示できます)。

次回は、UWP 用の API をデスクトップアプリケーションから利用する方法を使うことで Windows.Globalization 名前空間の JapanesePhoneticAnalyzer クラスの GetWords メソッドを利用して読み仮名を取得する方法です。


WPF で読みがなを取得(2)」への6件のフィードバック

  1. ピンバック: YomiganaWPFTextBox
  2. 継承して DependencyProperty を追加する、という手段もありますよ。

    C#:

    “`csharp
    using System.Windows;
    using Microsoft.International.Windows.Controls;

    namespace WpfApp
    {
    public class MyYomiganaWPFTextBox
    : YomiganaWPFTextBox
    {
    public MyYomiganaWPFTextBox()
    {
    YomiganaChanged += (sender, e) =>
    {
    Yomigana = TextCaptured;
    };
    }

    public static readonly DependencyProperty YomiganaProperty =
    DependencyProperty.RegisterAttached(
    “Yomigana”,
    typeof(string),
    typeof(MyYomiganaWPFTextBox),
    new FrameworkPropertyMetadata()
    {
    DefaultValue = null,
    BindsTwoWayByDefault = true
    });

    public string Yomigana
    {
    get { return (string)GetValue(YomiganaProperty); }
    set { SetValue(YomiganaProperty, value); }
    }
    }
    }
    “`

    XAML (一部):
    “`xaml
    …略…

    …略…
    “`

    1. おっと、XAMLのタグが消えてしまっていますね。張りなおします。
      XAML片:

      <local:MyYomiganaWPFTextBox
      TextCapturedPreferredConversion=”ToHiragana” MinWidth=”150″
      Text=”{Binding Name, UpdateSourceTrigger=PropertyChanged}”
      Yomigana=”{Binding ElementName=yomigana, Path=Text}”
      />

  3. あ~。。。たしかに継承して機能追加するという手もありますね(笑)

mak へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です