<Window x:Class="BenriTool.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BenriTool" Height="720" Width="880"
MinHeight="640" MinWidth="760"
Background="#F0F2F5"
FontFamily="Segoe UI" FontSize="13">
<Window.Resources>
<!-- Brushes -->
<SolidColorBrush x:Key="PrimaryBrush" Color="#1976D2"/>
<SolidColorBrush x:Key="SuccessBrush" Color="#388E3C"/>
<SolidColorBrush x:Key="FieldBorderBrush" Color="#DADCE0"/>
<!-- Card -->
<Style x:Key="CardStyle" TargetType="Border">
<Setter Property="Background" Value="White"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="18,14"/>
<Setter Property="Margin" Value="2,2,2,12"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect Color="#000000" Opacity="0.07"
BlurRadius="10" ShadowDepth="2" Direction="270"/>
</Setter.Value>
</Setter>
</Style>
<!-- TextBox (input) -->
<Style TargetType="TextBox">
<Setter Property="Height" Value="32"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="8,0"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Foreground" Value="#212121"/>
<Setter Property="BorderBrush" Value="#B0B8C4"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Cursor" Value="IBeam"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4" SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Background" Value="#EFEFEF"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="#D4D4D4"/>
<Setter Property="Foreground" Value="#AAAAAA"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
<Trigger Property="IsFocused" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="#1976D2"/>
<Setter TargetName="Bd" Property="BorderThickness" Value="1.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Log TextBox (dark terminal) -->
<Style x:Key="LogStyle" TargetType="TextBox">
<Setter Property="Background" Value="#1E1E1E"/>
<Setter Property="Foreground" Value="#D4D4D4"/>
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"
VerticalScrollBarVisibility="Auto"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ComboBox (basic modern) -->
<Style TargetType="ComboBox">
<Setter Property="Height" Value="32"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="8,0"/>
<Setter Property="Background" Value="#FAFAFA"/>
<Setter Property="BorderBrush" Value="{StaticResource FieldBorderBrush}"/>
</Style>
<!-- Browse button (outline) -->
<Style x:Key="BrowseBtn" TargetType="Button">
<Setter Property="Height" Value="32"/>
<Setter Property="Padding" Value="12,0"/>
<Setter Property="MinWidth" Value="62"/>
<Setter Property="Foreground" Value="#1976D2"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="White" CornerRadius="4"
BorderBrush="#1976D2" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#E3F2FD"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#BBDEFB"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Execute button (green solid) -->
<Style x:Key="ExecBtn" TargetType="Button">
<Setter Property="Height" Value="36"/>
<Setter Property="MinWidth" Value="130"/>
<Setter Property="Padding" Value="22,0"/>
<Setter Property="Background" Value="#388E3C"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#2E7D32"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1B5E20"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- DryRun button (blue solid) -->
<Style x:Key="DryRunBtn" TargetType="Button">
<Setter Property="Height" Value="36"/>
<Setter Property="Padding" Value="18,0"/>
<Setter Property="Background" Value="#1976D2"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1565C0"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#0D47A1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- CheckBox (modern) -->
<Style TargetType="CheckBox">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Foreground" Value="#212121"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Border x:Name="Box" Width="16" Height="16" CornerRadius="3"
BorderBrush="{StaticResource FieldBorderBrush}"
BorderThickness="2" Background="White">
<Path x:Name="Check" Data="M 2,7 L 6,11 L 13,3"
Stroke="White" StrokeThickness="2"
StrokeStartLineCap="Round" StrokeEndLineCap="Round"
Visibility="Collapsed"/>
</Border>
<ContentPresenter Margin="7,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Box" Property="Background" Value="#1976D2"/>
<Setter TargetName="Box" Property="BorderBrush" Value="#1976D2"/>
<Setter TargetName="Check" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Box" Property="BorderBrush" Value="#1976D2"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- RadioButton (modern) -->
<Style TargetType="RadioButton">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Foreground" Value="#212121"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Grid Width="16" Height="16">
<Ellipse x:Name="Ring" Stroke="{StaticResource FieldBorderBrush}"
StrokeThickness="2" Fill="White"/>
<Ellipse x:Name="Dot" Width="8" Height="8"
Fill="#1976D2" Visibility="Collapsed"/>
</Grid>
<ContentPresenter Margin="7,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Ring" Property="Stroke" Value="#1976D2"/>
<Setter TargetName="Dot" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Ring" Property="Stroke" Value="#1976D2"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Code editor TextBox (multiline, editable) -->
<Style x:Key="CodeInputStyle" TargetType="TextBox">
<Setter Property="AcceptsReturn" Value="True"/>
<Setter Property="AcceptsTab" Value="True"/>
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Foreground" Value="#212121"/>
<Setter Property="Padding" Value="8"/>
<Setter Property="BorderBrush" Value="#B0B8C4"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border x:Name="Bd" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4" SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="#1976D2"/>
<Setter TargetName="Bd" Property="BorderThickness" Value="1.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Code editor TextBox (multiline, read-only output) -->
<Style x:Key="CodeOutputStyle" TargetType="TextBox">
<Setter Property="AcceptsReturn" Value="True"/>
<Setter Property="FontFamily" Value="Consolas"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="TextWrapping" Value="NoWrap"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="Background" Value="#F6F8FA"/>
<Setter Property="Foreground" Value="#212121"/>
<Setter Property="Padding" Value="8"/>
<Setter Property="BorderBrush" Value="#D0D7DE"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Cursor" Value="Arrow"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4" SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- TabControl (flat) -->
<Style TargetType="TabControl">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabControl">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="White"
BorderThickness="0,0,0,1" BorderBrush="#E0E0E0">
<TabPanel x:Name="HeaderPanel" IsItemsHost="True" Margin="8,0"/>
</Border>
<ContentPresenter Grid.Row="1" x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- TabItem (flat with underline) -->
<Style TargetType="TabItem">
<Setter Property="Foreground" Value="#757575"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Grid x:Name="Root" Cursor="Hand">
<Border x:Name="Bd" Background="Transparent" Padding="20,12">
<TextBlock x:Name="Txt" Text="{TemplateBinding Header}"
FontSize="13" Foreground="{TemplateBinding Foreground}"/>
</Border>
<Border x:Name="Bar" Height="2.5" VerticalAlignment="Bottom"
Background="#1976D2" Visibility="Collapsed"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bar" Property="Visibility" Value="Visible"/>
<Setter TargetName="Txt" Property="Foreground" Value="#1976D2"/>
<Setter TargetName="Txt" Property="FontWeight" Value="SemiBold"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter TargetName="Bd" Property="Background" Value="#F5F5F5"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<TabControl>
<!-- ===== Tab 1: ファイル改行 ===== -->
<TabItem Header="ファイル改行">
<ScrollViewer VerticalScrollBarVisibility="Auto" Background="#F0F2F5">
<StackPanel Margin="16,16,16,8">
<!-- 入力ファイル -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="入力ファイル" FontWeight="SemiBold" FontSize="13" Foreground="#212121"/>
</StackPanel>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="ファイル" Foreground="#555" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="TbLineBreakInput"/>
<Button Grid.Column="2" Content="参照..." Margin="8,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnLineBreakInputBrowse_Click"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="文字コード" Foreground="#555" VerticalAlignment="Center"/>
<ComboBox Grid.Column="1" x:Name="CbLineBreakEncoding" Width="250"/>
</Grid>
</StackPanel>
</Border>
<!-- 改行設定 -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="改行設定" FontWeight="SemiBold" FontSize="13" Foreground="#212121"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="バイト数" Foreground="#555" VerticalAlignment="Center" Width="100"/>
<TextBox x:Name="TbByteCount" Width="90" Text="80"/>
<TextBlock Text="バイトごとに改行を挿入" Foreground="#555"
VerticalAlignment="Center" Margin="12,0,0,0"/>
</StackPanel>
</StackPanel>
</Border>
<!-- 出力設定 -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="出力設定" FontWeight="SemiBold" FontSize="13" Foreground="#212121"/>
</StackPanel>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="出力フォルダ" Foreground="#555" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="TbLineBreakOutput"/>
<Button Grid.Column="2" Content="参照..." Margin="8,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnLineBreakOutputBrowse_Click"/>
</Grid>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<RadioButton x:Name="RbSameFolder" Content="同じフォルダに出力"
GroupName="OutputMode" IsChecked="True"
Checked="RbOutputMode_Checked"/>
<RadioButton x:Name="RbSpecifyFolder" Content="フォルダを指定"
GroupName="OutputMode" Margin="20,0,0,0"
Checked="RbOutputMode_Checked"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="ChkAddExtension" Content="拡張子を付加する"/>
<TextBlock Text="拡張子 :" Foreground="#555" VerticalAlignment="Center" Margin="14,0,8,0"/>
<TextBox x:Name="TbAddExtension" Width="100" Text=".out"
IsEnabled="{Binding IsChecked, ElementName=ChkAddExtension}"/>
</StackPanel>
</StackPanel>
</Border>
<!-- 実行ボタン -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="2,0,2,12">
<Button Content="実行" Style="{StaticResource ExecBtn}"
Click="BtnLineBreakExecute_Click"/>
</StackPanel>
<!-- ログ -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="ログ" FontWeight="SemiBold" FontSize="13" Foreground="#212121"/>
</StackPanel>
<TextBox x:Name="TbLineBreakLog" Height="150" Style="{StaticResource LogStyle}"/>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ===== Tab 2: フォルダ内文字置き換え ===== -->
<TabItem Header="フォルダ内文字置き換え">
<ScrollViewer VerticalScrollBarVisibility="Auto" Background="#F0F2F5">
<StackPanel Margin="16,16,16,8">
<!-- フォルダ設定 -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="フォルダ設定" FontWeight="SemiBold" FontSize="13" Foreground="#212121"/>
</StackPanel>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="対象フォルダ" Foreground="#555" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="TbReplaceSourceFolder"/>
<Button Grid.Column="2" Content="参照..." Margin="8,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnReplaceSourceBrowse_Click"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="出力フォルダ" Foreground="#555" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="TbReplaceDestFolder"/>
<Button Grid.Column="2" Content="参照..." Margin="8,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnReplaceDestBrowse_Click"/>
</Grid>
</StackPanel>
</Border>
<!-- 置き換え設定 -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="置き換え設定" FontWeight="SemiBold" FontSize="13" Foreground="#212121"/>
</StackPanel>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="置き換え前" Foreground="#555" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="TbReplaceFrom" Margin="0,0,10,0"/>
<TextBlock Grid.Column="2" Text="置き換え後" Foreground="#555" VerticalAlignment="Center" Margin="8,0,0,0"/>
<TextBox Grid.Column="3" x:Name="TbReplaceTo"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="文字コード" Foreground="#555" VerticalAlignment="Center"/>
<ComboBox Grid.Column="1" x:Name="CbReplaceEncoding" Width="250"/>
</Grid>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="ChkReplaceCaseSensitive" Content="大文字/小文字を区別する" IsChecked="True"/>
<CheckBox x:Name="ChkReplaceFileName" Content="ファイル名も置き換える"
IsChecked="True" Margin="24,0,0,0"/>
</StackPanel>
</StackPanel>
</Border>
<!-- 除外設定 -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="除外設定" FontWeight="SemiBold" FontSize="13" Foreground="#212121"/>
</StackPanel>
<Grid Margin="0,0,0,3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="除外フォルダ" Foreground="#555" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="TbExcludeFolders"/>
</Grid>
<TextBlock Text="区切り文字 : ;(セミコロン) 例: .git;node_modules;bin"
Foreground="#9E9E9E" FontSize="11" Margin="100,3,0,12"/>
<Grid Margin="0,0,0,3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="除外拡張子" Foreground="#555" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="TbExcludeExtensions"/>
</Grid>
<TextBlock Text="区切り文字 : ;(セミコロン) 例: .exe;.dll;.png"
Foreground="#9E9E9E" FontSize="11" Margin="100,3,0,0"/>
</StackPanel>
</Border>
<!-- 実行ボタン群 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="2,0,2,12">
<Button Content="ドライラン(確認のみ)" Style="{StaticResource DryRunBtn}"
Margin="0,0,10,0" Click="BtnReplaceDryRun_Click"/>
<Button Content="実行" Style="{StaticResource ExecBtn}"
Click="BtnReplaceExecute_Click"/>
</StackPanel>
<!-- ログ -->
<Border Style="{StaticResource CardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="ログ" FontWeight="SemiBold" FontSize="13" Foreground="#212121"/>
</StackPanel>
<TextBox x:Name="TbReplaceLog" Height="190" Style="{StaticResource LogStyle}"/>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- ===== Tab 3: タブ区切り→JSON変換 ===== -->
<TabItem Header="タブ区切り→JSON変換">
<Grid Background="#F0F2F5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- オプション行 -->
<Border Grid.Row="0" Style="{StaticResource CardStyle}" Margin="16,16,16,8">
<WrapPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Margin="0,0,24,0">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="文字コード" FontWeight="SemiBold" Foreground="#212121"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<ComboBox x:Name="CbJsonEncoding" Width="210"
SelectionChanged="CbJsonEncoding_SelectionChanged"/>
</StackPanel>
<Rectangle Width="1" Fill="#E0E0E0" Margin="0,0,24,0"/>
<StackPanel Orientation="Horizontal" Margin="0,0,10,0">
<Border Width="3" CornerRadius="2" Background="#388E3C" Margin="0,1,10,1"/>
<TextBlock Text="出力オプション" FontWeight="SemiBold" Foreground="#212121"
VerticalAlignment="Center" Margin="0,0,14,0"/>
</StackPanel>
<CheckBox x:Name="ChkJsonPrettyPrint" Content="整形出力" IsChecked="True" Margin="0,0,18,0"/>
<CheckBox x:Name="ChkJsonArrayWrap" Content="配列形式" IsChecked="True" Margin="0,0,18,0"/>
<CheckBox x:Name="ChkJsonOmitNull" Content="null省略"/>
</WrapPanel>
</Border>
<!-- 左右分割エリア -->
<Grid Grid.Row="1" Margin="16,0,16,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 入力パネル -->
<Border Grid.Column="0" Style="{StaticResource CardStyle}" Margin="2,2,4,2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<Border Width="3" CornerRadius="2" Background="#1976D2" Margin="0,1,10,1"/>
<TextBlock Text="入力(タブ区切りテキスト)" FontWeight="SemiBold"
FontSize="13" Foreground="#212121"/>
</StackPanel>
<TextBox Grid.Row="1" x:Name="TbJsonInput"
Style="{StaticResource CodeInputStyle}"
TextChanged="TbJsonInput_TextChanged"
AllowDrop="True"
Drop="TbJsonInput_Drop"
PreviewDragOver="TbJsonInput_PreviewDragOver"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock x:Name="TxtJsonInputStatus" FontSize="11"
Foreground="#9E9E9E" VerticalAlignment="Center"/>
<Button Content="ファイルを開く" Margin="8,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnJsonOpenFile_Click"/>
<Button Content="貼り付け" Margin="6,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnJsonPaste_Click"/>
<Button Content="クリア" Margin="6,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnJsonClear_Click"/>
</StackPanel>
</Grid>
</Border>
<!-- 変換ボタン -->
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="4,0">
<Button Content="変換 →" Style="{StaticResource ExecBtn}"
Click="BtnJsonConvert_Click"/>
</StackPanel>
<!-- 出力パネル -->
<Border Grid.Column="2" Style="{StaticResource CardStyle}" Margin="4,2,2,2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<Border Width="3" CornerRadius="2" Background="#388E3C" Margin="0,1,10,1"/>
<TextBlock Text="出力(JSON)" FontWeight="SemiBold"
FontSize="13" Foreground="#212121"/>
</StackPanel>
<TextBox Grid.Row="1" x:Name="TbJsonOutput"
Style="{StaticResource CodeOutputStyle}"/>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,8,0,0">
<TextBlock x:Name="TxtJsonOutputStatus" FontSize="11"
Foreground="#9E9E9E" VerticalAlignment="Center"/>
<Button Content="コピー" Margin="8,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnJsonCopy_Click"/>
<Button Content="保存..." Margin="6,0,0,0"
Style="{StaticResource BrowseBtn}"
Click="BtnJsonSave_Click"/>
</StackPanel>
</Grid>
</Border>
</Grid>
<!-- エラー表示 -->
<TextBlock Grid.Row="2" x:Name="TxtJsonError"
Foreground="#C62828" FontSize="12" TextWrapping="Wrap"
Margin="18,0,18,10" Visibility="Collapsed"/>
</Grid>
</TabItem>
</TabControl>
</Window>
using Microsoft.Win32;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Windows;
using System.Windows.Controls;
namespace BenriTool;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
InitializeEncodings();
InitJsonEncodings();
RbOutputMode_Checked(null, null);
}
// ---------------------------------------------------------------
// 共通
// ---------------------------------------------------------------
private static readonly (string Label, string Name)[] Encodings =
[
("UTF-8", "utf-8"),
("UTF-8 BOM付き", "utf-8-bom"),
("Shift_JIS", "shift_jis"),
("EUC-JP", "euc-jp"),
("UTF-16 LE", "utf-16"),
("UTF-16 BE", "utf-16BE"),
("ASCII", "us-ascii"),
("EBCDIC (US-Canada / CP037)", "ibm037"),
("EBCDIC (International / CP500)", "ibm500"),
("EBCDIC (日本語カタカナ / CP930)", "ibm930"),
("EBCDIC (日本語英数字 / CP939)", "ibm939"),
];
private void InitializeEncodings()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
foreach (var (label, _) in Encodings)
{
CbLineBreakEncoding.Items.Add(label);
CbReplaceEncoding.Items.Add(label);
}
CbLineBreakEncoding.SelectedIndex = 0;
CbReplaceEncoding.SelectedIndex = 0;
}
private Encoding GetEncoding(ComboBox cb, bool useBom = false)
{
var name = Encodings[cb.SelectedIndex].Name;
if (name == "utf-8-bom") return new UTF8Encoding(true);
if (name == "utf-8") return new UTF8Encoding(false);
return Encoding.GetEncoding(name);
}
private static string BrowseFolder(string title)
{
var dialog = new OpenFolderDialog { Title = title };
return dialog.ShowDialog() == true ? dialog.FolderName : string.Empty;
}
// ---------------------------------------------------------------
// Tab1: ファイル改行
// ---------------------------------------------------------------
private void BtnLineBreakInputBrowse_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog { Title = "入力ファイルを選択", Filter = "すべてのファイル (*.*)|*.*" };
if (dialog.ShowDialog() == true)
TbLineBreakInput.Text = dialog.FileName;
}
private void BtnLineBreakOutputBrowse_Click(object sender, RoutedEventArgs e)
{
var folder = BrowseFolder("出力フォルダを選択");
if (!string.IsNullOrEmpty(folder))
TbLineBreakOutput.Text = folder;
}
private void RbOutputMode_Checked(object? sender, RoutedEventArgs? e)
{
if (TbLineBreakOutput == null) return;
TbLineBreakOutput.IsEnabled = RbSpecifyFolder?.IsChecked == true;
}
private void BtnLineBreakExecute_Click(object sender, RoutedEventArgs e)
{
TbLineBreakLog.Clear();
try
{
var inputPath = TbLineBreakInput.Text.Trim();
if (!File.Exists(inputPath))
{
Log(TbLineBreakLog, "エラー: 入力ファイルが存在しません。");
return;
}
if (!int.TryParse(TbByteCount.Text, out var byteCount) || byteCount <= 0)
{
Log(TbLineBreakLog, "エラー: バイト数に正の整数を入力してください。");
return;
}
var enc = GetEncoding(CbLineBreakEncoding);
string outputDir;
if (RbSameFolder.IsChecked == true)
outputDir = Path.GetDirectoryName(inputPath)!;
else
{
outputDir = TbLineBreakOutput.Text.Trim();
if (string.IsNullOrEmpty(outputDir))
{
Log(TbLineBreakLog, "エラー: 出力フォルダを指定してください。");
return;
}
Directory.CreateDirectory(outputDir);
}
var fileName = Path.GetFileName(inputPath);
if (ChkAddExtension.IsChecked == true)
fileName += TbAddExtension.Text;
var outputPath = Path.Combine(outputDir, fileName);
var content = File.ReadAllBytes(inputPath);
using var writer = new BinaryWriter(File.Open(outputPath, FileMode.Create));
var newline = enc.GetBytes(Environment.NewLine);
int pos = 0;
while (pos < content.Length)
{
int len = Math.Min(byteCount, content.Length - pos);
writer.Write(content, pos, len);
pos += len;
if (pos < content.Length)
writer.Write(newline);
}
Log(TbLineBreakLog, $"完了: {outputPath}");
Log(TbLineBreakLog, $"入力: {content.Length} バイト → {byteCount} バイトごとに改行を挿入");
}
catch (Exception ex)
{
Log(TbLineBreakLog, $"エラー: {ex.Message}");
}
}
// ---------------------------------------------------------------
// Tab2: フォルダ内文字置き換え
// ---------------------------------------------------------------
private void BtnReplaceSourceBrowse_Click(object sender, RoutedEventArgs e)
{
var folder = BrowseFolder("対象フォルダを選択");
if (!string.IsNullOrEmpty(folder))
TbReplaceSourceFolder.Text = folder;
}
private void BtnReplaceDestBrowse_Click(object sender, RoutedEventArgs e)
{
var folder = BrowseFolder("出力フォルダを選択");
if (!string.IsNullOrEmpty(folder))
TbReplaceDestFolder.Text = folder;
}
private void BtnReplaceDryRun_Click(object sender, RoutedEventArgs e) =>
ExecuteReplace(dryRun: true);
private void BtnReplaceExecute_Click(object sender, RoutedEventArgs e) =>
ExecuteReplace(dryRun: false);
private void ExecuteReplace(bool dryRun)
{
TbReplaceLog.Clear();
Log(TbReplaceLog, dryRun ? "=== ドライラン開始 ===" : "=== 実行開始 ===");
var srcRoot = TbReplaceSourceFolder.Text.Trim();
if (!Directory.Exists(srcRoot))
{
Log(TbReplaceLog, "エラー: 対象フォルダが存在しません。");
return;
}
var destRoot = TbReplaceDestFolder.Text.Trim();
if (string.IsNullOrEmpty(destRoot))
{
Log(TbReplaceLog, "エラー: 出力フォルダを指定してください。");
return;
}
var from = TbReplaceFrom.Text;
if (string.IsNullOrEmpty(from))
{
Log(TbReplaceLog, "エラー: 置き換え前の文字を入力してください。");
return;
}
var to = TbReplaceTo.Text;
var comparison = ChkReplaceCaseSensitive.IsChecked == true
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;
var excludeFolders = TbExcludeFolders.Text
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var excludeExts = TbExcludeExtensions.Text
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(x => x.StartsWith('.') ? x.ToLower() : "." + x.ToLower())
.ToHashSet();
var replaceFileName = ChkReplaceFileName.IsChecked == true;
var enc = GetEncoding(CbReplaceEncoding);
int fileCount = 0, replacedCount = 0;
try
{
if (!dryRun) Directory.CreateDirectory(destRoot);
ProcessDirectory(srcRoot, srcRoot, destRoot, from, to, comparison,
excludeFolders, excludeExts, replaceFileName, enc, dryRun,
ref fileCount, ref replacedCount);
Log(TbReplaceLog, $"");
Log(TbReplaceLog, $"処理ファイル: {fileCount} 件 置き換えあり: {replacedCount} 件");
Log(TbReplaceLog, dryRun ? "=== ドライラン完了 ===" : "=== 完了 ===");
}
catch (Exception ex)
{
Log(TbReplaceLog, $"エラー: {ex.Message}");
}
}
private void ProcessDirectory(
string srcRoot, string srcDir, string destRoot,
string from, string to, StringComparison comparison,
string[] excludeFolders, HashSet<string> excludeExts,
bool replaceFileName, Encoding enc, bool dryRun,
ref int fileCount, ref int replacedCount)
{
var relDir = Path.GetRelativePath(srcRoot, srcDir);
var dirName = Path.GetFileName(srcDir);
if (excludeFolders.Any(ef => dirName.Equals(ef, StringComparison.OrdinalIgnoreCase)))
{
Log(TbReplaceLog, $"[除外フォルダ] {relDir}");
return;
}
var destDirName = replaceFileName
? ReplaceString(dirName, from, to, comparison)
: dirName;
var destDir = srcDir == srcRoot
? destRoot
: Path.Combine(destRoot, ReplacePathSegments(
Path.GetRelativePath(srcRoot, srcDir), from, to, comparison, replaceFileName));
if (!dryRun) Directory.CreateDirectory(destDir);
foreach (var filePath in Directory.GetFiles(srcDir))
{
var ext = Path.GetExtension(filePath).ToLower();
if (excludeExts.Contains(ext))
{
Log(TbReplaceLog, $"[除外拡張子] {Path.GetRelativePath(srcRoot, filePath)}");
continue;
}
fileCount++;
var origFileName = Path.GetFileName(filePath);
var newFileName = replaceFileName
? ReplaceString(origFileName, from, to, comparison)
: origFileName;
bool contentReplaced = false;
string? newContent = null;
try
{
var content = File.ReadAllText(filePath, enc);
newContent = ReplaceString(content, from, to, comparison);
contentReplaced = !string.Equals(content, newContent, StringComparison.Ordinal);
}
catch
{
// バイナリファイルなどは内容置き換えをスキップ
newContent = null;
}
var fileNameChanged = !string.Equals(origFileName, newFileName, StringComparison.Ordinal);
if (contentReplaced || fileNameChanged)
{
replacedCount++;
var action = dryRun ? "[ドライ]" : "[置換]";
Log(TbReplaceLog, $"{action} {Path.GetRelativePath(srcRoot, filePath)}" +
(fileNameChanged ? $" → {newFileName}" : ""));
}
else
{
Log(TbReplaceLog, $"[コピー] {Path.GetRelativePath(srcRoot, filePath)}");
}
if (!dryRun)
{
var destFile = Path.Combine(destDir, newFileName);
if (newContent != null)
File.WriteAllText(destFile, newContent, enc);
else
File.Copy(filePath, destFile, overwrite: true);
}
}
foreach (var subDir in Directory.GetDirectories(srcDir))
{
ProcessDirectory(srcRoot, subDir, destRoot, from, to, comparison,
excludeFolders, excludeExts, replaceFileName, enc, dryRun,
ref fileCount, ref replacedCount);
}
}
private static string ReplaceString(string src, string from, string to, StringComparison comparison)
{
if (!src.Contains(from, comparison)) return src;
var sb = new StringBuilder();
int index = 0;
while (index < src.Length)
{
int found = src.IndexOf(from, index, comparison);
if (found < 0)
{
sb.Append(src, index, src.Length - index);
break;
}
sb.Append(src, index, found - index);
sb.Append(to);
index = found + from.Length;
}
return sb.ToString();
}
private static string ReplacePathSegments(string relPath, string from, string to,
StringComparison comparison, bool replace)
{
if (!replace) return relPath;
var parts = relPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return string.Join(Path.DirectorySeparatorChar,
parts.Select(p => ReplaceString(p, from, to, comparison)));
}
private void Log(TextBox tb, string message)
{
tb.AppendText(message + Environment.NewLine);
tb.ScrollToEnd();
}
// ---------------------------------------------------------------
// Tab3: タブ区切り→JSON変換
// ---------------------------------------------------------------
private static readonly (string Label, int CodePage)[] JsonEncodings =
[
("自動判別", 0),
("UTF-8", 65001),
("UTF-8 BOM付き", -1),
("Shift-JIS (CP932)", 932),
("EUC-JP", 51932),
("UTF-16 LE", 1200),
("UTF-16 BE", 1201),
];
private string? _jsonLastLoadedPath;
private void InitJsonEncodings()
{
foreach (var (label, _) in JsonEncodings)
CbJsonEncoding.Items.Add(label);
CbJsonEncoding.SelectedIndex = 0;
}
private bool IsJsonAutoDetect => JsonEncodings[CbJsonEncoding.SelectedIndex].CodePage == 0;
private Encoding GetJsonEncoding()
{
var (_, codePage) = JsonEncodings[CbJsonEncoding.SelectedIndex];
return codePage switch
{
0 => Encoding.UTF8,
-1 => new UTF8Encoding(true),
_ => Encoding.GetEncoding(codePage)
};
}
private void CbJsonEncoding_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_jsonLastLoadedPath is not null)
JsonLoadFile(_jsonLastLoadedPath);
}
private void TbJsonInput_TextChanged(object sender, TextChangedEventArgs e)
{
JsonUpdateInputStatus();
TxtJsonError.Visibility = Visibility.Collapsed;
}
private void JsonUpdateInputStatus()
{
var lines = TbJsonInput.Text.Split('\n', StringSplitOptions.None);
var nonEmpty = lines.Count(l => l.Trim().Length > 0);
int dataRows = nonEmpty > 1 ? nonEmpty - 1 : 0;
TxtJsonInputStatus.Text = $"{nonEmpty} 行 (データ: {dataRows} 件)";
}
private void BtnJsonOpenFile_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFileDialog
{
Title = "タブ区切りファイルを開く",
Filter = "テキストファイル (*.txt;*.tsv;*.tab;*.csv)|*.txt;*.tsv;*.tab;*.csv|すべてのファイル (*.*)|*.*"
};
if (dialog.ShowDialog() == true)
JsonLoadFile(dialog.FileName);
}
private void TbJsonInput_PreviewDragOver(object sender, DragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
? DragDropEffects.Copy
: DragDropEffects.None;
e.Handled = true;
}
private void TbJsonInput_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetData(DataFormats.FileDrop) is string[] files && files.Length > 0)
JsonLoadFile(files[0]);
}
private void JsonLoadFile(string path)
{
try
{
var encoding = IsJsonAutoDetect ? JsonDetectEncoding(path) : GetJsonEncoding();
TbJsonInput.Text = File.ReadAllText(path, encoding);
_jsonLastLoadedPath = path;
JsonUpdateInputStatus();
TxtJsonInputStatus.Text += $" [{Path.GetFileName(path)} / {encoding.WebName}]";
TxtJsonError.Visibility = Visibility.Collapsed;
}
catch (Exception ex)
{
JsonShowError($"ファイル読み込みエラー: {ex.Message}");
}
}
private static Encoding JsonDetectEncoding(string path)
{
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;
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 BtnJsonPaste_Click(object sender, RoutedEventArgs e)
{
if (!Clipboard.ContainsText()) return;
TbJsonInput.Text = Clipboard.GetText();
_jsonLastLoadedPath = null;
JsonUpdateInputStatus();
}
private void BtnJsonClear_Click(object sender, RoutedEventArgs e)
{
TbJsonInput.Text = string.Empty;
TbJsonOutput.Text = string.Empty;
TxtJsonInputStatus.Text = string.Empty;
TxtJsonOutputStatus.Text = string.Empty;
_jsonLastLoadedPath = null;
TxtJsonError.Visibility = Visibility.Collapsed;
}
private void BtnJsonConvert_Click(object sender, RoutedEventArgs e)
{
TxtJsonError.Visibility = Visibility.Collapsed;
TbJsonOutput.Text = string.Empty;
var input = TbJsonInput.Text;
if (string.IsNullOrWhiteSpace(input))
{
JsonShowError("入力が空です。タブ区切りテキストを入力してください。");
return;
}
try
{
var json = ConvertTabToJson(input,
prettyPrint: ChkJsonPrettyPrint.IsChecked == true,
asArray: ChkJsonArrayWrap.IsChecked == true,
omitNull: ChkJsonOmitNull.IsChecked == true);
TbJsonOutput.Text = json;
TxtJsonOutputStatus.Text = $"{json.Length} 文字";
}
catch (Exception ex)
{
JsonShowError($"変換エラー: {ex.Message}");
}
}
private static string ConvertTabToJson(string input, bool prettyPrint, bool asArray, bool omitNull)
{
var lines = input.Split('\n')
.Select(l => l.TrimEnd('\r'))
.ToArray();
var nonEmptyLines = lines
.Where(l => l.Length > 0)
.ToList();
if (nonEmptyLines.Count == 0)
throw new InvalidOperationException("有効な行がありません。");
var headers = nonEmptyLines[0].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;
if (string.IsNullOrEmpty(value))
{
if (omitNull) continue;
writer.WriteNull(key);
continue;
}
writer.WritePropertyName(key);
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 dblVal))
writer.WriteNumberValue(dblVal);
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 BtnJsonCopy_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(TbJsonOutput.Text)) return;
Clipboard.SetText(TbJsonOutput.Text);
TxtJsonOutputStatus.Text = "コピーしました";
}
private void BtnJsonSave_Click(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(TbJsonOutput.Text))
{
JsonShowError("保存する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, TbJsonOutput.Text, Encoding.UTF8);
TxtJsonOutputStatus.Text = $"保存: {Path.GetFileName(dialog.FileName)}";
}
}
private void JsonShowError(string message)
{
TxtJsonError.Text = message;
TxtJsonError.Visibility = Visibility.Visible;
}
}