C++/CLIで覚えておくべきこと

C#からCの関数をコールする場合、おもに2通りの方法があります。

  • CをDLLとして公開し、C#からP/Invokeを使う
  • CをDLLorLibとして公開し、C++/CLIで.NETラッパを作成しC#から.NET呼出し

後者の場合、C++/CLIについていくつか知っておくべきことがありますので簡単にご紹介。

 

1.デストラクタとファイナライザ

C++/CLIの・・・はC#の・・・に対応しており、、などとややこしい記述がたくさんあるのでここでは要点だけをまとめておきます。。C++/CLIのデストラクタ・ファイナライザについて覚えておくべきことは以下3点。

  • C++/CLIのデストラクタ、ファイナライザどちらもケースによって呼ばれないことがある
  • そのため、アンマネージリソースを扱うクラスを定義する場合、デストラクタからファイナライザをコールし、ファイナライザ内でアンマネージリソースを解放する
  • またアンマネージリソースを扱うクラスを使う場合、クラスを明示的にdeleteする。(C#からそのクラスを使う場合disposeする)

以上。C#からC++/CLIを参照することはあっても逆参照はないと思いますので上記内容があれば十分かと。上記2点目を実際に定義する場合、以下のようになります。

PatternSearch.cpp

#include "stdafx.h"
#include "PatternSearch.h"

namespace OpenCvWrap{

	// -----------------------------------
	// --  public func implementation   --
	// -----------------------------------
	PatternSearch::PatternSearch(System::Drawing::Bitmap^ srcImage)
	{
		// IplImage*
		cvTemplate = Converter::CreateIplImage(srcImage);
	}
	// デストラクタ
	PatternSearch::~PatternSearch()
	{
		this->!PatternSearch();
	}
	// ファイナライザ
	PatternSearch::!PatternSearch()
	{
		if (cvTemplate != NULL){
			pin_ptr<IplImage*> p = &cvTemplate;
			cvReleaseImage(p);
			cvTemplate = NULL;
		}
	}
}

補足ですが上のクラス内cvTemplateはOpenCVのIplImageへのポインタです。このようにC++/CLIにポインタをメンバ変数として持った場合、そのアドレスを&を使って取得しようとすると

‘cli::interior_ptr<Type>’から’IplImage **’に変換できません。

というエラーが出ます。GCによってアドレスが変わることを防いでいるためですね。そのため上のようにpin_ptrを使ってアドレスを固定して呼び出しをしてやる必要があります。

 

2.デザイナが出力するファイル

C++/CLIでクラスを新規作成したときにできる.hファイルはこんな感じです。

Converter.h

#pragma once
ref class Converter
{
public:
	Converter();
};

せめて.NETのクラス定義なんだから名前空間とクラスのアクセス修飾子は明示しましょう。

#pragma once
namespace OpenCvWrap{

	public ref class Converter
	{
	public:
		Converter();
	};
}

あとは.hファイルにはむやみにusing namespace System::Drawing;など書かないこと。C++/CLIのクラス間では.hファイルのインクルードなので名前がかぶってしまう可能性があります。

 

3.Nativeな構造体は持てない

C++/CLIのクラスにCで定義した構造体をメンバーに持つことはできません。保持できるのはWORD, SHORTなどのプリミティブ型と構造体へのポインタだけです。コンストラクタでnewしてやるなど適宜フリーストアからメモリを確保しましょう。

 

4.IntPtrとBYTE*の変換例

例えばSystem::Drawing::BitmapクラスとOpenCVのIplImageとの変換処理を作るとき、24BitのフルカラーBmpからモノクロ8bitへは以下のようなstatic関数で変換が可能です。

 Converter.cpp

namespace OpenCvWrap{

	IplImage* Converter::BitmapToIplImage(System::Drawing::Bitmap^ src){
		IplImage* dst = cvCreateImage(cvSize(src->Width, src->Height), IPL_DEPTH_8U, 1);
		int dstStride = dst->widthStep;
		System::Drawing::Rectangle^ srcRect = gcnew System::Drawing::Rectangle(Point::Empty, src->Size);
		BitmapData^ bmpData = src->LockBits(*srcRect,
			ImageLockMode::ReadWrite, PixelFormat::Format24bppRgb);
		void* pvSrc = (void*)bmpData->Scan0;
		BYTE* pbySrc = static_cast<BYTE*>(pvSrc);
		int srcStride = bmpData->Stride;

		for (int y = 0; y < dst->height; y++){
			for (int x = 0; x < dst->Width; x++){
				dst->imageData[y * dstStride + x] =
					(BYTE)(
						( 118 * pbySrc[y * srcStride + 3 * x + 0] +
						  600 * pbySrc[y * srcStride + 3 * x + 1] +
						  306 * pbySrc[y * srcStride + 3 * x + 2] ) / 1024
					);
			}
		}
		src->UnlockBits(bmpData);
		return dst;
	}
}

ここでbmpData->Scan0を一旦void*で受けて、その後static_cast<BYTE*>としていますがこれはIntPtrからBYTE*への直接変換ができないためです。これをしようとすると、

‘static_cst’ : ‘System::IntPtr’ から ‘BYTE *’に変換できません。

と言われてしまいます。

だいたいこれくらい知ってれば困らないんじゃないかなーと。

WCF再訪 – 構成ファイルによるサービス公開

ブログを書き始めた半年前、WCFによるプロセス間通信を紹介しました。その時は全てをC#のコードで紹介しましたがWCF再訪と題してサービスを構成ファイルを使った場合をご紹介します。今回、クライアント側は変更しませんので以前の投稿を参照ください。

0.コントラクト

ITestContract.cs

namespace TestContract
{
    using System;
    using System.ServiceModel;
    [ServiceContract]
    public interface ITestContract
    {
        [OperationContract]
        DateTime GetTime();
    }
}

ご覧の通り、時刻取得ができるサービスです。

 

1.サービスをC#コードで記述する

対比のために前回の内容を再度ご紹介。

TestServer.cs

namespace TestServer
{
    using System;
    using System.ServiceModel;
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class TestServer : TestContract.ITestContract
    {
        public DateTime GetTime()
        {
            return DateTime.Now;
        }
    }
}

これをC#コードでバインディングやアドレスを指定してホストするには以下のようにします。

 Program.cs

namespace TestServer
{
    using System;
    using System.ServiceModel;

    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(new TestServer(),
                new Uri("net.pipe://localhost"));
            host.AddServiceEndpoint(
                typeof(TestContract.ITestContract),
                new NetNamedPipeBinding(),
                "InterProcessCommunication");
            host.Open();
            Console.WriteLine("Enter押下で終了.");
            Console.ReadLine();
        }
    }
}

 

