WPF TextBoxのバリデーションで高さ・枠線が変わる原因と対策|Validation.ErrorTemplateカスタマイズ

WPFのTextBoxに必須チェックなどのバリデーションを適用したら、エラー時だけ高さが変わったり枠線が浮いて見えたりして、フォーム全体の見た目が崩れることがあります。原因は多くの場合、WPFが標準で持つValidation.ErrorTemplateの装飾です。崩れないUIを保ったままエラーを通知する実装を、すぐ使えるXAML例と設計のコツ付きで解説します。

目次

TextBoxにバリデーションを付けたら高さや枠線が変わる現象

WPFでは、TextBoxに必須チェック(空文字禁止)などの入力チェックを入れるために、ValidationRuleやIDataErrorInfo/INotifyDataErrorInfoを使うのが一般的です。ところが、バリデーションが発火したタイミングで、次のような“見た目の違和感”が起きることがあります。

  • エラー時のTextBoxだけ、周囲より高さが低く見える/詰まって見える
  • エラー時のTextBoxだけ、枠線が太く見える・二重に見えるなど、テーマから浮いて見える
  • 右側に謎のアイコン(!のようなもの)が出て、隣のコントロールと干渉しているように見える

質問で示されているXAMLのように、Validation.HasErrorのトリガーでツールチップにエラーメッセージを出す実装はよく見かけます。

<TextBox ...>
    <TextBox.Text>
        <Binding Path="FirstName"
                 ValidatesOnDataErrors="True"
                 ValidatesOnExceptions="True"
                 UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:RequiredFieldValidation />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>

    <TextBox.Style>
        <Style TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={RelativeSource Self},
                                            Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
</TextBox>

バリデーション自体は正しく動くのに、エラー時だけUIが崩れてしまうのは、ユーザー体験としてかなり痛いポイントです。入力フォームは横並びで比較されやすく、ほんの数pxの差でも「何かおかしい」「押しづらい」「見づらい」につながります。

症状見た目の原因になりやすいものよくある環境
高さが低く見える/詰まる既定エラーテンプレートの枠・装飾が重なり、内部余白や境界が強調される固定高さのTextBox、フォントサイズ混在、テーマ適用
枠線が別物に見えるValidation.ErrorTemplateが赤枠など独自の装飾を描画するデフォルトテーマ、カスタムテーマ、サードパーティテーマ
右側に記号が出て干渉する既定テンプレートがアイコン等の“付加要素”を表示する密なレイアウト(横にボタン、隣にTextBox)

原因はValidation.ErrorTemplateの既定装飾

結論から言うと、崩れの主因はWPFが標準で適用するValidation.ErrorTemplateです。

WPFのバリデーションは、エラーが発生すると以下の2つが同時に起きます。

  • Validation.HasError = True(添付プロパティ)になり、Triggersが反応できる
  • 同時に、Validation.ErrorTemplate(既定のエラーテンプレート)が適用され、赤枠やアイコンなどの装飾が描画される

質問の実装は「Validation.HasErrorを使ってツールチップ表示」までは自前で整えていますが、既定のErrorTemplateはそのまま生きているため、赤枠・アイコンなどの“標準装飾”が重なり、結果として見た目が変わります。

特にWPFのErrorTemplateは、アドーナー(Adorner)という仕組みでコントロールの上に装飾を重ねます。アドーナーは「レイアウト計算とは別枠」で描画されますが、次のような事情で結果的に“サイズが変わったように見える”ことがあります。

  • 赤枠やアイコンがコントロールの縁に重なって描画され、境界が太く見える
  • 固定高さの場合、枠の描画が強調されて内側が狭く見える(高さが低くなったように感じる)
  • 隣接コントロールと密接していると、付加要素が横方向に干渉して“押し潰された”ように錯覚する
  • テーマ(標準/Aero、カスタム、Material Design、MahAppsなど)側がValidation状態に反応して枠線表現を変える

つまり、入力チェックのロジック自体ではなく、エラー時の見せ方(ErrorTemplate)が標準のままであることが問題です。ここを制御すれば、フォーム全体の統一感を維持できます。

対処方針の選び方

見た目を崩さずにバリデーションを使う方法は、大きく分けて次の2パターンです。

方針やること向いているケース注意点
見た目を一切変えないValidation.ErrorTemplateを空に差し替える「エラーはツールチップだけで十分」「レイアウト最優先」赤枠などの標準表示は消えるので、別の通知手段が必要
統一感を保ちつつ分かりやすくする独自Validation.ErrorTemplateを作り、TextBox本体はそのまま、横に印だけ出す「必須や形式エラーを視認させたい」「フォーム全体でUXを揃えたい」密なレイアウトでは印が隣に重なることがあるため配置設計が必要

