[UWP] テキストファイルの読み書き

RaspberryPi3に入れたWindows10 IoT Core上で動作するアプリを作成中なんだけど色々と判らないことが多い。
そもそもIoT Core以前にUWPアプリ(Universal Windows Platform アプリ)についての知識が全然ない。
しかもIoT Core上だと使えないUWPのAPIがたくさんある。

  • Universal APIs not functional in Windows10 IoT Core at this time

https://developer.microsoft.com/en-us/windows/iot/docs/unavailableapis

今回USBメモリをRaspberryPi3に挿してログファイルを保存したり、設定ファイルを読み書きしたりしてみたのでメモ。
開発環境は、Visual Studio 2015 Update3

PCの場合 (Windows10)

IoT Coreの前にまずは普通のWindows10の場合。保存先のフォルダを選択するにはFolderPicker(Windows.Storage.Pickers.FolderPicker)が使える。
これは、.NET FrameworkのFolderBrowserDialog(System.Windows.Forms.FolderBrowserDialog)みたいなものかな。
これを使えば簡単にフォルダ選択が出来る。

private async void btnFolderPicker_Click(object sender, RoutedEventArgs e)
{
	FolderPicker fp = new FolderPicker();
	fp.SuggestedStartLocation = PickerLocationId.ComputerFolder;
	fp.FileTypeFilter.Add("*");
	StorageFolder sf = await fp.PickSingleFolderAsync();
	if (sf != null)
	{
		//----- 書込み時に使う為にStorageFolderを保持
		m_storageFolder = sf;
		//----- これをやっとかないとアクセスできない
		Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.AddOrReplace("PikcedFolder", m_storageFolder);
	}
}

要はアクセスしたいフォルダのStorageFolderを取得すればいい。
で、ファイルを書き込むには

private async Task WriteFile()
{
	if (m_storageFolder == null)
	{
		return;
	}
	//----- ファイル名
	DateTime dt = DateTime.Now;
	string strFilename = string.Format("{0}.txt", dt.ToString("yyyyMMdd-HHmmssfff"));
	//----- ファイルを作成
	StorageFile sf = await m_storageFolder.CreateFileAsync(strFilename, CreationCollisionOption.ReplaceExisting);
	//----- ストリームを取得
	var stream = await sf.OpenAsync(FileAccessMode.ReadWrite);
	//----- 出力ストリームを取得
	using (var outputStream = stream.GetOutputStreamAt(0))
	{
		//----- データライタを取得
		using (var dataWriter = new Windows.Storage.Streams.DataWriter(outputStream))
		{
			dataWriter.WriteString("ABCDEFG\r\n");
			await dataWriter.StoreAsync();
			await outputStream.FlushAsync();
		}
	}
	stream.Dispose();
}