2.サービスを構成ファイルに記述する

コントラクトを実現する部分は同じです。

TestServer.cs

namespace TestServerByConfigFile
{
    using System;
    using System.ServiceModel;
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class TestServer : TestContract.ITestContract
    {
        public DateTime GetTime()
        {
            return DateTime.Now;
        }
    }
}

構成ファイルapp.configは以下のようにします。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
  <system.serviceModel>
    <services>
      <service name="TestServerByConfigFile.TestServer">
        <host>
          <baseAddresses>
            <add baseAddress="net.pipe://localhost"/>
          </baseAddresses>
        </host>
        <endpoint address="InterProcessCommunication"
                  binding="netNamedPipeBinding"
                  contract="TestContract.ITestContract"/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

ABCを構成ファイルに移動した分、これをホストするコードはシンプルになります。

Program.cs 

namespace TestServerByConfigFile
{
    using System;
    using System.ServiceModel;

    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(typeof(TestServer));
            host.Open();
            Console.WriteLine("Enter押下で終了.");
            Console.ReadLine();
        }
    }
}

どちらでも書けるようになっておきたいですね。

ReactiveUI WinFormsをかじる

WPFと言えばMVVMですが実際の現場でWPFの浸透度はどうでしょうか?WinXPのサポートが切れたことで 『.NET3.0のインストールは30分も掛かってユーザに迷惑だから.NET2.0で開発する』 などと古臭い主張をする保守派への反論材料が揃いだした昨今ではまだ、WindowsFormsで新規開発しているほうがまだ多いと感じます。

