【実務・中級編】Windows Formsにおけるユーザー定義コントロールの継承:TextBoxを拡張して「フォーカス取得時の全選択」と「エラー時背景色変更」を標準装備した共通パーツの作り方 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

【設計の極意】WinFormsのコピペ地獄を脱却せよ:継承による「高機能TextBox」の構築と堅牢なUI設計

開発現場において、最も忌むべき悪習は何だと考えるか?
それは、「似たようなコードを、異なる画面のイベントハンドラにコピペすること」だ。

「フォーカスが当たったら全選択したい」「入力エラーなら背景を赤くしたい」。これらは業務アプリケーションにおいて極めて一般的な要件だ。しかし、多くの開発者は、各画面の `TextBox_Enter` や `TextBox_Leave` イベントに、同じようなロジックを書き散らす。

その結果、どうなるか?
仕様変更があった際、修正すべき箇所は「全画面」に及ぶ。修正漏れはバグを生み、保守コストは指数関数的に増大する。これはエンジニアリングではなく、単なる「作業」だ。

我々プロフェッショナルが目指すべきは、「振る舞いをコントロールに閉じ込める」設計である。
本記事では、Windows Formsにおけるクラス継承を駆使し、標準の `TextBox` を拡張して、業務に耐えうる堅牢なカスタムコントロールを構築する手法を伝授する。

1. なぜ「継承」なのか? イベントハンドラとの決定的な違い

多くの初学者は、既存の `TextBox` に対して、フォーム側からイベントを購読(Subscribe)しようとする。

' 【アンチパターン】 フォーム側にロジックが漏れ出している
Private Sub txtInput_Enter(sender As Object, e As EventArgs) Handles txtInput.Enter
    DirectCast(sender, TextBox).SelectAll()
End Sub

これでは、新しい入力項目が増えるたびに、このコードを記述しなければならない。
一方で、継承(Inheritance)を用いたアプローチは、コントロール自身に「自分はどう振る舞うべきか」というアイデンティティを持たせる。

一度 `SmartTextBox` という基底クラスを作ってしまえば、あとはそれをツールボックスからドラッグ&ドロップするだけで、全機能が備わったコントロールが手に入る。設計の責務(Responsibility)を、フォームからコントロールへと移譲するのだ。

2. 実装:SmartTextBox の設計図

では、具体的に「フォーカス時の全選択」と「エラー状態の可視化」を備えた `SmartTextBox` を実装しよう。

ここでは、単に色を変えるだけでなく、「プロパティの変更が即座に外観に反映される」という、オブジェクト指向として正しい設計(カプセル化)を徹底する。

Imports System.ComponentModel
Imports System.Drawing

