1

As opposed to the TextBox control which use CrLf line endings the RichTextBox control use Lf line endings. I don't want that. I need consistency. I need a RichTextBox control whom use CrLf line endings.

I opened the control in reflector and noticed that the getter of the Text property calls the following function:

Private Function StreamOut(ByVal flags As Integer) As String
    '...
    Me.StreamOut(data, flags, False)
    '...
End Function

Which ends up calling:

Private Sub StreamOut(ByVal data As Stream, ByVal flags As Integer, ByVal includeCrLfs As Boolean)
    '...
    Dim es As New EDITSTREAM
    '...
    ElseIf includeCrLfs Then
        num = (num Or &H20)
    Else
    '...
    es.dwCookie = DirectCast(num, IntPtr)
    '...
End Sub

And as you can see, the includeCrLfs parameter will always be False.

So I subclassed the control and intercepted the EM_STREAMOUT message. The LParam of this message contains the pointer to the EDITSTREAM structure. I appended the &H20 flag as seen in the function above, but this didn't work. The Text property started to return an empty string. I believe that I might have to remove/append other flags, but I have no clue as to which flags. Also, MSDN do not provide any hints other than application-defined value.

dwCookie
Specifies an application-defined value that the rich edit control passes to the EditStreamCallback callback function specified by the pfnCallback member.

Here's my subclassed control:

Public Class UIRichTextBox
    Inherits System.Windows.Forms.RichTextBox

    Private Sub EmStreamOut(ByRef m As Message)
        Dim es As New EDITSTREAM
        es = DirectCast(Marshal.PtrToStructure(m.LParam, GetType(EDITSTREAM)), EDITSTREAM)
        If (IntPtr.Size = 4) Then
            Dim cookie As Int32 = es.dwCookie.ToInt32()
            'cookie = (cookie Or &H20I) '<- Didn't work
            es.dwCookie = New IntPtr(cookie)
        Else
            Dim cookie As Int64 = es.dwCookie.ToInt64()
            'cookie = (cookie Or &H20L) '<- Didn't work
            es.dwCookie = New IntPtr(cookie)
        End If
        Marshal.StructureToPtr(es, m.LParam, True)
        MyBase.WndProc(m)
    End Sub

    Protected Overrides Sub WndProc(ByRef m As Message)
        Select Case m.Msg
            Case EM_STREAMOUT
                Me.EmStreamOut(m)
                Exit Select
            Case Else
                MyBase.WndProc(m)
                Exit Select
        End Select
    End Sub

    Private Const EM_STREAMOUT As Integer = &H44A

    Private Delegate Function EDITSTREAMCALLBACK(ByVal dwCookie As IntPtr, ByVal buf As IntPtr, ByVal cb As Integer, <Out()> ByRef transferred As Integer) As Integer

    <StructLayout(LayoutKind.Sequential)> _
    Private Class EDITSTREAM
        Public dwCookie As IntPtr = IntPtr.Zero
        Public dwError As Integer
        Public pfnCallback As EDITSTREAMCALLBACK
    End Class

End Class

Update

So it turns out that the flags are not undocumented at all. They are part of the EM_GETEDITSTYLE and EM_SETEDITSTYLE messages. But as you can see, the flag is obsolete.

SES_USECRLF Obsolete. Do not use.

So I guess I'm back at square one overriding the text property.

Public Overrides Property Text() As String
    Get
        Dim value As String = MyBase.Text
        If (Not value Is Nothing) Then
            value = value.Replace(ChrW(13), "")
            value = value.Replace(ChrW(10), Environment.NewLine)
        End If
        Return value
    End Get
    Set(value As String)
        MyBase.Text = value
    End Set
End Property
Bjørn-Roger Kringsjå
  • 9,849
  • 6
  • 36
  • 64
  • Rather than tinkering with the undocumented internals why don't you just replace lf with crlf in the output string? – Alex K. Jun 06 '14 at 14:22
  • i don't see the problem. difference make, no it doesn't. in fact, i can copy+paste out of the box, into notepad and they get converted to CRLF, also when saving the text to a file, they also end up as CRLF (0D+0A). but i can confirm they are LF only in the RichTextBox. oh well, i can see how this would be a problem if comparing text ;o or just strip all 0D. also, the richtextbox itself has like 20-30 ascii characters appended to the start, which don't end up in the output. – porkchop Jun 06 '14 at 15:01
  • oh, those 30 chars are: `System.Windows.Forms.RichTextBox, Text: ` lmao ;p when you do a .ToString :) WTH? ;p `for each line in RTB.lines` :\ – porkchop Jun 06 '14 at 15:19
  • ah, if you use `RichTextBox.Rtf` property it returns a string with CRLF (0D & 0A) to replace LF (0A). so there is internal code there. you can also set that property to load text from file i suppose. but it also returns some RTF formatting codes for font, etc :\ – porkchop Jun 06 '14 at 15:29
  • Related C# question: [RichTextBox Newline Conversion?](https://stackoverflow.com/questions/7067899/richtextbox-newline-conversion) Just ran into this recently, this is very odd behavior. – jrh Nov 03 '17 at 12:15