Windows Formsで『MVC(おそらく正確にはMVP)で開発する』と意気込んだプロジェクトの何%が目標通りの設計、メンテナンス性が実現できているのか疑問です。Frameworkサポートとしては弱いWindows FormsでMVVMを実現させるライブラリの紹介です。

 

ReactiveUI WinForms

まずWindowsFormsアプリのプロジェクトを作成した状態で、Nugetパッケージマネージャで

PM> install-package reactiveui-winforms

としてReactiveUI WinFormsをインストールしておきます。VS2010ではダメだったのですが、VS2013ではokでした、VS2012以降などの制限があるかもしれません。

サンプルFormは以下の構成とします。

form

ReactiveUI-WinFormsではFormをただのViewとして扱い、ViewModelは以下のように別クラスに定義します。

 SampleViewModel.cs

namespace ReactiveUiForWinFormsTest
{
    using System;
    using ReactiveUI;

    public class SampleViewModel : ReactiveObject
    {
        private string inputString1;
        public string InputString1 {
            get { return inputString1; }
            set { this.RaiseAndSetIfChanged(ref inputString1, value); }
        }

        private string inputString2 = "input2の初期値です";
        public string InputString2 {
            get { return inputString2; }
            set { this.RaiseAndSetIfChanged(ref inputString2, value); }
        }

        private DateTime currentDateTime = DateTime.Now;
        public DateTime CurrentDateTime {
            get { return currentDateTime; }
            set { this.RaiseAndSetIfChanged(ref currentDateTime, value); }
        }

        // 初期化コマンド
        public ReactiveCommand<object> Initialize{ get; set;}

        public SampleViewModel()
        {
            // 初期化コマンドの作成
            Initialize = ReactiveCommand.Create(this.WhenAny(vm => vm.InputString1, str => true)); // =常時有効
            Initialize.Subscribe(_ =>
            {
                InputString1 = "初期値1";
                InputString2 = "初期値2";
                CurrentDateTime = new DateTime(2020, 1, 1);
            }
            );
        }

        public override string ToString(){
            return "1:" + InputString1 + Environment.NewLine +
                   "2:" + InputString2 + Environment.NewLine +
                   CurrentDateTime.ToString();
        }
    }
}

MVVMに従い、Viewに対する操作は全てViewModelに定義します。ViewModelの基本クラスとしてReactiveUI.ReactiveObjectクラスから派生させます。

データ更新は通常INotifyPropertyChangedを実装しますが、ReactiveObjectクラスが実装するRaiseAndSetIfChangedメソッドの呼び出しでフィールドへの値のセット及びINotifyPropertyChangedのイベント発砲の処理を行います。thisを明示しないとこのメソッドが出てこず苦労しました;

データ操作はReactiveCommandとして公開します。ReactiveExtensionsを使い、いつ、どんな処理を行うかを定義します。上の例では常時有効にしていますが、これをプロパティ状態に応じてtrue/falseが切り替わるようにするとこの後でバインドするボタンの有効状態が変わります。WPFではおなじみのICommandを実装しているのでCanExecute/Executeを実装しているのでWindowsFormsであっても有効状態も同期する利点がでています。

これをバインドする先のViewはこんな感じになります。

 SampleViewForm.cs

namespace ReactiveUiForWinFormsTest
{
    using System.Windows.Forms;
    using ReactiveUI;