とやればOK。これはほぼMicrosoftのサンプルのまま。
(https://docs.microsoft.com/ja-jp/windows/uwp/files/quickstart-reading-and-writing-files)

RaspberryPi3の場合 (Windows10 IoT Core)

IoT Coreの場合もPCと同じ様に、保存したいフォルダのStorageFolderを取得出来れば、書込み自体は全く同じ。
ただ残念ながらFolderPickerが使えない。
しょうがないから自分で何とかしなきゃいけないんだけど、まず始めにアプリマニフェストを編集する必要がある。
今回はUSBメモリを使うからPackage.appxmanifestを開いてCapabilitiesタブを選び、Removable Storageにチェックをする。
これをやるとXMLにはPackageタグ内に下の内容が追加される。

<Capabilities>
  <uap:Capability Name="removableStorage" />
</Capabilities>

それから保存するファイルのファイルタイプを登録しないといけない。
マニフェストのDeclarationsタブを選んで、Available Declarations:からFile Type Associationsを選択してAdd。
右側に色々と項目が出てくるけど、必須なのは赤い×印のとこ。(NameとFile Type)
テキストファイルを保存したいから、
Name: text
File type: .txt
とする。
これをやるとXMLにはApplicationタグ内に下の内容が追加される。

<Extensions>
  <uap:Extension Category="windows.fileTypeAssociation">
    <uap:FileTypeAssociation Name="text">
      <uap:SupportedFileTypes>
        <uap:FileType>.txt</uap:FileType>
      </uap:SupportedFileTypes>
    </uap:FileTypeAssociation>
  </uap:Extension>
</Extensions>

これでとりあえず、準備は完了。後はコード。
USBメモリを使いたいからリムーバブルメディアの一覧を取得する。

	//----- 接続されてるリムーバブルメディアの一覧を配下に持つStorageFolderを取得
	// デスクトップの「PC」を開いて並んでるドライブがリムーバブルディスクだけになってる感じ
	StorageFolder sf = Windows.Storage.KnownFolders.RemovableDevices;
	//----- リムーバブルメディアの一覧を取得
	IReadOnlyList<StorageFolder> listSf = await sf.GetFoldersAsync();

上のコードで対象のUSBメモリを含む一覧が取得できたから、後はStorageFolderのNameとかDisplayNameプロパティを見て使いたいドライブを選べばOK。
更にドライブ内のフォルダを選択するには

	//----- 例えばリストの2番目が使いたいUSBメモリの場合
	StorageFolder sfUsb = listSf[1];
	//----- USBメモリルートのフォルダ一覧を取得
	IReadOnlyList<StorageFolder> listUsb = await sfUsb.GetFoldersAsync();

とやればいい。同様に更にフォルダ階層をもぐっていくなら

	//----- リストの3番目のフォルダを選択する場合
	StorageFolder sfNext = listUsb[2];
	//----- その下のフォルダ一覧
	listUsb = await sfNext.GetFoldersAsync();

と繰り返していく。で、ファイル書込みをしたいフォルダにたどり着いたら

	//----- リストの2番目のフォルダを保存フォルダにしたい場合
	//----- 対象のStorageFolderを保持
	m_storageFolder = listUsb[1];
	//----- FolderPickerの時と同様、これをやっとかないとアクセスできない
	Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.AddOrReplace("PikcedFolder", m_storageFolder);

これで選択完了。ファイル書込みは上のPCの場合で書いたのと同じでいける。

選択方法が判ったので、適当なUIを作ってみる。画面(ページ)上には

  • ListBox lbFolder
  • Button btnSet
  • Button btnWrite

があるだけ。

namespace XXXXXXXX
{
	public sealed partial class TestPage : Page
	{
		private StorageFolder m_storageFolder;
		private Stack<StorageFolder> m_stackStorageFolder;
		private IReadOnlyList<StorageFolder> m_listStorageFolder;
		
		public TestPage()
		{
			this.InitializeComponent();
			m_storageFolder = null;
			m_stackStorageFolder = new Stack<StorageFolder>();
			m_listStorageFolder = null;
		}
		//----- ページロードイベントハンドラ
		// リムーバブルメディアの一覧を表示する
		private async void Page_Loaded(object sender, RoutedEventArgs e)
		{
			m_storageFolder = Windows.Storage.KnownFolders.RemovableDevices;
			m_stackStorageFolder.Clear();
			m_listStorageFolder = await m_storageFolder.GetFoldersAsync();
			DispFolders();
		}

		//----- リストボックスダブルタップイベントハンドラ
		private async void lbData_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
		{
			int iSelectedIndex = lbData.SelectedIndex;
			if (iSelectedIndex == -1)
			{
				return;
			}
			//----- リストの先頭がダブルタップされた場合、1つ上のフォルダへ移動
			// (リストの先頭には現在のカレントフォルダ名が表示されている。".."とか"↑"の方がいいかも)
			if (iSelectedIndex == 0)
			{
				await UpFolder();
			}
			//----- リストの2番目以降がダブルタップされた場合、選択されたフォルダへ入る
			else
			{
				await DownFolder(iSelectedIndex - 1);
			}
			//----- リスト表示更新
			DispFolders();
		}
		
		//----- 1つ上のフォルダへ移動
		private async Task UpFolder()
		{
			if (m_stackStorageFolder.Count == 0)
			{
				//----- トップ(PC)にいるから上に行けない
				return;
			}
			m_storageFolder = m_stackStorageFolder.Pop();
			m_listStorageFolder = await m_storageFolder.GetFoldersAsync();
		}
		
		//----- 選択されたフォルダへ移動
		private async Task DownFolder(int iFolderIndex)
		{
			//----- 今いるとこをスタックにPush
			m_stackStorageFolder.Push(m_storageFolder);
			//----- 選択されたStorageFolderを取得
			m_storageFolder = m_listStorageFolder[iFolderIndex];
			//----- 選択されたStorageFolder下のリストを取得
			m_listStorageFolder = await m_storageFolder.GetFoldersAsync();
		}
		
		//----- リスト表示更新
		private void DispFolders()
		{
			lbData.Items.Clear();
			if (m_stackStorageFolder.Count == 0)
			{
				//----- トップにいる場合、リスト先頭は[PC]にする
				lbData.Items.Add("[PC]");
			}
			else
			{
				//----- トップ以外の場合、カレントフォルダ名を表示
				lbData.Items.Add(string.Format("[{0}]", m_storageFolder.Name));
			}
			//----- 配下のフォルダを表示
			for (int iIndex = 0; iIndex < m_listStorageFolder.Count; iIndex++)
			{
				StorageFolder sf = m_listStorageFolder[iIndex];
				if (m_stackStorageFolder.Count == 0)
				{
					//----- カレントがトップの場合、DisplayNameも表示
					// DisplayNameはボリュームラベルになってるから判りやすい
					// Nameはドライブレター
					lbData.Items.Add(string.Format(" - [{0} / {1}]", sf.DisplayName, sf.Name));
				}
				else
				{
					//----- フォルダ名を表示
					lbData.Items.Add(string.Format(" - [{0}]", sf.Name));
				}
			}
		}

		//----- Setボタンクリックイベントハンドラ
		private void btnSetFolder_Click(object sender, RoutedEventArgs e)
		{
			//----- 現在選択中のフォルダを書込み対象として登録する
			Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.AddOrReplace("PikcedFolder", m_storageFolder);
		}

		//----- Writeボタンクリックイベントハンドラ
		private async void btnWrite_Click(object sender, RoutedEventArgs e)
		{
			await WriteFile();
		}

		//----- テキストファイル書込み
		private async Task WriteFile()
		{
			if (m_storageFolder == null)
			{
				//----- フォルダが選択されてない
				return;
			}
			//----- ファイル名
			DateTime dt = DateTime.Now;
			string strFilename = string.Format("{0}.txt", dt.ToString("yyyyMMdd-HHmmssfff"));
			//----- ファイルを作成
			StorageFile sf = await m_storageFolder.CreateFileAsync(strFilename, CreationCollisionOption.ReplaceExisting);
			//----- ストリームを取得
			var stream = await sf.OpenAsync(FileAccessMode.ReadWrite);
			//----- 出力ストリームを取得
			using (var outputStream = stream.GetOutputStreamAt(0))
			{
				//----- データライタを取得
				using (var dataWriter = new Windows.Storage.Streams.DataWriter(outputStream))
				{
					dataWriter.WriteString("ABCDEFG\r\n");
					await dataWriter.StoreAsync();
					await outputStream.FlushAsync();
				}
			}
			stream.Dispose();
		}
	}
}

ま、細かいところは色々あるが(主にエラー処理)、とりあえずこんな感じで何とかなりそう。