DataGridViewの入力文字幅制限

# 古のWindowsFormsアプリに手を入れる羽目に
## 環境など
・WindowsFormsアプリケーション
・VB.NET
・確か、.NET Framework4.5あたりだったと思う。

## こんなアプリ
・DataGridViewが配置されており、セルに入力された内容が、そのままのレイアウトでエクセルに出力される模様。

## 修正内容
DataGridViewの特定のカラムに対して、入力文字数制限する。

# CRやったるデー
## 楽勝やろ
CellValidatingイベントを実装し、指定の文字数を超えていたら、MessageBoxで通知するようにした。余裕。

## え？違う？
入力させてから怒るのでなく、入力できないようにしろって言われた。
「そんな要件聞いてませんけど？」
などと言い返す実力も余裕も収入も無い。
何とかしようとする旅が、今始まる！

## 最終的にお客様のして欲しい事は？
エクセルで文字が縮小されたり、セルの大きさが変わったりしないように、入力時に適切な幅までで入力を止めて欲しい。半角文字も全角文字も入ります、と。
・・・なるほど。

# 考えた
簡単な解決策としては、エクセルもDataGridViewも等幅フォントにして、入力値の文字コードをShift-JISに変換して、特定のバイト数で切り捨てればいいかな、と思った。
けど、そんな修正じゃなんか物足りない。

# とりあえずコードを提示
まず、架空のPictureBoxに文字を描画して、どれくらいの幅になるか計算するクラス。
```vb:DrawEx.vb
Imports System.Drawing

Public Class DrawEx
    ''' <summary>
    ''' 文字列を描画した際の幅を計算するんだぜ
    ''' </summary>
    ''' <remarks>パクリ元：dobon.net</remarks>
    Public Function StringWidth(str As String) As Decimal
        'このサイズは適当。長い文字列になる場合は、もっと大きいPictureBoxが必要なのかも？
        Dim picBox As New PictureBox With {.Width = 500, .Height = 50}

        Dim canvas As New Bitmap(picBox.Width, picBox.Height)
        Dim g As Graphics = Graphics.FromImage(canvas)

        'フォント。文字がきれいに整列するようにという要望で等幅フォント
        Dim fnt = New Font("ＭＳ ゴシック", 14)
        Dim sf As New StringFormat

        '実際に描画する
        g.DrawString(str, fnt, Brushes.Black, 0, 0, sf)

        '幅の最大値が1000ピクセルとして、文字列を描画するときの大きさを計測する
        Dim stringSize As SizeF = g.MeasureString(str, fnt, 1000, sf)

        Return stringSize.Width
    End Function
```

次。stringの拡張メソッドとして、文字列の幅を返すメソッドを定義。
なんで拡張メソッドなん？って問い詰められても、やりたかったからとしか答えようがないわな。

```vb:Extensions.vb
Imports System.Runtime.CompilerServices

Public Module Extenstions

    <Extension()>
    Public Function CalcWidth(str As String) As Decimal
        Dim drEx = New DrawEx
        Return drEx.StringWidth(str)
    End Function

    <Extension()>
    Public Function FitWidth(src As String, limit As Integer) As String
        '未入力の場合は判定の必要無し
        If src.Length = 0 Then
            Return ""
        End If

        Dim isOk = False
        Dim len = src.Length
        Dim drEx = New DrawEx

        While len > 0
            Dim str = src.Substring(0, len)

            ' 文末のスペースを描画幅に含むために置換
            If drEx.StringWidth(str.Substring(0, len).Replace(" ", "_").Replace("　", "_")) <= limit Then
                Return str
            End If
            len -= 1
        End While

        Return ""
    End Function

End Module
```

んで、DataGridViewがあるフォームで、イベントを定義
```vb
Private Sub DataGridView1_EditingControlShowing(sender As Object, e As DataGridViewEditingControlShowingEventArgs) Handles DataGridView1.EditingControlShowing
    Dim currentCell = DataGridView1.CurrentCell
    If currentCell IsNot Nothing Then
        '制限したい列か判定。
        Select Case currentCell.OwningColumn.Name
            Case DataGridViewTextBoxColumn1.Name
                Dim tbc = TryCast(e.Control, DataGridViewTextBoxEditingControl)
                If tb IsNot Nothing Then
                    RemoveHandler tb.TextChanged, AddressOf TextBoxChanged
                    AddHandler tb.TextChanged, AddressOf TextBoxChanged
                End If
        End Select
    End If
End Sub

Private Sub TextBoxChanged(sender As Object, e As EventArgs)
    Dim tb = TryCast(sender, TextBox)
    If tb IsNot Nothing Then
        Dim dst = tb.Text.FitWidth(任意の幅)
        tb.Text = dst
        ' キャレットを末尾にセット。これをしないと先頭に行ってしまうから。
        tb.Select(tb.Text.Length, 0)
    End If
End Sub
```

# 補足
* EditingControlShowingイベントで、編集が開始されたセルを評価。当該TextBoxColumnならば、TextChangedイベントを付与して入力制限させる。

* TextBoxChanged内の、任意の幅の部分は、あらかじめ必要な文字数の幅を、CalcWidth()で計測しておく必要がある。


# 雑談
バイト数じゃなくて、描画した幅で入力を制限しているので、お客様がフォントを変えたいと仰ったときにも対応できるはず。

色々なサイトから情報をパクったので、太子橋・・イマイチ、内容はよく把握できていない。
こんな負の遺産のことなんか理解しなくていいよな！
と自分に言い聞かせている。自分の頭の悪さは棚上げ。一生棚卸しない！！

あと、DrawExクラスは、staticクラスの方がいいのか考えたり、staticじゃないならちゃんとインスタンスを破棄した方がいいのか考えたりしました。


# あとがき
CellValueChangedだなんだと色々試してみたが、編集中のセルでごにょごにょする方法が見つけられなかった。TextBoxならできるのになー、という考えから、このような結果に至りました。公式サイトや、DataGridViewについていろんな情報を公開してくださっている皆様、おかげさまでお客様のご要望を満たすことができました。ありがとうございました。