    public partial class SampleViewForm : Form, IViewFor<SampleViewModel>
    {
        public SampleViewForm(SampleViewModel viewModel)
        {
            InitializeComponent();

            ViewModel = viewModel;
            this.Bind(ViewModel, vm => vm.InputString1, v => v.input1TextBox.Text);
            this.Bind(ViewModel, vm => vm.InputString2, v => v.input2TextBox.Text);
            this.Bind(ViewModel, vm => vm.CurrentDateTime, v => v.currentDateTimePicker.Value);
            this.BindCommand(ViewModel, vm => vm.Initialize, v => v.executeButton);
            this.closeButton.Click += (_, __) => this.Close();
        }

        public SampleViewModel ViewModel{
            get;set;
        }
        object IViewFor.ViewModel{
            get { return ViewModel; }
            set { ViewModel = value as SampleViewModel; }
        }
    }
}

ViewはViewModelに依存するので、依存先のViewModelを指定したIViewFor<TViewModel>から派生させます。CloseだけはViewで処理していますが、他は全てViewCommandの処理とバインドさせておりViewが処理を担うことをなくしています。ここでのFormは前述の通りただのViewなのでViewModelはConstructor Injectionで渡してもらうのが妥当でしょう。

蛇足ですがこれを呼び出すのはこんな感じ。

Program.cs

namespace ReactiveUiForWinFormsTest
{
    using System;
    using System.Windows.Forms;
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var viewModel = new SampleViewModel();
            Application.Run(new SampleViewForm(viewModel));
            MessageBox.Show(viewModel.ToString());
        }
    }
}

実行結果はご想像の通り、ちゃんとViewModelとViewのバインドはとれています。

binding__

上の例では単純にBindでバインドしただけですが、WPFでのバインド似でOneWayBindがあったり、バインド時の引数にIValueConverterみたいなものがあったり、かなり使えそうな印象です。