見た目を変えたくない場合はErrorTemplateを空にする

とにかくTextBoxの高さ・枠線・テーマ表現を変えたくないなら、いちばん確実なのはErrorTemplateを空にする方法です。標準の赤枠やアイコンを完全に無効化できます。

空のControlTemplateを用意して適用する

リソースに空のテンプレートを定義し、TextBoxのValidation.ErrorTemplateに指定します。

<Window.Resources>
    <!-- 何も描画しない空のエラーテンプレート -->
    <ControlTemplate x:Key="EmptyErrorTemplate" />
</Window.Resources>

<TextBox
    Margin="10,10,5,5"
    Validation.ErrorTemplate="{StaticResource EmptyErrorTemplate}">
    <TextBox.Text>
        <Binding Path="FirstName"
                 UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:RequiredFieldValidation />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>

    <TextBox.Style>
        <Style TargetType="{x:Type TextBox}">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={RelativeSource Self},
                                            Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
</TextBox>

これで、エラー時でもTextBox本体の描画は変わらず、ツールチップだけで通知できます。フォームの縦横がピタッと揃うので、業務アプリの入力画面では特に扱いやすい構成です。

画面全体に一括適用したい場合

同じ方針をフォーム全体(またはアプリ全体)で統一したいときは、TextBoxのStyleにまとめると保守が楽になります。

<Application.Resources>
    <ControlTemplate x:Key="EmptyErrorTemplate" />

    <Style TargetType="{x:Type TextBox}">
        <Setter Property="Validation.ErrorTemplate"
                Value="{StaticResource EmptyErrorTemplate}" />

        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="True">
                <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={RelativeSource Self},
                                        Path=(Validation.Errors)[0].ErrorContent}" />
            </Trigger>
        </Style.Triggers>
    </Style>
</Application.Resources>

このスタイルを入れておくと、個別のTextBoxに毎回ErrorTemplateを指定しなくても、崩れない見た目でバリデーションが使えます。例外的に標準の赤枠を使いたいTextBoxがあるなら、そのコントロールだけ個別に別テンプレートを指定する運用も可能です。

統一感を保ちつつエラーを“見える化”する独自ErrorTemplate

ツールチップだけだと、ユーザーが「どこが必須なのか」「何がエラーなのか」を見落としやすいことがあります。特に入力欄が多い画面では、エラー箇所がひと目で分かる工夫が欲しくなります。

そこでおすすめなのが、TextBox本体の見た目やサイズは一切変えず、エラー時だけ右側に小さな印(例:赤い*)を出し、そこにツールチップで理由を出す方法です。WPFのアドーナーをうまく使うと、フォームの整列を崩さずに実現できます。

AdornedElementPlaceholderを使うのがポイント

独自ErrorTemplateでは、AdornedElementPlaceholderを必ず入れます。これは「元のTextBoxをそのまま描画するためのプレースホルダー」です。ここを中心に組むと、TextBox本体のテンプレートに手を入れずに、エラー表示だけを追加できます。

<UserControl.Resources>
    <ControlTemplate x:Key="TextErrorTemplate">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <!-- 元のTextBoxをそのまま表示 -->
            <AdornedElementPlaceholder x:Name="placeholder" />

            <!-- エラー印:右側に赤い * を表示し、ツールチップにエラーメッセージ -->
            <TextBlock Grid.Column="1"
                       Margin="4,0,0,0"
                       VerticalAlignment="Center"
                       FontWeight="Bold"
                       FontSize="12"
                       Foreground="Red"
                       Text="*"
                       ToolTip="{Binding ElementName=placeholder,
                                         Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
        </Grid>
    </ControlTemplate>
</UserControl.Resources>

このテンプレートをTextBoxに適用します。

<TextBox
    Margin="10,10,5,5"
    Validation.ErrorTemplate="{StaticResource TextErrorTemplate}">

    <TextBox.Text>
        <Binding Path="FirstName"
                 UpdateSourceTrigger="PropertyChanged"
                 ValidatesOnDataErrors="True"
                 ValidatesOnExceptions="True">
            <Binding.ValidationRules>
                <local:RequiredFieldValidation />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

この構成のメリットは明確です。

  • TextBox本体はプレースホルダーで“そのまま”描画されるため、高さ・枠線・テーマの見た目が変わらない
  • エラー時だけ右側に印(*)が出るので、入力欄が多くてもエラー箇所が一目で分かる
  • ツールチップは印側に持たせるので、TextBox自体にToolTipを常時設定するよりも意図が明確

