shift-jis と utf-8 の混在問題に関する記事(リンクリスト)に戻る
C# による UWP (Universal Windows Platform) アプリの場合、SDIに特化している為、ファイルIOや通信など待機時間が発生する処理は、処理が終了するまで待つことがないように、マルチスレッドで非同期処理をするように作られている。
ファイルIOも読み込み書き込みが完了する前に、呼び出し元にCPUタイムが返される。
その為、ファイルIO系のAPIも他の FileStream 系APIとは異なるモノを使用する。
主なファイル操作は、StorageFolder , StorageFile , FileIO で操作する。
それぞれ非同期処理に対応しており、StreamReader, StreamWriter 等とは扱い方も異なる。
ここでUWP非同期API の解説は出来ないが、StorageFile , FileIO を用いたファイルIOと、BOMを含むテキストファイルの読み書きのサンプルソースコードを掲載する。
所詮サンプルソースコードなのでコードは美しくないし、処理が単純なので例外処理なども組み込んでいない。
あくまで参考資料でしかない。
そのままで、実用に耐えるコードではないので、その点はご了承ください。
実用に耐えるコードはブログ一ページで表せるものではないので、GitHub等で探してください。
これまでもこのブログでいくつかのサンプルソースコードを掲載したが、実用的なコードにするにはもっと長いコードが必要なので、本格的なコードを書くときはGitHub等で公開します。
今回はUWPなので画面レイアウトも含み、やや長いコードになる。
サンプルコードの機能
画面上部に「Open and Read File」ボタンと「Write File」ボタンがあり、間に読み込んだファイル名を表示するTextBlockがある。
「Open and Read File」ボタンをクリックするとファイルダイアログが開き、特定ファイルを選択できる。
選択したファイルは下の 大きな複数行表示の TextBox に表示する。
この TextBox のテキストを編集して「Write File」ボタンをクリックすると、その内容を同一ファイルに上書きする。
ようするに簡単なテキストエディタである。
対応する文字エンコーディングは、shift-jis とBOM有りの utf-8, utf-16LE, utf-16BE, utf-32LE, utf-32BE である。
BOMなし utf には対応していない。
BOMなしは全て shift-jis と見なします。
ビルドの仕方
ビルドには Visual Studio 2019 を使用する。
「新しいプロジェクトの作成」で「空白のアプリ(ユニバーサルWindows)」を選び、プロジェクトを新規作成する。
プロジェクト名は「UWPReadWriteBOM」とし、.NET のバージョンなどは適当にデフォルトで作成する。
そこへ以下に掲載するサンプルソースをコピーしてビルドし、実行すれば動作する(はず)。
ソースは全て「MainPage.xaml」と「MainPage.xaml.cs」に記載している。
実用的ではないが、簡単にするためだ。
例外処理も書いていない。
サンプルコード
以下が全てのサンプルコードになる。
[MainPage.xaml]
<Page
x:Class="UWPReadWriteBOM.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UWPReadWriteBOM"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="6*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Name="FileOpenButton" Content="Open and Read File" Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" Click="FileOpenButton_Click" />
<TextBlock Name="FileName" Grid.Row="0" Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<Button Name="FileWriteButton" Content="Write File" Grid.Row="0" Grid.Column="2"
HorizontalAlignment="Right" VerticalAlignment="Center" Click="FileWriteButton_Click" />
<TextBox Name="TextFileBox" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
IsReadOnly="False" AcceptsReturn="True" TextWrapping="NoWrap" />
</Grid>
</Page>
[MainPage.xaml.cs]
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.Storage;
using Windows.Storage.Pickers;
using System.Text;
using Windows.Security.Cryptography;
using Windows.Storage.Streams;
namespace UWPReadWriteBOM
{
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
private FileOpenPicker picker = null;
private StorageFile file = null;
private Encoding enc = null;
private int codepage = 0;
private int bomLen = 0;
private byte[] bomUTF8 = { 0xEF, 0xBB, 0xBF };
private byte[] bomUTF16Little = { 0xFF, 0xFE };
private byte[] bomUTF16Big = { 0xFE, 0xFF };
private byte[] bomUTF32Little = { 0xFF, 0xFE, 0x00, 0x00 };
private byte[] bomUTF32Big = { 0x00, 0x00, 0xFE, 0xFF };
private async void FileOpenButton_Click(object sender, RoutedEventArgs e)
{
picker = new FileOpenPicker();
picker.FileTypeFilter.Add(".txt");
picker.ViewMode = PickerViewMode.List;
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
file = await picker.PickSingleFileAsync();
if (file == null) return;
this.FileName.Text = file.Path;
var buff = await FileIO.ReadBufferAsync(file);
byte[] byteBuff = null;
if (buff.Length > 0)
{
byteBuff = buff?.ToArray();
}
else
{
return;
}
if (IsBOM(byteBuff, out codepage, out bomLen) == false)
{
//shift-jis
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
byte[] byteWriteBuff = new byte[byteBuff.Length + bomLen];
Array.Copy(byteBuff, bomLen, byteWriteBuff, 0, byteBuff.Length - bomLen);
enc = System.Text.Encoding.GetEncoding(codepage);
string text = enc.GetString(byteWriteBuff);
this.TextFileBox.Text = text;
}
private async void FileWriteButton_Click(object sender, RoutedEventArgs e)
{
if (picker == null || file == null) return;
string text = this.TextFileBox.Text;
byte[] byteBuff = enc.GetBytes(text);
byte[] byteWriteBuff = new byte[byteBuff.Length + bomLen];
Array.Copy(byteBuff, 0, byteWriteBuff, bomLen, byteBuff.Length);
byte[] bom;
switch (codepage)
{
case 65001:
bom = bomUTF8;
break;
case 12000:
bom = bomUTF32Little;
break;
case 12001:
bom = bomUTF32Big;
break;
case 1200:
bom = bomUTF16Little;
break;
case 1201:
bom = bomUTF16Big;
break;
case 932:
bom = null;
break;
default:
bom = null;
break;
}
if(bom != null)
{
Array.Copy(bom, 0, byteWriteBuff, 0, bomLen);
}
IBuffer wbuff = CryptographicBuffer.CreateFromByteArray(byteWriteBuff);
await FileIO.WriteBufferAsync(file, wbuff);
}
private bool IsBOM(byte[] bomByte, out int codepage, out int bomLen)
{
bool result;
if(bomByte == null)
{
result = false;
codepage = 0;
bomLen = 0;
}
else if (bomByte.Length >= 3 && IsMatched(bomByte, bomUTF8))
{
result = true;
codepage = 65001; //utf-8,Unicode (UTF-8)
bomLen = 3;
}
else if (bomByte.Length >= 4 && IsMatched(bomByte, bomUTF32Little))
{
result = true;
codepage = 12000; //utf-32,Unicode (UTF-32)
bomLen = 4;
}
else if (bomByte.Length >= 4 && IsMatched(bomByte, bomUTF32Big))
{
result = true;
codepage = 12001; //utf-32BE,Unicode (UTF-32 Big-Endian)
bomLen = 4;
}
else if (bomByte.Length >= 2 && IsMatched(bomByte, bomUTF16Little))
{
result = true;
codepage = 1200; //utf-16,Unicode
bomLen = 2;
}
else if (bomByte.Length >= 2 && IsMatched(bomByte, bomUTF16Big))
{
result = true;
codepage = 1201; //utf-16BE,Unicode (Big-Endian)
bomLen = 2;
}
else
{
result = false;
//codepage = 0; //non BOM !
codepage = 932; //shift_jis,Japanese (Shift-JIS)
bomLen = 0;
}
return result;
}
private bool IsMatched(byte[] data, byte[] bom)
{
bool result = true;
for (int i = 0; i < bom.Length; i++)
{
if (bom[i] != data[i])
result = false;
}
return result;
}
}
}
コード解説
処理は「FileOpenButton_Click」のファイル読み込みと「FileWriteButton_Click」の二つだけ。
他の関数はBOM判定をしているだけである。
この処理に使用しているファイル関連クラスは FileOpenPicker と StorageFile だけだ。
一度読み込んだインスタンスを保持して書き込みに使用する為、プロパティに保持している。
private FileOpenPicker picker = null;
private StorageFile file = null;
また、BOM判定に必要なプロパティも読み取りと書き込みの両方で使用するため、プロパティに保持している。
private Encoding enc = null;
private int codepage = 0;
private int bomLen = 0;
private byte[] bomUTF8 = { 0xEF, 0xBB, 0xBF };
private byte[] bomUTF16Little = { 0xFF, 0xFE };
private byte[] bomUTF16Big = { 0xFE, 0xFF };
private byte[] bomUTF32Little = { 0xFF, 0xFE, 0x00, 0x00 };
private byte[] bomUTF32Big = { 0x00, 0x00, 0xFE, 0xFF };
読み込み処理
FileOpenButton_Click は、最初に「FileOpenPicker」でファイルダイアログを開いて、ユーザーに読み込みファイルを選択させる。
PickSingleFileAsync() でファイルの情報を非同期で取り込み、取り込めなければ終了する。
「this.FileName.Text = file.Path;」は取り込んだファイルのパスを表示している。
picker = new FileOpenPicker();
picker.FileTypeFilter.Add(".txt");
picker.ViewMode = PickerViewMode.List;
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
file = await picker.PickSingleFileAsync();
if (file == null) return;
this.FileName.Text = file.Path;
次にファイルの内容を非同期で読み込む。
読み込みはテキスト型ではなくバイナリ型で読み込む。
読み込みは「IBuffer」型で読み込むので、後で編集しやすいように「byte」型の配列にコピーしている。
var buff = await FileIO.ReadBufferAsync(file);
byte[] byteBuff = null;
if (buff.Length > 0)
{
byteBuff = buff?.ToArray();
}
else
{
return;
}
「byte」型の配列を参照してBOM判定を行い、表示用の「byteWriteBuff」にBOMを除いてコピーする。
UWP はデフォルトでは shift-jis をサポートしていないので、「System.Text.Encoding.RegisterProvider」で shift-jis の使用を宣言する。
if (IsBOM(byteBuff, out codepage, out bomLen) == false)
{
//shift-jis
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
byte[] byteWriteBuff = new byte[byteBuff.Length + bomLen];
Array.Copy(byteBuff, bomLen, byteWriteBuff, 0, byteBuff.Length - bomLen);
読み込んだデータはバイナリなので、そのままでは TextBox に表示できない。
BOM判定の結果分かった文字エンコーディングで、テキスト文字列にエンコーディングする。
enc = System.Text.Encoding.GetEncoding(codepage);
string text = enc.GetString(byteWriteBuff);
最後に下の大きな TextBox に表示する。
this.TextFileBox.Text = text;
書き込み処理
FileWriteButton_Click では TextBox の内容を、読み込んだファイルに上書きする。
読み込まれていなければ終了する。
enc.GetBytes(text); でbyte型配列に変換し、byteWriteBuff にコピーする際に先頭へBOMを上書きする。
BOMは読み込み時と同じものを書く。
if (picker == null || file == null) return;
string text = this.TextFileBox.Text;
byte[] byteBuff = enc.GetBytes(text);
byte[] byteWriteBuff = new byte[byteBuff.Length + bomLen];
Array.Copy(byteBuff, 0, byteWriteBuff, bomLen, byteBuff.Length);
byte[] bom;
switch (codepage)
{
case 65001:
bom = bomUTF8;
break;
case 12000:
bom = bomUTF32Little;
break;
case 12001:
bom = bomUTF32Big;
break;
case 1200:
bom = bomUTF16Little;
break;
case 1201:
bom = bomUTF16Big;
break;
case 932:
bom = null;
break;
default:
bom = null;
break;
}
if(bom != null)
{
Array.Copy(bom, 0, byteWriteBuff, 0, bomLen);
}
CryptographicBuffer.CreateFromByteArray で、byte型配列を IByffer 型へ変換し、
FileIO.WriteBufferAsync でデータをファイルへ上書きする。
これも非同期処理である。
IBuffer wbuff = CryptographicBuffer.CreateFromByteArray(byteWriteBuff);
await FileIO.WriteBufferAsync(file, wbuff);
FileIO はテキストでも読み書きできるが、UTFにしか対応していないので、shift-jis に対応するにはバイナリで読み書きする必要がある。
複数の文字エンコーディングに対応する為にも、バイナリで読み書きする必要がある。
実際にアプリ開発する場合は、もっと綺麗なコードにする必要があるだろうが、それにしても動くサンプルコードがあった方が書きやすいと思う。
クラスライブラリやAPIの使い方を調べるのは、それなりに手間が掛かる。
このコードがお役に立てば幸いである。
shift-jis と utf-8 の混在問題に関する記事(リンクリスト)に戻る
参考資料
StorageFolder Class
StorageFile Class
FileIO Class
FolderPicker Class
FileOpenPicker Class
ファイルの作成、書き込み、および読み取り
ピッカーでファイルやフォルダーを開く