多言語対応(C#、WPF編)

Windows Formsの多言語対応はそういくつも方法がある訳ではありません、以前紹介した方法かちょっとしたバリエーション違い・・程度ではないかと思っています。

一方WPFの場合は表示部(View)をXAMLで構成する都合、そのXAMLのUI要素をどう多言語対応するかにはいくつかの方法が出てきます。私が知り得たところを少しご紹介します。

1.LocBAMLを使う方法

MSDN他、MCTS(70-511)という.NET4.0(WinForms+WPF)の資格試験があったのですが、その試験の自習教材の中でWPFのローカライズ方法としてこのLocBamlを使う方法が紹介されています。この意味でMicrosoftがWPFアプリケーションの多言語対応として推奨(?)想定(?)しているのはこの方法のように感じます。この方法の大まかな手順は、

  1. UIをXAMLで構成する。
  2. MSBuildを使ってローカライズ対象要素にx:UID属性を付与する。
  3. x:UID属性がついた要素をLocBamlツールを使ってcsvとしてエキスポート
  4. csvファイルをローカライズ、ビルドしてサテライトアセンブリを作る

というものです。LocBamlは自分でビルドが必要なツールであることや.NET4.0以降ではそのままでは動作しないとやりにくいところがあるようですが、こちらのサイトでやり方が説明されています。作業が面倒で一部自動化のツール用意が必要など、ローカライズの主流ではないように感じます。

2.Resource.resxを使う方法

VisualStudioのC#プロジェクトに存在するResource.resxを使う方法です。Windows Formsの多言語対応時と文言定義場所位置が同じため、WinFormsアプリとWPFアプリを両方提供する場合には便利そうです。日本のブロガーによく紹介されているようです、この方法の大まかな手順は、

  1. UIをXAMLで構成する
  2. Resource.resxに言語リソースを定義する。
  3. 対応言語分resxファイルの追加、文言定義を繰り返す。
  4. XAMLから文言をStaticResourceで参照する

もともとCodeProjectで紹介された方法のようですが、こちらのサイトでとてもわかりやすく説明されています。

3.リソースディクショナリを使う方法

先に補足ですが、WPFではPenやBrushといったオブジェクトをリソースとして定義できますが、それらをリソースディクショナリという別ファイルにすることが可能です。これは上記2の方法で出てきたResource.resxファイルとは別モノのWPF特有ですのでご注意ください。

リソースディクショナリにはString型、つまり文字列もリソースとして定義可能ですのでこのリソースディクショナリに文言をXAMLで定義しておき、UI要素からリソースとして参照する方法があります。海外のブロガーがよく紹介されているようです、大まかな手順は、

  1. UIをXAMLで構成する
  2. プロジェクトに新しい項目としてリソースディクショナリを追加します。ここにsystem:String型の要素として文字列リソースを定義します。
  3. 対応言語分リソースディクショナリを作成します、このとき同一文言に対しては手順1で割り当てたKeyを合わせておくことが必要です。
  4. 手順1のXAMLのUI要素から必要な文字列をStaticResource or DynamicResourceで参照します。

これはこちらのサイトで紹介されています。2の方法との違いは、resxファイルにリソース定義するかxamlファイルにリソース定義するかの違いです。resxファイルはVisualStudioにエディタがありますが、文言翻訳作業を考えると私はこれがメリットには感じず、XamlのほうがReader/Writerがある分運用しやすく感じます。

ということで3の方法とその運用で使いそうな内容についてちょっとご紹介します。まずUIをXAMLで定義し、翻訳必要な文言を別リソースファイルとして定義するとこのような形になります。

MainWindow.xaml

<Window x:Class="MultiLanguageWpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="179" Width="534">
    <Grid>
        <Label Content="{DynamicResource language}" Height="28" HorizontalAlignment="Left" Margin="12,12,0,0" Name="label1" VerticalAlignment="Top" />
        <ComboBox Height="24" HorizontalAlignment="Left" Margin="100,12,0,0" Name="languageComboBox" VerticalAlignment="Top" Width="120" SelectedIndex="0" SelectionChanged="languageComboBoxSelectionChanged">
            <ComboBoxItem>日本語</ComboBoxItem>
            <ComboBoxItem>English</ComboBoxItem>
            <ComboBoxItem>Deutsch</ComboBoxItem>
        </ComboBox>
        <Button Content="{DynamicResource convert}" Height="23" HorizontalAlignment="Left" Margin="396,85,0,0" Name="convertButton" VerticalAlignment="Top" Width="88" Click="convertButton_Click" />
        <Label Content="{DynamicResource input}" Height="28" HorizontalAlignment="Left" Margin="12,66,0,0" Name="label2" VerticalAlignment="Top" />
        <TextBox Height="24" HorizontalAlignment="Left" Margin="100,70,0,0" Name="inputFileTextBox" VerticalAlignment="Top" Width="290" />
        <Label Content="{DynamicResource output}" Height="28" HorizontalAlignment="Left" Margin="12,100,0,0" Name="label3" VerticalAlignment="Top" />
        <TextBox Height="24" HorizontalAlignment="Left" Margin="100,100,0,0" Name="outputFileTextBox" VerticalAlignment="Top" Width="290" />
    </Grid>
</Window>

リソースの実体はそれぞれ以下のような形で定義します。

StringResource.ja-jp.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">言語</system:String>
    <system:String x:Key="update">更新</system:String>
    <system:String x:Key="convert">変換</system:String>
    <system:String x:Key="input">入力</system:String>
    <system:String x:Key="output">出力</system:String>
    <system:String x:Key="file">ファイル</system:String>
</ResourceDictionary>

StringResource.en-us.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">Language</system:String>
    <system:String x:Key="update">Update</system:String>
    <system:String x:Key="convert">Convert</system:String>
    <system:String x:Key="input">Input</system:String>
    <system:String x:Key="output">Output</system:String>
    <system:String x:Key="file">File</system:String>
</ResourceDictionary>

StringResource.de-DE.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:system="clr-namespace:System;assembly=mscorlib">
    <system:String x:Key="language">Sprache</system:String>
    <system:String x:Key="update">Aktualisierung</system:String>
    <system:String x:Key="convert">Umwandlung</system:String>
    <system:String x:Key="input">Eingabe</system:String>
    <system:String x:Key="output">Ausgabe</system:String>
    <system:String x:Key="file">Akte</system:String>
</ResourceDictionary>

japan

上のようなUIで、コードビハインドに以下のような簡単な処理を用意しておくと

MainWindow.xaml.cs

namespace MultiLanguageWpfApp
{
    using System;
    using System.Windows;

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            SetLanguage("ja-jp");
        }
        private void SetLanguage(string cultureCode)
        {
            var dictionary = new ResourceDictionary();
            dictionary.Source = new Uri(@"Resources/StringResource." + cultureCode + @".xaml", UriKind.Relative);
            this.Resources.MergedDictionaries.Add(dictionary);
        }
        private void languageComboBoxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
        {
            string[] culcureCodes = { "ja-jp", "en-us", "de-DE" };
            SetLanguage(culcureCodes[languageComboBox.SelectedIndex]);
        }
        // xaml-xls変換
        private void convertButton_Click(object sender, RoutedEventArgs e)
        {
            // 後述
        }
    }
}

