winformsのresxを他のGUIフレームワークで使いまわす


Windows Formsのコードを他のGUIフレームワークに移植して使い回したい時、障害の一つになるのがresx形式のリソースだ。この時、特にmonoやモバイル環境などで使うことを考えて、System.Windows.Forms.dllやSystem.Drawing.dllへの参照を要求しないようにしたい。これは可能だろうか?

WPFで*.resxから自動生成された Resources.Designer.cs のようなファイルを使いまわすには、XAMLのStaticResource要素を使えば簡単に出来るようだ。しかし、その中ではあくまでSystem.Drawing.Bitmapなどが参照されている。これでは上記の前提を満たしていないことになる。

というわけで、今回は(2015年にもなって)*.resxを他のGUIフレームワークで使いまわせるようなやり方を考えたので、それについてまとめておきたい。

本題に入る前に念を押しておくべきことは、*.resxを使っているのはWindows Formsくらいのものである、ということだ。筆者は今ほとんどWindowsとVisual Studioを使っていないので詳しくはわからないが、Visual Studioでは*.resxファイルを編集するUIがあって、そこには文字列や画像ファイルを登録する仕組みがある、はずだ。

*.resourcesによるリソース解決

resxファイルは、Visual Studioあるいはresgen.exeによって、*.resourcesファイルに変換されることもあるし、C#などのソースコード(Resources.Designer.csなど)が生成されることもある。

*.resourcesファイルは、*.resxファイルで指定された各種リソースをバイナリシリアライズしておき、アセンブリにEmbeddedResourceとして埋め込んだ上で、実行時にmanifest resource streamからバイナリデシリアライズする。

実際にはバイナリシリアライズである必要は無く、<resheader name=“writer”> という要素の内容で指定されたCLI型のSystem.Resources.IResourceWriterの実装(通常はSystem.Windows.Forms.dllに含まれるSystem.Resources.ResXResourceWriter)でシリアライズし、<resheader name=“reader”> という要素の内容で指定されたCLI型のSystem.Resources.IResourceReaderの実装(通常はSystem.Windows.Forms.dllに含まれるSystem.Resources.ResXResourceReader)でデシリアライズ出来れば良い。はずである。resourcesについては、筆者は動作確認していないので、実際の動作と異なる部分があるかもしれない。

実際に*.resourcesをリソースの解決に使用することはあるかどうかは、筆者には実のところ分からないが、ひとつ言えることとしては、*.resourcesを読み込むためには、ResXResourceReaderが必要になり、これはSystem.Windows.Forms.dllに含まれているため、望ましくない参照が発生してしまうことになる。そうすると、冒頭で挙げた前提を満たさなくなってしまうのである。

Contentとしてアプリケーション ディレクトリにコピーされたリソースを解決する - dynamicアプローチ

一方、実は、*.resxで指定されたリソースファイルをContent形式でアプリケーションのフォルダにコピーし、実行時に*.resxファイルの内容から直接リソースをデシリアライズする仕組みであれば、ResXResourceReaderおよびResXResourceWriterに依存することなく、リソースが取り出せるのである。

このとき、*.resxはEmbeddedResourceとして(*.resourcesの代わりに)アセンブリに埋め込まれることになるし、各リソースファイル自体は、Contentとしてアプリケーションのフォルダにコピーするようにしておく。