''' <summary>
''' 業務アプリケーション向け拡張TextBox
''' フォーカス時の全選択およびエラー状態の視覚化機能を標準装備。
''' </summary>
Public Class SmartTextBox
    Inherits TextBox

    ' --- プライベート変数 ---
    Private _isError As Boolean = False
    Private _errorBackColor As Color = Color.MistyRose ' エラー時の背景色

    ' --- プロパティ ---

    ''' <summary>
    ''' エラー状態かどうか。Trueの場合、背景色がエラー色に変わります。
    ''' </summary>
    <Category("Appearance")>
    <Description("エラー状態かどうかを指定します。")>
    Public Property IsError As Boolean
        Get
            Return _isError
        End Get
        Set(value As Boolean)
            _isError = value
            UpdateAppearance() ' 値が変わった瞬間に外観を更新する
        End Set
    End Property

    ''' <summary>
    ''' エラー時に表示する背景色を指定します。
    ''' </summary>
    <Category("Appearance")>
    <Description("エラー状態の時に表示される背景色です。")>
    Public Property ErrorBackColor As Color
        Get
            Return _errorBackColor
        End Get
        Set(value As Color)
            _errorBackColor = value
            UpdateAppearance()
        End Set
    End Property

    ' --- コンストラクタ ---
    Public Sub New()
        ' 初期状態の設定
        UpdateAppearance()
    End Sub

    ' --- メソッド ---

    ''' <summary>
    ''' 状態に基づいてコントロールの外観を更新する。
    ''' </summary>
    Private Sub UpdateAppearance()
        If _isError Then
            Me.BackColor = _errorBackColor
        Else
            ' 通常時はシステム標準の背景色に戻す
            Me.BackColor = SystemColors.Window
        End If
    End Sub

    ' --- イベントのオーバーライド ---

    ''' <summary>
    ''' フォーカス取得時の挙動を制御。
    ''' </summary>
    Protected Overrides Sub OnEnter(e As EventArgs)
        MyBase.OnEnter(e)

        ' 【重要】SelectAll() を直接呼ぶと、OSのフォーカス処理とのタイミング問題で
        ' 選択が解除されることがある。BeginInvokeを使用して、
        ' フォーカス移動が完全に完了した直後に実行されるようスケジュールする。
        Me.BeginInvoke(Sub()
                           Me.SelectAll()
                       End Sub)
    End Sub

    ''' <summary>
    ''' フォーカス喪失時の挙動を制御。
    ''' </summary>
    Protected Overrides Sub OnLeave(e As EventArgs)
        MyBase.OnLeave(e)
        ' 必要に応じて、Leave時にバリデーションをトリガーする設計も可能
    End Sub

End Class

3. アーキテクトの視点:実装の急所

このコードには、単なる「動くコード」を超えた、プロフェッショナルとしての配慮が3点組み込まれている。

① `BeginInvoke` によるタイミングの制御

`OnEnter` 内で直接 `Me.SelectAll()` を呼び出すと、Windowsのメッセージループの都合上、選択が即座にキャンセルされてしまうケースが多々ある。`BeginInvoke` を使い、メッセージキューの末尾に「全選択」という命令を積むことで、確実にフォーカス取得後に選択処理を実行させている。これは、UIの「触り心地」を決定づける極めて重要なテクニックだ。

② `OnEnter` のオーバーライド vs イベント購読

`AddHandler` で `Enter` イベントを購読するのではなく、`Protected Overrides Sub OnEnter` を使用している。これは継承を利用する場合の鉄則だ。イベントハンドラを購読する方法では、もし子クラスでさらに別の `Enter` 処理を追加しようとした際に、イベントの重複や実行順序の制御が困難になるからだ。

③ 状態(State)と外観(Appearance)の分離

`IsError` プロパティの `Set` アクセッサ内で `UpdateAppearance()` を呼び出している。これにより、開発者は `txtInput.IsError = true` と書くだけで、色変更のロジックを意識することなく、コントロールの見た目を制御できる。これが「関心の分離」である。

4. 実務での活用:バリデーションとの連携

この `SmartTextBox` を作成したら、あとは各画面のバリデーションロジックで、そのプロパティを叩くだけだ。

' 画面側のバリデーション例
Private Sub btnSave_Click(sender As Object, e As EventArgs) Handles btnSave.Click
    Dim isAllValid As Boolean = True

    ' 数値チェックの例
    If Not IsNumeric(txtAge.Text) Then
        txtAge.IsError = True
        isAllValid = False
    Else
        txtAge.IsError = False
    End If

    ' 必須チェックの例
    If String.IsNullOrWhiteSpace(txtName.Text) Then
        txtName.IsError = True
        isAllValid = False
    Else
        txtName.IsError = False
    End If

    If isAllValid Then
        ' 保存処理へ...
    End If
End Sub

5. まとめ:保守性の高いシステムを作るために

「動けばいい」という考え方は、エンジニアとしての成長を止める。
今回紹介した手法は、単にコードの行数を減らすためのものではない。

1. 一貫性(Consistency): すべての入力項目が同じ挙動、同じエラー表示を行う。
2. 予測可能性(Predictability): コントロールのプロパティを変えれば、見た目が変わるという確信を持てる。
3. 拡張性(Extensibility): 将来「エラー時にアイコンを表示したい」となった場合、`SmartTextBox` 一箇所を直すだけで、全画面に適用される。

これこそが、大規模な業務システムを支える「堅牢な設計」の正体だ。
次なるプロジェクトでは、コピペを捨て、継承によるスマートな設計を選択せよ。

タイトルとURLをコピーしました