ドイツ語

コンボボックスアイテム変更で動的な言語切り替えが可能となります。

最後に。私が考える翻訳作業では、ビルド用にはXAMLリソースディクショナリが必要ですが、翻訳業者に提出する資料としたり、文字数制限チェックをしたりと、ベースはExcelファイルの方が都合よさそうです。となるとExcelファイルは読み書きがほしくなるのでNPOIを使って、XAML(リソースディクショナリ)⇔Excel間の変換に以下のようなクラスを使います。

XamlResourceConverter.cs

namespace MultiLanguageWpfApp
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Collections;
    using System.Windows;
    using System.Windows.Markup;
    using NPOI.HSSF.UserModel;
    using NPOI.SS.UserModel;

    public class XamlResourceConverter
    {
        private const int targetColumnIndex = 1;
        private static readonly string sheetName = "sheet1";

        // コンストラクタを外部から隠ぺい
        private XamlResourceConverter() { }

        // ID, 文言のペア
        private Dictionary<string, string> stringTable = new Dictionary<string, string>();

        // Dictionaryから
        public XamlResourceConverter(Dictionary<string, string> table)
        {
            stringTable = table;
        }

        // XAMLファイルから読み込み
        public static XamlResourceConverter FromXamlFile(string xamlResourceFile)
        {
            Dictionary<string, string> table = new Dictionary<string, string>();
            using (FileStream inputStream = new FileStream(xamlResourceFile, FileMode.Open))
            {
                ResourceDictionary xamlTop = XamlReader.Load(inputStream) as ResourceDictionary;
                if (xamlTop == null)
                {
                    throw new InvalidDataException();
                }
                foreach (DictionaryEntry item in xamlTop)
                {
                    table.Add(item.Key as String, item.Value as String);
                }
            }
            return new XamlResourceConverter(table);
        }

        // XLSファイルから読み込み
        public static XamlResourceConverter FromExcelFile(string excelFile)
        {
            Dictionary<string, string> table = new Dictionary<string, string>();
            using (FileStream inputStream = new FileStream(excelFile, FileMode.Open, FileAccess.Read))
            {
                HSSFWorkbook book = new HSSFWorkbook(inputStream);
                ISheet sheet = book.GetSheet(sheetName);
                for (int rowIndex = 0; rowIndex <= sheet.LastRowNum; rowIndex++)
                {
                    IRow row = sheet.GetRow(rowIndex);
                    table.Add(row.GetCell(0).StringCellValue, row.GetCell(1).StringCellValue);
                }
            }
            return new XamlResourceConverter(table);
        }

        // XAMLファイルへの書き込み
        public void SaveToXaml(string xamlResourceFile)
        {
            var resourceDictionary = new ResourceDictionary();
            foreach (var item in stringTable)
            {
                resourceDictionary.Add(item.Key, item.Value);
            }
            var xaml = XamlWriter.Save(resourceDictionary);
            File.WriteAllText(xamlResourceFile, xaml);
        }

        // XLSファイルへの書き込み
        public void SaveToExcel(string excelFile)
        {
            using (FileStream outputStream = new FileStream(excelFile, FileMode.OpenOrCreate))
            {
                // NPOIを使ってExcel出力
                var book = new HSSFWorkbook();
                var sheet = book.CreateSheet(sheetName);
                int rowIndex = 0;
                foreach (KeyValuePair<string,string> item in stringTable)
                {
                    var row = sheet.CreateRow(rowIndex++);
                    var cell1 = row.CreateCell(0, NPOI.SS.UserModel.CellType.String);
                    var cell2 = row.CreateCell(targetColumnIndex, NPOI.SS.UserModel.CellType.String);
                    cell1.SetCellValue(item.Key);
                    cell2.SetCellValue(item.Value);
                }
                book.Write(outputStream);
            }
        }
    }
}

