LoginSignup
6
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-03-15

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を使いまわす方が楽な状況であったら、このアプローチを試してみるのもいいだろう。

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6