1 Answers1

1

So I managed to create a working solution using reflection. I'm sure there must be a good reason as to why SES_USECRLF is obsolete, so proceed with caution.

Public Class UIRichTextBox
    Inherits System.Windows.Forms.RichTextBox

    Shared Sub New()
        UIRichTextBox.InternalEditStream = GetType(System.Windows.Forms.RichTextBox).GetField("editStream", (BindingFlags.NonPublic Or BindingFlags.Instance))
        UIRichTextBox.InternalStreamIn = GetType(System.Windows.Forms.RichTextBox).GetMethod("StreamIn", (BindingFlags.NonPublic Or BindingFlags.Instance), Nothing, New Type() {GetType(System.IO.Stream), GetType(System.Int32)}, Nothing)
        UIRichTextBox.InternalStreamOut = GetType(System.Windows.Forms.RichTextBox).GetMethod("StreamOut", (BindingFlags.NonPublic Or BindingFlags.Instance), Nothing, New Type() {GetType(System.IO.Stream), GetType(System.Int32), GetType(System.Boolean)}, Nothing)
    End Sub

    Public Sub New()
        Me.m_includeCrLfs = True
    End Sub

    <DefaultValue(True), Category("Behavior")> _
    Public Property IncludeCrLfs() As Boolean
        Get
            Return Me.m_includeCrLfs
        End Get
        Set(value As Boolean)
            If (value <> Me.m_includeCrLfs) Then
                Me.m_includeCrLfs = value
                Me.RecreateHandle()
            End If
        End Set
    End Property

    Public Overrides Property [Text]() As String
        Get
            Dim value As String = Nothing
            If (Me.StreamOut(&H11, value)) Then
                Return value
            End If
            Return MyBase.[Text]
        End Get
        Set(ByVal value As String)
            If (Not Me.StreamIn(value, &H11)) Then
                MyBase.[Text] = value
            End If
        End Set
    End Property

    Private Function StreamIn(ByVal str As String, ByVal flags As Integer) As Boolean
        If (((Me.IsHandleCreated AndAlso ((Not Me.IsDisposed) AndAlso (Not Me.Disposing))) AndAlso ((Not str Is Nothing) AndAlso (str.Length > 0))) AndAlso ((Not UIRichTextBox.InternalEditStream Is Nothing) AndAlso (Not UIRichTextBox.InternalStreamIn Is Nothing))) Then
            Dim bytes As Byte()
            Dim index As Integer = str.IndexOf(ChrW(0))
            If (index <> -1) Then
                str = str.Substring(0, index)
            End If
            If ((flags And &H10) <> 0) Then
                bytes = Encoding.Unicode.GetBytes(str)
            Else
                bytes = Encoding.Default.GetBytes(str)
            End If
            Dim data As New System.IO.MemoryStream()
            UIRichTextBox.InternalEditStream.SetValue(Me, data)
            data.Write(bytes, 0, bytes.Length)
            data.Position = 0
            UIRichTextBox.InternalStreamIn.Invoke(Me, New Object() {data, flags})
            Return True
        End If
        Return False
    End Function

    Private Function StreamOut(ByVal flags As Integer, ByRef result As String) As Boolean
        If ((Me.IsHandleCreated AndAlso ((Not Me.IsDisposed) AndAlso (Not Me.Disposing))) AndAlso (Not UIRichTextBox.InternalStreamOut Is Nothing)) Then
            Dim data As New System.IO.MemoryStream()
            UIRichTextBox.InternalStreamOut.Invoke(Me, New Object() {data, flags, Me.m_includeCrLfs})
            data.Position = 0
            Dim length As Integer = CInt(data.Length)
            Dim str As String = String.Empty
            If (length > 0) Then
                Dim buffer As Byte() = New Byte(length - 1) {}
                data.Read(buffer, 0, length)
                If ((flags And &H10) <> 0) Then
                    str = Encoding.Unicode.GetString(buffer, 0, buffer.Length)
                Else
                    str = Encoding.Default.GetString(buffer, 0, buffer.Length)
                End If
                If ((Not String.IsNullOrEmpty(str)) AndAlso (str.Chars((str.Length - 1)) = ChrW(0))) Then
                    str = str.Substring(0, (str.Length - 1))
                End If
            End If
            result = str
            Return True
        End If
        Return False
    End Function

    Private Shared ReadOnly InternalEditStream As FieldInfo
    Private Shared ReadOnly InternalStreamIn As MethodInfo
    Private Shared ReadOnly InternalStreamOut As MethodInfo

    Private m_includeCrLfs As Boolean

End Class
Bjørn-Roger Kringsjå
  • 9,849
  • 6
  • 36
  • 64