NPOIはxlsxは扱えないみたいで、xls限定になります。他にもWPFの多言語対応化は方法があるようですが、TPOで適宜判断ください。

WPFマークアップ拡張の自作

色々あって更新ができない状況、何とか1ネタ。

WPFではほとんどの場合XAMLBindingしますよね。そのBindingにはマークアップ拡張を使います、マークアップ拡張ってのは”{ … }” という部分。Bindingのほか、StaticResourceやDynamicResourceの参照でも使われます。WPFの機能を使いこなすのにビヘイビアとは違うアプローチで覚えておいて損がないと思われます。ということでやってみましょう。

題材はXAMLのバインドのマークアップ拡張の自作。

マークアップ拡張はMarkupExtension抽象クラスを派生させて作ります。この抽象クラスは下のような単一のメソッドを実装します。このメソッドで戻す値はマークアップ拡張として使った時に返すべき値となります。

    public class EmulateBindingExtension : MarkupExtension
    {
        public override object ProvideValue(IServiceProvider serviceProvider)
        }
    }

バインドするのでBindingにならって名前はEmulateBindingクラスとします。使うときは下のような感じになることを想定しています。

<Window x:Class="MarkupExtSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:me="clr-namespace:MarkupExtSample"
        Title="MainWindow" Height="142" Width="370">
    <StackPanel>
        <TextBox x:Name="TextBox1" Text="{Binding Path=StringProperty}" Height="24" VerticalAlignment="Top" Width="348" />
        <TextBox x:Name="TextBox2" Text="{me:EmulateBinding Path=StringProperty}" Height="24" VerticalAlignment="Top" Width="348" />
    </StackPanel>
</Window>

さぁ、実装。

namespace MarkupExtSample
{
    using System;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Markup;

    public class EmulateBindingExtension : MarkupExtension
    {
        public PropertyPath Path { get; set; }

        public EmulateBindingExtension(){}

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget provider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (provider == null){
                return null;
            }
            var target = provider.TargetObject as FrameworkElement;
            if (target == null){
                return null;
            }
            var property = provider.TargetProperty as DependencyProperty;
            if (property == null){
                return null;
            }

            Binding bind = new Binding();
            bind.Path = Path;
            BindingOperations.SetBinding(target, property, bind);
            return target.GetValue(property);
        }
    }
}

これに対してMainWindowのDataContextにバインドソースをセットしたらokです。

namespace MarkupExtSample
{
    using System.Windows;

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new TestModelView() { StringProperty = "初期値だよ" };
        }
    }
}

DataContextの場合はBindingのSourceプロパティに何の指定もいらないことにご注意。

動作

他にもSourceなど適宜プロパティを公開すればBindingにより似てくると思われます。

