<Window x:Class="TabToJson.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="タブ区切り → JSON 変換" Height="600" Width="1000"
MinHeight="400" MinWidth="700">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- ヘッダー -->
<TextBlock Grid.Row="0" Text="タブ区切りテキスト → JSON 変換ツール"
FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
<!-- メインパネル -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 入力パネル -->
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="入力(タブ区切りテキスト)" FontWeight="Bold" Margin="0,0,0,4"/>
<TextBox Grid.Row="1" x:Name="InputTextBox"
AcceptsReturn="True" AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
FontFamily="Consolas" FontSize="12"
TextWrapping="NoWrap"
Padding="4"
TextChanged="InputTextBox_TextChanged"
AllowDrop="True"
Drop="InputTextBox_Drop"
PreviewDragOver="InputTextBox_PreviewDragOver"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock x:Name="InputStatusText" FontSize="11" Foreground="Gray" VerticalAlignment="Center"/>
<TextBlock Text="文字コード:" Margin="8,0,4,0" VerticalAlignment="Center" FontSize="11"/>
<ComboBox x:Name="EncodingComboBox" Width="130" VerticalAlignment="Center" FontSize="11"
SelectionChanged="EncodingComboBox_SelectionChanged"/>
<Button Content="ファイルを開く..." Margin="8,0,0,0" Padding="8,2" Click="OpenFile_Click"/>
<Button Content="貼り付け" Margin="4,0,0,0" Padding="8,2" Click="PasteInput_Click"/>
<Button Content="クリア" Margin="4,0,0,0" Padding="8,2" Click="ClearInput_Click"/>
</StackPanel>
</Grid>
<!-- 中央ボタン -->
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="10,0">
<Button Content="→ 変換" Padding="10,8" Click="Convert_Click"
FontWeight="Bold" FontSize="13"/>
</StackPanel>
<!-- 出力パネル -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="出力(JSON)" FontWeight="Bold" VerticalAlignment="Center"/>
<CheckBox x:Name="PrettyPrintCheck" Content="整形出力" IsChecked="True"
Margin="12,0,0,0" VerticalAlignment="Center"/>
<CheckBox x:Name="ArrayWrapCheck" Content="配列形式" IsChecked="True"
Margin="8,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<TextBox Grid.Row="1" x:Name="OutputTextBox"
AcceptsReturn="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
FontFamily="Consolas" FontSize="12"
TextWrapping="NoWrap"
IsReadOnly="True"
Padding="4"
Background="#F9F9F9"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock x:Name="OutputStatusText" FontSize="11" Foreground="Gray" VerticalAlignment="Center"/>
<Button Content="コピー" Margin="8,0,0,0" Padding="8,2" Click="CopyOutput_Click"/>
<Button Content="保存..." Margin="4,0,0,0" Padding="8,2" Click="SaveOutput_Click"/>
</StackPanel>
</Grid>
</Grid>
<!-- エラーメッセージ -->
<TextBlock Grid.Row="2" x:Name="ErrorText" Foreground="Red"
FontSize="11" Margin="0,6,0,0" TextWrapping="Wrap"
Visibility="Collapsed"/>
<!-- フッター -->
<StackPanel Grid.Row="3" Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock x:Name="FilePathText" FontSize="10" Foreground="Gray" VerticalAlignment="Center"/>
<TextBlock FontSize="10" Foreground="Gray" VerticalAlignment="Center"
Text="1行目をフィールド名として使用します。空セルは null として出力されます。 ファイルのドラッグ&ドロップにも対応しています。"/>
</StackPanel>
</Grid>
</Window>
using System.IO;
using System.Text;
using System.Text.Json;
using System.Windows;
using Microsoft.Win32;
namespace TabToJson;
public partial class MainWindow : Window
{
private record EncodingItem(string Label, int CodePage);
// 最後に読み込んだファイルのパス(エンコード変更時に再読み込みするため)
private string? _lastLoadedPath;
public MainWindow()
{
InitializeComponent();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
InitEncodingComboBox();
}
private void InitEncodingComboBox()
{
var items = new[]
{
new EncodingItem("Shift-JIS (CP932)", 932),
new EncodingItem("UTF-8", 65001),
new EncodingItem("UTF-8 BOM付き", -1), // 特殊扱い
new EncodingItem("UTF-16 LE", 1200),
new EncodingItem("UTF-16 BE", 1201),
new EncodingItem("EUC-JP", 51932),
new EncodingItem("自動判別", 0),
};
EncodingComboBox.ItemsSource = items;
EncodingComboBox.DisplayMemberPath = nameof(EncodingItem.Label);
EncodingComboBox.SelectedIndex = 0; // デフォルト: Shift-JIS
}
private Encoding GetSelectedEncoding()
{
if (EncodingComboBox.SelectedItem is not EncodingItem item)
return Encoding.GetEncoding(932);
return item.CodePage switch
{
0 => Encoding.GetEncoding(932), // 自動判別はLoadFile内で処理
-1 => new UTF8Encoding(encoderShouldEmitUTF8Identifier: true),
_ => Encoding.GetEncoding(item.CodePage)
};
}
private bool IsAutoDetect =>
EncodingComboBox.SelectedItem is EncodingItem { CodePage: 0 };
private void EncodingComboBox_SelectionChanged(object sender,
System.Windows.Controls.SelectionChangedEventArgs e)
{
// ファイルが読み込まれていれば選択変更時に再読み込み
if (_lastLoadedPath is not null)
LoadFile(_lastLoadedPath);
}
private void InputTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
UpdateInputStatus();
ErrorText.Visibility = Visibility.Collapsed;
}
private void UpdateInputStatus()
{
var lines = InputTextBox.Text.Split('\n', StringSplitOptions.None);
var nonEmpty = lines.Count(l => l.Trim().Length > 0);
int dataRows = nonEmpty > 1 ? nonEmpty - 1 : 0;
InputStatusText.Text = $"{nonEmpty} 行 (データ: {dataRows} 件)";
}
private void Convert_Click(object sender, RoutedEventArgs e)
{
ErrorText.Visibility = Visibility.Collapsed;
OutputTextBox.Text = string.Empty;
var input = InputTextBox.Text;
if (string.IsNullOrWhiteSpace(input))
{
ShowError("入力が空です。タブ区切りテキストを入力してください。");
return;
}
try
{
var json = ConvertToJson(input,
prettyPrint: PrettyPrintCheck.IsChecked == true,
asArray: ArrayWrapCheck.IsChecked == true);
OutputTextBox.Text = json;
OutputStatusText.Text = $"{json.Length} 文字";
}
catch (Exception ex)
{
ShowError($"変換エラー: {ex.Message}");
}
}
private static string ConvertToJson(string input, bool prettyPrint, bool asArray)
{
// 行分割(CRLF / LF 両対応)
var lines = input.Split('\n')
.Select(l => l.TrimEnd('\r'))
.ToArray();
// 空行を除去
var nonEmptyLines = lines
.Select((line, i) => (line, i))
.Where(x => x.line.Length > 0)
.ToList();
if (nonEmptyLines.Count == 0)
throw new InvalidOperationException("有効な行がありません。");
// 1行目をヘッダーとして取得
var headers = nonEmptyLines[0].line.Split('\t');
if (headers.Length == 0)
throw new InvalidOperationException("ヘッダー行にタブ区切りデータが見つかりません。");
var options = new JsonWriterOptions
{
Indented = prettyPrint,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);
if (asArray)
writer.WriteStartArray();
foreach (var (line, _) in nonEmptyLines.Skip(1))
{
var values = line.Split('\t');
writer.WriteStartObject();
for (int i = 0; i < headers.Length; i++)
{
var key = headers[i].Trim();
if (string.IsNullOrEmpty(key))
key = $"field{i + 1}";
var value = i < values.Length ? values[i] : null;
writer.WritePropertyName(key);
if (string.IsNullOrEmpty(value))
writer.WriteNullValue();
else if (long.TryParse(value, out var longVal))
writer.WriteNumberValue(longVal);
else if (double.TryParse(value, System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var doubleVal))
writer.WriteNumberValue(doubleVal);
else if (bool.TryParse(value, out var boolVal))
writer.WriteBooleanValue(boolVal);
else
writer.WriteStringValue(value);
}
writer.WriteEndObject();
}
if (asArray)
writer.WriteEndArray();
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}
private void ClearInput_Click(object sender, RoutedEventArgs e)
{
InputTextBox.Text = string.Empty;
OutputTextBox.Text = string.Empty;
OutputStatusText.Text = string.Empty;
InputStatusText.Text = string.Empty;
FilePathText.Text = string.Empty;
_lastLoadedPath = null;
ErrorText.Visibility = Visibility.Collapsed;
}
private void OpenFile_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Title = "タブ区切りファイルを開く",
Filter = "テキストファイル (*.txt;*.tsv;*.tab;*.csv)|*.txt;*.tsv;*.tab;*.csv|すべてのファイル (*.*)|*.*"
};
if (dialog.ShowDialog() == true)
LoadFile(dialog.FileName);
}
private void InputTextBox_PreviewDragOver(object sender, DragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
? DragDropEffects.Copy
: DragDropEffects.None;
e.Handled = true;
}
private void InputTextBox_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetData(DataFormats.FileDrop) is string[] files && files.Length > 0)
LoadFile(files[0]);
}
private void LoadFile(string path)
{
try
{
var encoding = IsAutoDetect ? DetectEncoding(path) : GetSelectedEncoding();
InputTextBox.Text = File.ReadAllText(path, encoding);
_lastLoadedPath = path;
FilePathText.Text = $"{Path.GetFileName(path)} [{encoding.WebName}] ";
UpdateInputStatus();
ErrorText.Visibility = Visibility.Collapsed;
}
catch (Exception ex)
{
ShowError($"ファイル読み込みエラー: {ex.Message}");
}
}
private static Encoding DetectEncoding(string path)
{
// BOMチェック
using var fs = File.OpenRead(path);
var bom = new byte[4];
_ = fs.Read(bom, 0, 4);
if (bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF) return Encoding.UTF8;
if (bom[0] == 0xFF && bom[1] == 0xFE) return Encoding.Unicode;
if (bom[0] == 0xFE && bom[1] == 0xFF) return Encoding.BigEndianUnicode;
// BOMなし → UTF-8 として試みる(失敗時は Shift-JIS フォールバック)
try
{
var bytes = File.ReadAllBytes(path);
var utf8 = new UTF8Encoding(false, throwOnInvalidBytes: true);
utf8.GetString(bytes);
return utf8;
}
catch
{
return Encoding.GetEncoding(932);
}
}
private void PasteInput_Click(object sender, RoutedEventArgs e)
{
if (Clipboard.ContainsText())
{
InputTextBox.Text = Clipboard.GetText();
FilePathText.Text = string.Empty;
UpdateInputStatus();
}
}
private void CopyOutput_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(OutputTextBox.Text))
{
Clipboard.SetText(OutputTextBox.Text);
OutputStatusText.Text = "コピーしました";
}
}
private void SaveOutput_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(OutputTextBox.Text))
{
ShowError("保存するJSONがありません。先に変換を実行してください。");
return;
}
var dialog = new SaveFileDialog
{
Title = "JSONファイルを保存",
Filter = "JSON ファイル (*.json)|*.json|すべてのファイル (*.*)|*.*",
DefaultExt = "json",
FileName = "output.json"
};
if (dialog.ShowDialog() == true)
{
File.WriteAllText(dialog.FileName, OutputTextBox.Text, Encoding.UTF8);
OutputStatusText.Text = $"保存しました: {Path.GetFileName(dialog.FileName)}";
}
}
private void ShowError(string message)
{
ErrorText.Text = message;
ErrorText.Visibility = Visibility.Visible;
}
}