1. Herfried K. Wagner’s VB.Any
  2. .NET
  3. Frequently Asked Questions

Getting and setting the position of the caret in a textbox control

Getting and setting the position of the caret in a textbox control
<URL:https://dotnet.currifex.org/dotnet/faqs/textboxcaretpos/en/>
----------------------------------------------------------------------------

Getting and setting the position of the caret in a textbox control

The 'TextBox' class shipped as part of .NET's Windows Forms does not
support querying and setting the position of the caret.  Not even the
underlying 'EDIT' control class provides messages for this purpose.
Instead, it is necessary to utilize the functions 'GetCaretPos',
'SetCaretPos', and 'SendMessage' in conjunction with the 'EM_POSFROMCHAR'
and 'EM_CHARFROMPOS' messages.  The code below contains an implementation
of an extended textbox control providing a 'CaretPosition' property which
supports both getting and setting the position of the textbox' caret.  In
addition to that a handy replacement for the Win32 macros 'MAKELONG',
'LOWORD', and 'HIWORD' is supplied.

\\\
Imports MvpsOrg.Dotnet.InteropHelpers.WordConverter
Imports System.ComponentModel
Imports System.Runtime.InteropServices

Public Class ExtendedTextBox
    Inherits TextBox

    Private Declare Auto Function SendMessage Lib "user32.dll" ( _
        ByVal hWnd As IntPtr, _
        ByVal Msg As Int32, _
        ByVal wParam As IntPtr, _
        ByVal lParam As IntPtr _
    ) As IntPtr

    Private Declare Auto Function GetCaretPos Lib "user32.dll" ( _
        ByRef lpPoint As POINTAPI _
    ) As Boolean

    Private Declare Auto Function SetCaretPos Lib "user32.dll" ( _
        ByVal x As Int32, _
        ByVal y As Int32 _
    ) As Boolean

    Private Const EM_POSFROMCHAR As Int32 = &HD6
    Private Const EM_CHARFROMPOS As Int32 = &HD7

    <StructLayout(LayoutKind.Sequential)> _
    Private Structure POINTAPI
        Public x As Int32
        Public y As Int32
    End Structure

    Private Declare Auto Function DrawText Lib "user32.dll" ( _
        ByVal hDC As IntPtr, _
        ByVal lpStr As String, _
        ByVal nCount As Int32, _
        ByRef lpRect As RECT, _
        ByVal wFormat As Int32 _
    ) As Int32

    Private Const DT_CALCRECT As Int32 = &H400

    Private Structure RECT
        Public Left As Int32
        Public Top As Int32
        Public Right As Int32
        Public Bottom As Int32
    End Structure

    Private Declare Function SelectObject Lib "gdi32.dll" ( _
        ByVal hdc As IntPtr, _
        ByVal hObject As IntPtr _
    ) As IntPtr

    < _
        DesignerSerializationVisibility( _
            DesignerSerializationVisibility.Hidden _
        ) _
    > _
    Public Property CaretPosition() As Integer
        Get
            Dim pt As POINTAPI
            If Not GetCaretPos(pt) Then
                Throw New Win32Exception()
                Exit Property
            End If
            Return _
                LoWord( _
                    SendMessage( _
                        Me.Handle, _
                        EM_CHARFROMPOS, _
                        IntPtr.Zero, _
                        New IntPtr(MakeLong(pt.x, pt.y)) _
                    ).ToInt32() _
                )
        End Get
        Set(ByVal Value As Integer)
            Dim x As Integer
            If Value >= Me.TextLength Then
                Value = Me.TextLength - 1
                Dim g As Graphics = Me.CreateGraphics()
                Dim hDC As IntPtr = g.GetHdc()
                Dim hNewFont As IntPtr = Me.Font.ToHfont()
                Dim hOldFont As IntPtr = SelectObject(hDC, hNewFont)
                Dim rct As RECT
                rct.Right = Int32.MaxValue
                rct.Bottom = Int32.MaxValue
                If _
                    DrawText( _
                        hDC, _
                        Strings.Right(Me.Text, 1), _
                        1, _
                        rct, _
                        DT_CALCRECT _
                    ) = 0 _
                Then
                    Throw New Win32Exception()
                    Exit Property
                End If
                x = rct.Right
                SelectObject(hDC, hOldFont)
                g.ReleaseHdc(hDC)
                g.Dispose()
            End If
            Dim n As Int32 = _
                SendMessage( _
                    Me.Handle, _
                    EM_POSFROMCHAR, _
                    New IntPtr(Value), _
                    IntPtr.Zero _
                ).ToInt32()
            x += LoWord(n)
            Dim y As Integer = HiWord(n)

            ' HACK:  Is there any other way to get the Y-offset of the
            ' caret?
            Select Case Me.BorderStyle
                Case BorderStyle.Fixed3D
                    y += 1
                Case BorderStyle.FixedSingle
                    y += 2
                'Case BorderStyle.None
                '    y += 0
            End Select

            If Not SetCaretPos(x, y) Then
                Throw New Win32Exception()
                Exit Property
            End If
        End Set
    End Property
End Class

Public Class WordConverter
    <StructLayout(LayoutKind.Explicit)> _
    Private Structure DWord
        <FieldOffset(0)> Public LongValue As Integer
        <FieldOffset(0)> Public LoWord As Short
        <FieldOffset(2)> Public HiWord As Short
    End Structure

    Private Shared m_DWord As DWord

    Public Shared Function MakeLong( _
        ByVal LoWord As Short, _
        ByVal HiWord As Short _
    ) As Integer
        m_DWord.LoWord = LoWord
        m_DWord.HiWord = HiWord
        Return m_DWord.LongValue
    End Function

    Public Shared Function MakeLong( _
        ByVal LoWord As Integer, _
        ByVal HiWord As Integer _
    ) As Integer
        Return MakeLong(CShort(LoWord), CShort(HiWord))
    End Function

    Public Shared Function LoWord(ByVal LongValue As Integer) As Short
        m_DWord.LongValue = LongValue
        Return m_DWord.LoWord
    End Function

    Public Shared Function HiWord(ByVal LongValue As Integer) As Short
        m_DWord.LongValue = LongValue
        Return m_DWord.HiWord
    End Function
End Class
///