この時点で筆者が考えたのは、Properties.Resources型のstaticメンバーとしてstrongly typedな(C#などのコードからアクセスできる)リソースを表すメンバーを生成する代わりに、C# 4.0のdynamic型を使用して、リソースを動的に解決できないか、ということだった。これは、以下のようなクラスで実現できる(やっつけではある)。

public static class Properties
{
    internal static dynamic Resources = new DynamicResource ();
}
public class DynamicResource : DynamicObject
{
    static readonly string[] resource_names = new DirectoryInfo (".").GetFiles ("resources/*").Select (d => d.ToString ()).ToArray ();

    public override bool TryGetMember (GetMemberBinder binder, out object result)
    {
        result = GetResource (binder.Name);
        return result != null;
    }

    public object GetResource (string name)
    {
        var res = resource_names.FirstOrDefault (n => Path.GetFileNameWithoutExtension (n) == name);
        if (res != null) {
            switch (Path.GetExtension (res)) {
            case ".png":
                return Xwt.Drawing.BitmapImage.FromFile (res);
            case ".ico":
                return null;
            case ".txt":
                return File.ReadAllText (res);
            }
        }
        return null;
    }
}

画像はXwt.Drawing.BitmapImageとして返される。これで、以下のようなコードが書けるようになる:

Xwt.Drawing.Image image1 = Properties.Resources.image1;

これでもう十分ではないか。とも思ったが、筆者のコードでは、この後さらにXwt.Drawing.BitmapImageに対するextension methodの呼び出しが多用されていて、dynamicとの相性が非常に悪かった(拡張メソッドはコンパイル時に静的に解決されるものなので、dynamicオブジェクトからは解決できない)。

Contentとしてアプリケーション ディレクトリにコピーされたリソースを解決する - ResXFileRef

というわけで、もう少しオリジナルのProperties.Resourcesクラスに近いコードが利用できるような解決策を考えなければならなくなった。とりあえず、もう少し*.resxの仕組みを掘り下げてみよう。

*.resxファイル内では、各リソースは、次のようなdata要素として指定されている:

<data name="alarm_clock" type="System.Resources.ResXFileRef, System.Windows.Forms">
  <value>..\resources\alarm-clock.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
</data>

この内容が今回のキモのひとつだ。typeという属性は、そのリソースをどのCLI型でdata要素からインスタンス化するかを示している。この例では、System.Windows.Forms.dll上に存在する System.Resources.ResXFileRef という型が使用されている。この型は何かというと、ファイル名と変換先のCLI型を渡すことで、その型に対する変換をサポートする、というものだ。

具体的には、System.Resources.ResXFileRefには、TypeConverterAttributeが指定されていて、その中ではSystem.Resources.ResXFileRef.Converterの型名が指定されている。このConverterはTypeConverterの実装クラスで、ResXFileRefからSystem.Drawing.Bitmapへの変換(つまりResXFileRef.FileNameをBitmap.FromFile()に渡すだけの処理)などをまとめている、と考えられる(monoの実装はそのようになっている)。

strongly-typedなリソースへのアクセスを実現している Resources.Designer.cs では、各リソースに対応するメンバーは、次のように定義されている:

internal static System.Drawing.Bitmap image1
{
    get
    {
        object obj = ResourceManager.GetObject("image1", resourceCulture);
        return ((System.Drawing.Bitmap)(obj));
    }
}

実のところ、このResourceManagerというのも、(型ではなく)このResourcesクラスに定義されたプロパティだ:

internal static global::System.Resources.ResourceManager ResourceManager { get; }

この System.Resources.ResourceManager型は、mscorlib.dllで定義されていて、特にWindows.Formsに固有のものではない。これに対して、ResXFileRef型などは、System.Windows.Forms.dllで定義されている、Windows.Forms固有の変換をサポートしたものだ。ResourceManager.GetObject()では、指定されたリソースについて、*.resxファイル(EmbeddedResourceとして取得できる)の内容を読み込んで、data要素でname属性がマッチするものを取得し、type属性で指定された型のインスタンスを、value属性で指定されたコンストラクタ引数をもとに生成する。その上で、要求された型のオブジェクトへの変換が行われる。

今回は、System.Windows.Forms.dllに依存しないリソースの定義を志しているので、このResXFileRef型を使うわけにはいかない。ではどうすればいいのか? 簡単である。同じようなResXFileRefクラスを、ただし自分たちが期待するような型変換(たとえば、System.Drawing.dllのBitmapではなく、Xwt.dllのBitmapへの変換)をサポートするかたちで、定義して、*.resxで指定してやればいいのである。

ResXFileRefで定義すべき内容は簡単なので、ここではmonoのソースコードにリンクするにとどめておきたい。 https://github.com/mono/mono/blob/master/mcs/class/System.Windows.Forms/System.Resources/ResXFileRef.cs

未解決の問題 - デザイナー クラスの自動生成

ここまでで、とりあえずプラットフォーム中立なかたちでresxファイルを定義することは、何となく可能そうであるということが伝わったかもしれない。ここで最後に問題になるのが、デザイナー クラスのコードを自動生成できるかどうか、である。

実のところ、System.Resources.StronglyTypedResourceBuilderというクラスを使うと、このデザイナー クラスを自動生成することができる。このクラスはSystem.Design.dllに含まれていて、プラットフォーム中立と言えなくもない。実際にはこのアセンブリはSystem.Windows.Forms.dllなどを参照するのだけど、われわれは実行時にコード生成を行うわけではなく、生成されたコードは不必要な参照を要求しない。

Windows SDKに含まれるresgen.exeには、/strというオプションがあって、このデザイナー クラスを生成することは出来るようだ。monoのresgen.exeは、そこまでの要求がなかったこともあってか、未だに実装されていないようだ(Windows.Forms自体がobsoleteな感じになっているので、着手する価値があるとは考えにくい)。

ただ、いずれにしろ、問題は、*.resxに変更を加えて保存した時に、自動的にデザイナー クラスを生成する仕組みは、IDE側で個別に実装するしか無い、ということだ。このデザイナー クラスをどう生成するかを指定する方法は、筆者には分からない。Visual Studioでは、旧バージョンではCOMを登録する必要があったようだ。VS2013では、たぶんVS SDKとアドインAPIでいけることだろう。 (参考: http://www.codeproject.com/Articles/13830/Extended-Strongly-Typed-Resource-Generator)

MSBuildでビルドするcsprojなどの中で、<BeforeBuild>としてresgen.exeあるいは自分のツールの呼び出しを行う、という方法も考えられる(ただしIDEの保存から自動的に呼び出すことは出来ないだろう)。resgen.exeには、独自のアセンブリ参照(たとえばXwt.dllなど)を追加することが出来ないようなので、StronglyTypedResourceBuilderを呼び出す自前のツールを書かなければならないことも、あるかもしれない。

monodevelopの場合、コードジェネレーターはこの辺にある。 https://github.com/mono/monodevelop/tree/master/main/src/core/MonoDevelop.Ide/MonoDevelop.Ide.CustomTools

筆者は、とりあえず手元のプロジェクトではリソースが追加されることがないので、手書きでResources.Designer.csを書き換えることで対応した。

総括

非Windows.Forms環境でも、*.resxリソースを使用することは可能である。ただし、内部的にはちまちまとWindows.Forms依存の実装が含まれているので(とは言っても、そうならないようにMicrosoftがAPIを慎重に規定している様子が、関係ある型の存在するアセンブリの分散ぶりからも、窺い知ることが出来る)、使いまわせる部分と、そうでない部分を意識したほうが良いだろう。ともあれ、もし*.resxを使いまわす方が楽な状況であったら、このアプローチを試してみるのもいいだろう。

March 15, 2015
213 words


Categories

Tags

Author

Backlog