TextBox.StyleのTriggerを消してErrorTemplate側に寄せると競合しにくい

質問のように、TextBox.StyleのTriggerでToolTipを付ける実装でも動きますが、複数画面・複数スタイルが絡むと「どのStyleが勝ったか」が見えづらくなり、見た目の差異が増えがちです。

エラー表示(印+ツールチップ)をErrorTemplate側に完結させておくと、TextBox本体のStyleは通常状態の見た目だけに集中でき、保守が安定します。

印が隣のコントロールに重なるときの対策

ErrorTemplateの表示はアドーナーのため、印(*)が“レイアウト上の幅”を確保しません。横にボタンや別のTextBoxが密に並んでいると、印が重なって見えることがあります。よくある回避策は次のいずれかです。

回避策やり方向いている場面
右側に余白を確保TextBoxに右Marginを入れる(例:Margin=”10,10,22,5″)フォームの列が揃っていて、余白を統一しやすい
印をTextBoxの内側に出す同じ列に重ねて右寄せで表示し、TextBoxのPadding右を少し増やす密なレイアウトで外側に出せない
エラー表示の場所を別領域へValidation.AdornerSite/AdornerSiteForで別の場所に出す、またはラベル欄へ集約入力欄が非常に多い、ガイド領域がある

「印を内側に出す」場合のイメージ例です(右Paddingを確保して文字と重ならないようにします)。

<ControlTemplate x:Key="TextErrorTemplate_Inside">
    <Grid>
        <AdornedElementPlaceholder x:Name="placeholder" />

        <TextBlock HorizontalAlignment="Right"
                   Margin="0,0,6,0"
                   VerticalAlignment="Center"
                   FontWeight="Bold"
                   FontSize="12"
                   Foreground="Red"
                   Text="*"
                   ToolTip="{Binding ElementName=placeholder,
                                     Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
    </Grid>
</ControlTemplate>

<TextBox Padding="6,2,18,2"
         Validation.ErrorTemplate="{StaticResource TextErrorTemplate_Inside}" />

外側に印を出すのが難しい画面でも、内側右寄せならレイアウトが崩れにくくなります。

ClipToBoundsがTrueだとアドーナーが切れることがある

コンテナ側でClipToBounds=”True”が指定されていると、アドーナー(ErrorTemplateの表示)が切り取られて見えないケースがあります。スクロール領域や境界線のあるパネルで起きやすいので、印が出ない場合は周辺コンテナのClip設定も確認してください。

必須チェックのValidationRule実装例

入力欄の必須チェックは最も多いバリデーションです。ValidationRuleで作る場合は、次のように最小構成で実装できます。

using System.Globalization;
using System.Windows.Controls;

public class RequiredFieldValidation : ValidationRule
{
    public string Message { get; set; } = "必須項目です。";

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var text = value as string;

        if (string.IsNullOrWhiteSpace(text))
        {
            return new ValidationResult(false, Message);
        }

        return ValidationResult.ValidResult;
    }
}

XAML側では先ほどのようにBinding.ValidationRulesへ指定します。エラーメッセージを画面ごとに変えたいなら、MessageプロパティをXAMLから指定すると便利です。

<Binding Path="FirstName" UpdateSourceTrigger="PropertyChanged">
    <Binding.ValidationRules>
        <local:RequiredFieldValidation Message="姓は必須です。" />
    </Binding.ValidationRules>
</Binding>

ValidatesOnDataErrors/NotifyDataErrorsとの使い分け

質問のXAMLにはValidatesOnDataErrors/ValidatesOnExceptionsが含まれています。これは「ViewModel側のエラー通知(IDataErrorInfoなど)」と「例外」をバリデーションとして扱うための指定です。一方、ValidationRulesはBindingにぶら下げたローカルルールです。両方を同時に使うこと自体は可能ですが、ルールが二重化すると同じ入力に対して複数のエラーが出る運用になりがちです。

設計をシンプルにするなら、まずは次のどちらかに寄せるのがおすすめです。

  • 画面固有のルールが中心 → ValidationRule中心(今回の記事の例)
  • ドメインルールをViewModelに集約したい → INotifyDataErrorInfo中心(ValidatesOnNotifyDataErrorsを使う)

INotifyDataErrorInfoを使う場合は、BindingにValidatesOnNotifyDataErrors=”True”を付ける構成が一般的です。

<TextBox Text="{Binding FirstName,
                        UpdateSourceTrigger=PropertyChanged,
                        ValidatesOnNotifyDataErrors=True}" />

この方式でも、見た目の崩れ対策としてErrorTemplateの制御は同じ考え方で適用できます。

入力体験を良くする小さな工夫

