2005年10月10日

TextBoxと未実装メッセージ(3)

今回は久し振りに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



posted by Hongliang at 23:18| Comment(1) | TrackBack(0) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
GoogleでEM_SETRECTを検索して飛んできました。
こちらで公開されているコード(VB.NET版)のお陰で、
RichTextBoxに行番号を表示するという野望に
一歩近づけた気がします。
まだScrollBarと一体どうやって行番号を
表示するか?という課題が残っていますが・・・

とりあえず、とても勉強になるコードを公開していただいて感謝感謝です。
Posted by ken at 2007年07月11日 04:39
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。