今回は久し振りにTextBoxのメッセージ実装をやりましょう。ターゲットは、EM_SETRECTです。ところでEM_SETMARGINSとEM_SETRECTの使い分けが微妙に良く分かりません。単一行エディットコントロールにはEM_SETMARGINSを使い、複数行にはEM_SETRECTを使うという方向で良いんですかね?
EM_SETRECTは、フォーマット領域を設定するメッセージです。フォーマット領域ってのは、まあ分かりやすく言えば実際に文字を描画する領域です。つまりこのメッセージを使用することで、本来きちきちに表示されるテキストボックスが、上下左右にスペースを持たせて表示できるようになるって訳ですね。それだけなら見やすくなるなで済む話ですが、これはその後の自作エディタで役に立つテクニックです(私は作るつもりはありませんけど)。つまり、左にスペースを持たせてやれば、そこに行番号を表示させたりもできるようになるわけです。
さてそんな将来性を感じさせるEM_SETRECTですが、実装は割と簡単です。注意点として、
- RECT構造体が要求されますが、これはSystem.Drawig.Rectangle構造体とは名前は似てますが別の構造です。ですから自前で定義してやらなければなりません。私はつい趣味に走ってRectangleと相互変換できるようにしたりします。
- EM_SETRECTで設定したフォーマット領域は、テキストボックスのサイズが変更されるとクリアされてしまいます。そのためResizeイベントで再設定してやる必要があります。
- EM_SETRECTはMultilineがfalseに設定されているTextBox/RichTextBoxには無効です。サンプルでは基底クラスのMultilineプロパティを隠蔽し、getアクセサのみにしています。ところでこれ、オーバーライドしてsetアクセサでNotSupportedException投げるのとどっちがより優れた手段なんでしょう? インターフェイスのメンバのように基底クラスのメンバの明示的実装/再実装とかできればいいんですけど。
- EM_SETRECTはその名の通り描画領域を四角形で指定します。しかしこれはユーザにとって直感的ではないので、サンプルではPaddingという形でユーザに提供しています。つまり各辺それぞれの端からの距離ですね。内部でそれを四角形(Rect構造体)に変換しています。左辺と上辺はそのまま距離を座標に置き換えられますので、右辺と下辺だけ計算が必要です。クライアント領域の幅と高さからならそのまま差し引くだけで求められます。
- サンプルではEM_GETRECTも一応実装していますが、機能させていません。必要が感じられないので。内部でフィールドに保持しておけばそれでいいやー、みたいな。
- もしクライアント領域の幅か高さがEM_SETRECTで設定した値よりも小さくなってしまった場合、EM_SETRECTの設定は無効になり、クライアント領域全体に文字列が描画されるようになります。サンプルはこの場合を考慮していないので、実際に使うときは注意してください。
ところでちょうど「TextBoxでOwnerDraw VB.NET」なんてキーワードでいらっしゃった方がいました。今更なので多分見ていないでしょうが、TextBoxで自前描画をしたいって言うのは、恐らく一部だけ書き換えたいとか追加したいとかそう言う要求なんでしょう。一から書くならテキストボックス使わなきゃ良いんだし。で、TextBoxの場合Windowsに描画を任せているのでPaintイベントも発生しません。そう言うときはWndProcをオーバーライドして、WM_PAINTメッセージを処理します。一旦基底クラスのWndProcを呼んで一通り描画させた後、おもむろにCreateGraphicsでGraphicsオブジェクトを作って描画します。
XMLドキュメント、イベント部分を含む完全版コードはこちら。
それではまずC#のコードからです。
using System; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; namespace HongliangSoft.Utilities { public class PaddedTextBox : TextBox { [DllImport("user32.dll", SetLastError=true)] private static extern int SendMessage(IntPtr hwnd, int msg, IntPtr wParam, ref Rect lParam); public PaddedTextBox() { base.Multiline = true; } public new bool Multiline {get {return base.Multiline;}} protected sealed class Msg { public const int GetRect = 0x00B2; public const int SetRect = 0x00B3; } public Padding TextPadding { get { return textPadding; } set { this.textPadding = value; SetPadding(value); } } private Padding textPadding; private Padding GetPadding() { Rect rect = new Rect(); SendMessage(Handle, Msg.GetRect, IntPtr.Zero, ref rect); Size cli = this.ClientSize; //rectは上下左右の各位置の座標が入っているので、 //PaddingにするためにはClientSizeと比較する return new Padding(rect.Left, rect.Top, cli.Width - rect.Right, cli.Height - rect.Bottom); } private void SetPadding(Padding value) { Rect rect = new Rect(); rect.Left = value.Left; rect.Top = value.Top; Size cli = this.ClientSize; //丁度GetPaddingの時と同じ操作が必要(操作は同じだけど意味は逆) rect.Right = cli.Width - value.Right; rect.Bottom = cli.Height - value.Bottom; SendMessage(Handle, Msg.SetRect, IntPtr.Zero, ref rect); } protected override void OnResize(EventArgs e) { //リサイズしたときにフォーマット領域はクリアされるので、再指定 SetPadding(this.textPadding); } [StructLayout(LayoutKind.Sequential)] protected struct Rect { public int Left; public int Top; public int Right; public int Bottom; public int Width { get {return Right - Left;} set {Right = Left + value;} } public int Height { get {return Bottom - Top;} set {Bottom = Top + value;} } } } #if !V10 && !V11 #else //.NET 2.0ではPaddingはSystem.Windows.Forms名前空間に定義されているので、 //.NET 1.0/1.1のみ定義。 public struct Padding { public int Left {get {return left;} set {left = value;}} public int Top {get {return top;} set {top = value;}} public int Right {get {return right;} set {right = value;}} public int Bottom {get {return bottom;} set {bottom = value;}} public Padding(int padding) { this.left = this.top = this.right = this.bottom = padding; } public Padding(int leftRight, int topBottom) { this.left = this.right = leftRight; this.top = this.bottom = topBottom; } public Padding(int left, int top, int right, int bottom) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; } //空のパディング量を表します。 public static readonly Padding Empty; private int left; private int top; private int right; private int bottom; } #endif }
次にVB.NETのコード。
Imports System Imports System.Drawing Imports System.Runtime.InteropServices Imports System.Windows.Forms Namespace HongliangSoft.Utilities Public Class PaddedTextBox Inherits TextBox Private Declare Function SendMessage Lib "User32.dll" ( _ ByVal window As IntPtr, ByVal msg As Integer, _ ByVal wParam As IntPtr, ByRef lParam As Rect) As Integer Public Sub New () MyBase.Multiline = True End Sub Public Shadows ReadOnly Property Multiline() As Boolean Get Return MyBase.Multiline End Get End Property Protected NotInheritable Class Msg Public Const GetRect As Integer = &HB2 Public Const SetRect As Integer = &HB3 End Class Public Property TextPadding() As Padding Get Return m_textPadding End Get Set(ByVal Value As Padding) Me.m_textPadding = Value SetPadding(Value) End Set End Property Private m_textPadding As Padding Private Function GetPadding() As Padding Dim r As New Rect() SendMessage(Me.Handle, Msg.GetRect, IntPtr.Zero, r) Dim cli As Size = Me.ClientSize 'rectは上下左右の各位置の座標が入っているので、 'PaddingにするためにはClientSizeと比較する Return New Padding(r.Left, r.Top, cli.Width - r.Right, _ cli.Height - r.Bottom) End Function Private Sub SetPadding(ByVal value As Padding) Dim r As New Rect() r.Left = value.Left r.Top = value.Top Dim cli As Size = Me.ClientSize '丁度GetPaddingの時と同じ操作が必要(操作は同じだけど意味は逆) r.Right = cli.Width - value.Right r.Bottom = cli.Height - value.Bottom SendMessage(Me.Handle, Msg.SetRect, IntPtr.Zero, r) End Sub Protected Overrides Sub OnResize(ByVal e As EventArgs) 'リサイズしたときにフォーマット領域はクリアされるので、再指定 SetPadding(Me.m_textPadding) End Sub Public Structure Rect Public Left As Integer Public Top As Integer Public Right As Integer Public Bottom As Integer Public Property Width() As Integer Get Return Right - Left End Get Set(ByVal Value As Integer) Right = Left + Value End Set End Property Public Property Height() As Integer Get Return Bottom - Top End Get Set(ByVal Value As Integer) Bottom = Top + Value End Set End Property End Structure End Class '.NET 2.0ではPaddingはSystem.Windows.Forms名前空間に定義されているので、 '.NET 1.0/1.1のみ定義してください。 Public Structure Padding Public Property Left() As Integer Get Return m_left End Get Set(ByVal Value As Integer) m_left = Value End Set End Property Public Property Top() As Integer Get Return m_top End Get Set(ByVal Value As Integer) m_top = Value End Set End Property Public Property Right() As Integer Get Return m_right End Get Set(ByVal Value As Integer) m_right = Value End Set End Property Public Property Bottom() As Integer Get Return m_bottom End Get Set(ByVal Value As Integer) m_bottom = Value End Set End Property Public Sub New(ByVal value As Integer) Me.m_left = value Me.m_top = value Me.m_right = value Me.m_bottom = value End Sub Public Sub New(ByVal leftRight As Integer, ByVal topBottom As Integer) Me.m_left = leftRight Me.m_right = leftRight Me.m_top = topBottom Me.m_bottom = topBottom End Sub Public Sub New(ByVal left As Integer, ByVal top As Integer, _ ByVal right As Integer, ByVal bottom As Integer) Me.m_left = left Me.m_top = top Me.m_right = right Me.m_bottom = bottom End Sub '空のパディング量を表します。 Public Shared ReadOnly Empty As Padding Private m_left As Integer Private m_top As Integer Private m_right As Integer Private m_bottom As Integer End Structure End Namespace
こちらで公開されているコード(VB.NET版)のお陰で、
RichTextBoxに行番号を表示するという野望に
一歩近づけた気がします。
まだScrollBarと一体どうやって行番号を
表示するか?という課題が残っていますが・・・
とりあえず、とても勉強になるコードを公開していただいて感謝感謝です。