バリデーションを入れると「正しく入力させる」ことはできますが、出し方を間違えるとユーザーのストレスが増えます。見た目を揃えることに加えて、次のポイントも押さえるとフォーム全体の完成度が上がります。

入力中に毎回エラーを出さない

UpdateSourceTrigger=PropertyChangedだと、1文字入力するたびに必須チェックが走るため、空→1文字→空…のタイミングでチラつきやすくなります。必須チェックはLostFocusに寄せる、もしくはBindingのDelayを入れて“入力が落ち着いた頃”に判定すると体験が良くなります。

<TextBox Text="{Binding FirstName,
                        UpdateSourceTrigger=PropertyChanged,
                        Delay=300}" />

(Delayは入力の反応性と相談して調整します。必須チェックは300〜500ms程度でも体感が良くなることがあります。)

枠線を変えるなら太さは変えない

エラー時に枠線の色だけ変えたい場合、BorderThicknessまで変えると“サイズが変わった感”が出やすくなります。見た目の統一を重視するなら、太さは固定のままBorderBrushだけ変えるのが安全です。

<Style TargetType="{x:Type TextBox}">
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
            <Setter Property="BorderBrush" Value="Red" />
            <!-- BorderThicknessは変えない -->
        </Trigger>
    </Style.Triggers>
</Style>

ただし、テーマ側のテンプレートが既に枠線を複雑に描画している場合は、ErrorTemplate側で印表示に寄せるほうが競合しにくいです。

画面全体で統一するためのリソース設計

入力欄が数十個ある画面や、画面数が多い業務アプリでは、コントロールごとにValidation.ErrorTemplateを指定していくのはつらくなります。おすすめは、共通のResourceDictionaryにErrorTemplateとStyleをまとめる構成です。

ResourceDictionaryで共通化する例

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">


<ControlTemplate x:Key="TextErrorTemplate">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <AdornedElementPlaceholder x:Name="placeholder" />

        <TextBlock Grid.Column="1"
                   Margin="4,0,0,0"
                   VerticalAlignment="Center"
                   FontWeight="Bold"
                   FontSize="12"
                   Foreground="Red"
                   Text="*"
                   ToolTip="{Binding ElementName=placeholder,
                                     Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" />
    </Grid>
</ControlTemplate>

<Style x:Key="ValidationStableTextBoxStyle" TargetType="{x:Type TextBox}">
    <Setter Property="Validation.ErrorTemplate"
            Value="{StaticResource TextErrorTemplate}" />
</Style>


各TextBoxでは次のようにStyleを当てるだけになります。

<TextBox Style="{StaticResource ValidationStableTextBoxStyle}"
         Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" />

さらに徹底するなら、アプリ全体の既定Style(Keyなし)としてTextBoxに適用する方法もあります。ただし、サードパーティテーマを使っている場合はBasedOnで既存テーマのStyleを継承しないと見た目が崩れることがあるため、導入時は注意してください。

よくあるハマりどころチェックリスト

「ErrorTemplateを変えたのにまだ違和感がある」「一部だけ崩れる」という場合は、見た目の差が出やすいポイントを潰していくと解決が早いです。

チェック項目起きやすい問題対策
TextBoxにHeightが固定で設定されている枠・余白の見え方が強調され、詰まって見えるMinHeightに寄せる、Padding/VerticalContentAlignmentも見直す
Validation.HasErrorでBorderThicknessを変えているエラー時だけサイズ差が出やすいBorderBrushのみ変更、Thicknessは固定
隣のコントロールと間隔が無い印(*)が重なる/干渉して見える右Marginを統一して確保、または印を内側表示へ
親要素がClipToBounds=Trueアドーナーが切れて印が見えないClipToBoundsを見直す、表示場所を内側にする
テーマやライブラリでValidationの見た目を上書きしている一部だけ枠線が別物になるBasedOnで継承、競合するTriggerを整理

まとめ:崩れないバリデーションはErrorTemplateを制御する

WPFのTextBoxでバリデーションを有効にしたとき、エラー時だけ高さや枠線が変わって見えるのは、多くの場合既定のValidation.ErrorTemplateが装飾を描画することが原因です。

  • 見た目を一切変えないなら、Validation.ErrorTemplateを空テンプレートに差し替える
  • 統一感を保ちつつ分かりやすくするなら、AdornedElementPlaceholderでTextBox本体をそのまま描画し、印+ツールチップだけ追加

フォームUIは「揃っていること」自体が品質に直結します。ErrorTemplateを意図通りに設計しておけば、必須チェックや形式チェックを入れても画面が崩れず、ユーザーにも分かりやすい入力体験を提供できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次