shuhelohelo’s blog

Xamarin.Forms多めです.

プロジェクト内のテキストファイルを読み込む方法 ~Resource~[引越記事]

いきなり追記(2018.10.15)

@Zuishinさんより教えていただいた方法だと、ビルドアクションにResourceを設定したファイルに非常にスマートにアクセスできます。@Zuishinさんありがとうございます。

さて、ファイルの追加、ビルドアクションの設定の仕方は僕の書いたほうを読んでいただくとして、スマートな方法は以下のとおりです。

//@Zuishinさんに教えていただいた方法
string fileCount;
var info = Application.GetResourceStream(new Uri("/TextFiles/TextFile1.txt",UriKind.Relative));
using (var sr = new StreamReader(info.Stream))
{
    fileContent = sr.ReadToEnd();
}

波括弧を除けば3行でファイルの中身を取得できます。スマートです。

頑張って説明してみます。

Application.GetResourceStream()メソッドでファイルへのストリームを取得、と思いきやこのメソッドの返り値はStreamResourceInfoです。が、この返り値がファイルへのStreamを持っていますので、それを使ってStreamReaderでファイルの中身を取得します。

さて、GetResourceStream()メソッドですが、引数に目的のファイルの位置をUriで受け取ります。Uriの部分は、下のようにソリューションエクスプローラにおいて、プロジェクトフォルダを起点としたパス文字列を構成し、UriKind.Relativeで「相対パスですよ」と指定します。

new Uri("/TextFiles/TextFile1.txt",UriKind.Relative)

後は前述のとおりStreamReaderを使ってファイルの中身を取得します。

素晴らしくお手軽ですね。 以上です。 この方法を追加したプロジェクトはこちら

はじめに

(注:コード内にAssemblyクラスが出てきますが、これはSystem.Reflection名前空間内にあるので、適宜usingしてください)

Visual Studioでアプリケーションのプロジェクトにフォルダやファイルを追加できますが、プログラム中からそれらのファイルにどうやってアクセスするか、の第2弾です。 ファイルのプロパティでビルドアクションを設定できるのですが、埋め込みリソースResourceコンテンツと設定することでプログラムからそれらのファイルにアクセスすることができます。 以前埋め込みリソースなファイルへのアクセスの仕方について説明しましたが、今回はResourceなファイルへのアクセスの仕方について説明します。

ファイルを追加します

image.png

TextFilesフォルダを追加して、その中にTextFile1.txtを追加します。TextFile1.txtに何か適当なものを書き込んでおきます。

ファイルのビルドアクションをResourceに設定します

ソリューションエクスプローラ上でTextFile1.txtのプロパティを開いて、ビルドアクションをResourceに設定します。 image.png 通常、ファイルを追加した場合はデフォルトでResourceになっています。

まずは一度コンパイルします

ビルドアクションにResourceを設定したファイルはコンパイルされてアプリケーション名.g.resourcesというファイルにバイナリデータとして書き込まれます。 このファイルはビルドすると作成されるので、まずは一度ビルドしてみましょう。 ビルドが正常終了したら以下のフォルダ内に先ほどのアプリケーション名.g.resourcesファイルができていることが確認できます。

ソリューションフォルダ\プロジェクトフォルダ\obj\Debug

image.png

この中にResourceに指定したテキストファイルがバイナリデータで入っているはずです。

アプリケーション名.g.resourcesファイルへのストリームを開く

何はともあれ、ファイルがあればそこへのストリームを開きます。 このプロジェクトに含まれるファイルへアクセスする場合、まずはアセンブリ(今書いているプロジェクトがコンパイルされて出来上がるexeやdllのこと)へアクセスる手段を取得します。

using System.Reflection;
//~~~~~~~~~~~~~~~~

public MainWindow()
{
    InitializeComponent();

    var assembly = Assembly.GetExecutingAssembly();
}

GetExecutingAssembly()わかりやすいメソッド名ですね。実行中のアセンブリを取得しろ、と。

そして、アプリケーション名.g.resourcesへのストリームを開きます。

using (var stream = assembly.GetManifestResourceStream("AccessToResourceInProject.g.resources"))
{
・・・
}