多言語対応(C#、Windows Forms編)

チラホラ見かけた情報だけだと実務には足りない気がしたので、Windows Formsでの多言語対応について少々。まず前提として、デフォルトの言語を決めておき、それ以外の文言を必要に応じて追加するという方法を取ります。

デフォルトは日本語か英語でしょう、多国語対応がタイ語、スペイン語やポルトガル語などまで意識するようなものなら英語をデフォルトとしたほうがよさそうです。

 

1.Formのリソース

よくあるのがコチラ。Formを多言語対応する場合、

  1. FormのLanguageプロパティが(規定値)の状態で、Formを作成します。
  2. FormのLocalizableプロパティをTrueにし、Languageプロパティを多言語対応する言語に変更した上で、1で編集したFormを編集しなおします。
  3. 必要言語分、2を繰り返します。

form_propertyというステップ。

ステップ2でLanguageを変更後、Formのコントロールを初めて変更するタイミングで新たなリソースファイルが作成されます。

multiple_resource_files

この例では規定の言語用のTestForm.resxと、日本語用のTestForm.ja.resx、ドイツ語用のTestForm.de.resxができています。これをビルドすると実行ファイルに[ja]とか、[de]とかいうフォルダがあり、リソースだけを含んだDLL(サテライトアセンブリと呼びます)ができます。実行時にCurrentUICultureに基づいて自動的に読みだしてくれます。

 

2.その他のリソース

MessageBoxやファイル出力などForm以外でも文言が必要な場合はあります。そのような文言も多言語対応する場合はResources.resxファイルに格納します。これも初期状態では規定の言語用のファイルしかありませんので、多言語対応する言語ごとに作り直します。

resource_2

多言語用のresxファイルは、規定の言語用のResources.resxファイルをコピーしてプロジェクトに張り付けした後、適切なカルチャコードを加えてPropertiesフォルダに格納します。カルチャーコードはネット上で調べてもわかるし、Formを多言語対応する時にはカルチャコードがわかるのでそれを真似て作ればOKです。

resources_13

アクセスするときはこんな感じ。

Console.WriteLine(Properties.Resources.IDS_BUTTON);

これも同じくビルドすると先ほど同様サテライトアセンブリに組込まれるので、あとはCurrentCultureに基づいてロードされます。

 

3.文言翻訳運用

確かにメインは以上なんですが、『これでお終い』って言われると・・・?オレオレ翻訳ならOkだけど、普通は翻訳家・翻訳業者に頼んだり、プルーフするはずで実際の運用だとどう考えてもこの辺のリソースをまとめる作業が必要になる。

例えば文言リストを一覧形式で保持する必要があるならExcelで運用しといて、翻訳が完了したらresxファイルを作るって手がある。つまり、Excelにこんな感じで文字列一覧を作って、

string_talbe_2

このExcelファイルを読みこむを↓のような感じで作って、

ExcelToResx.cs

namespace MultiLanguageWindowsFormsApp.Tool
{
    using System.Resources;
    using System.Linq;
    using LinqToExcel;

    class ExcelToResx
    {
        private string excelPath;
        public ExcelToResx(string excelPath)
        {
            this.excelPath = excelPath;
        }

        // language="日本語"とか。
        public void Save(string filePath, string language)
        {
            var factory = new ExcelQueryFactory(excelPath);
            var query = factory.Worksheet("Sheet1");
            using (var writer = new ResXResourceWriter(filePath))
            {
                foreach (var item in query){
                    writer.AddResource(item["ID"], item[language]);
                }
            }
        }
    }
}

Resources.rexもForm.resxも上のExcelにまとめて作ってやって、どのリソースファイルなのかを示す列を作っておけば簡単です。LinqToExcelなんだから、ファイルでLoop回しながらSelectするという手があります、あとは出力されるresxファイルをプロジェクトに登録しなおせばOK。

文字数制限チェックはExcelVBAなどでもチェックできます。また、ResXResourceWriterは書き込み専用で “追記” ができないので、リソースファイルの使い方によってはFormで参照する文字列も全てResource.resxに定義しておき、FormのコンストラクタでResourcesを参照する構成にすれば翻訳対象のresxファイルが一つになるので管理しやすいかもしれません。