追加したテキストファイルへアクセスする

ここまででアプリケーション名.g.resourcesファイルへアクセスできるようになりました。 では、このファイルの中に入っているだろうテキストファイルへはどのようにアクセスしたらよいのでしょうか。 ここで使用するのがSystem.Resources.ResourceReaderクラスです。 このクラスを使用してアプリケーション名.g.resourcesファイル内のリソースへアクセスするためのストリームを開きます。

using (var stream = assembly.GetManifestResourceStream("AccessToResourceInProject.g.resources"))
{
    if (stream != null)
    {
        using (var rr = new ResourceReader(stream))
        {
      ・・・
        }
    }
}

この変数rrアプリケーション名.g.resourcesファイル内のリソースファイルへアクセスするための手段となるわけです。 このResourceReaderというクラス、名前からしてさぞResourceなファイルへのアクセスを楽にしてくれそうですね。

ファイルへアクセスします

インテリセンスによると、やはりそのものずばりといったメソッドがあります。GetResourceData()image.png

もうゴールは目の前な気がします。 GetResourceData()を使う場合、以下のようになります。 resourceNameにはフォルダ名とファイル名を全部小文字にしてリソースへのパスを作って入れておきます。 ここで気を付けることはもう一つあって、フォルダ名とファイル名を「.」でつなぐのではなく「/」でつなぐということ。

string fileContent = "";
using (var stream = assembly.GetManifestResourceStream("AccessToResourceInProject.g.resources"))
{
    if (stream != null)
    {
        using (var rr = new ResourceReader(stream))
        {
            if (rr != null)
            {
                var resourceName = "textfiles/textfile1.txt";
                string type;
                byte[] resourceData;
                rr.GetResourceData(resourceName,out type,out resourceData);
                
                if (resourceData != null)
                {
                    fileContent = Encoding.UTF8.GetString(resourceData);
                }
            }
        }
    }
} 

GetResourceData()に必要な引数を渡して呼び出すと、resourceDataの中に指定したファイルの中身がbyte配列で入ります。 今回はテキストファイルなのでそれを文字列に変換すると以下のような値が取得できます。 image.png

あれ? ファイルの中身は取得できているようですが、なんだか様子がおかしいです。 取得した文字列の先頭にJ\0\0\0がついています。 これが何なのかはわかりませんが、ほしいのはこの4バイト分を除いた文字列なので、その場合は以下のようにして先頭4バイトを除いたデータを文字列に変換するとよいかもしれません。

fileContent = Encoding.UTF8.GetString(resourceData.Skip(4).ToArray());

おあつらえ向きと思ったGetSourceData()ですが、それほど使いやすくはないように感じました。

もう一つ

string fileContent = "";
using (var stream = assembly.GetManifestResourceStream("AccessToResourceInProject.g.resources"))
{
    if (stream != null)
    {
        using (var rr = new ResourceReader(stream))
        {
            if (rr != null)
            {
                var resourceName = "textfiles/textfile1.txt";

                foreach (DictionaryEntry resource in rr)
                {
                    if ((string) resource.Key == resourceName)
                    {
                        using (var sr = new StreamReader((Stream) resource.Value))
                        {
                            fileContent = sr.ReadToEnd();
                        }
                    }
                }
            }
        }
    }
}

rrはResourceReaderのインスタンスですが、これをforeachで回すと、DictionaryEntryクラスのデータを順に取得することができます。 このDictionaryEntryクラスのデータがキー/バリューのペアになっているので、Keyプロパティが目的のファイルと同じパス文字列かをチェックし、そうであればValueプロパティがStreamクラスのデータなので、StreamReaderと合わせてReadToEnd()で中身のテキストを取得できます。

全然お手軽ではないですね。

ビルドアクションに埋め込みリソースを指定して、ここの手順でファイルを扱ったほうが楽な気がしました。

おわりに

なんだか思ってたんと違う感じになってしまいましたが、ビルドアクションにResourceを設定したファイルへのアクセス方法の一つはわかりました。 しかし、これだけ面倒ということは、まず間違いなく僕のアプローチが間違っているはずです。 正しい方法があれば、それを書きたいと思います。

今回使ったプロジェクトはこちら