Search code examples
c#.netwpfvb.nettextblock

dynamically change text decorations on multiple spans/regions in a wpf textblock


I am trying to design an app that strikes out certain characters on a string if another 1 or 2 strings are contained within that string.

So far I can seem to get the leading sub string to do its thing fairly reliably, but the second sub-string always seems to produce unpredictable results, can anyone help? (c# or vb is fine)

A screen snip example of it working for the main (1st) filter, but not the sub (2nd): enter image description here

Heres the code:

Public Function FormatFilteredName(mainFilter As String, subFilter As String) As TextBlock
    Dim tbNew As New TextBlock(New Run(FullFileName))
    tbNew.Text = FullFileName
    tbNew.FontSize = FONT_SIZE
    If Not String.IsNullOrEmpty(FullFileName) Then
        If Not String.IsNullOrEmpty(mainFilter) Then
            GetFilterSpan(mainFilter, tbNew)
        End If
        If Not String.IsNullOrEmpty(subFilter) Then
            GetFilterSpan(subFilter, tbNew)
        End If
    End If
    Return tbNew

Private Function GetFilterSpan(filter As String, ByVal tbNew As TextBlock) As Span
    Dim offset As Integer = tbNew.Text.ToLower().IndexOf(filter.ToLower()) + 1
    Try
        If offset > -1 Then
            Dim tpStart As TextPointer
            tpStart = tbNew.ContentStart.GetPositionAtOffset(offset)
            Dim tpEnd As TextPointer
            tpEnd = tbNew.ContentStart.GetPositionAtOffset(offset + filter.Length)
            If Not tpStart Is Nothing And Not tpEnd Is Nothing Then
                Dim result As New Span(tpStart, tpEnd)
                result = ApplySpanStrikeOutStyle(result)
                Return result
            End If
        End If
    Catch ex As Exception
        Return Nothing
    End Try

Solution

  • If you run your code with a watch set on tbNew.ContentEnd.Offset, you'll see the value change as the strikethroughs are applied. That illustrates why using

    Dim offset As Integer = tbNew.Text.ToLower().IndexOf(filter.ToLower()) + 1

    is not accurate - you're getting the IndexOf from the plain text, and then applying that offset to content which has text decorations applied.

    From the answer here, this should get you on your way.

      Public Function FormatFilteredName(mainFilter As String, subFilter As String) As TextBlock
            ' Dim tbNew As New TextBlock(New Run("111xxJoe Blogs09"))
            ' tbNew.Text = "111xxJoe Blogs09"
            ' tbNew.FontSize = 10
            If Not String.IsNullOrEmpty(tbNew.Text) Then
                If Not String.IsNullOrEmpty(mainFilter) Then
                    Dim mainFilterRange As TextRange = FindWordFromPosition(tbNew.ContentStart, mainFilter)
                    If mainFilterRange IsNot Nothing Then
                        ApplyStrikeOutStyle(mainFilterRange)
                    End If
                End If
                If Not String.IsNullOrEmpty(subFilter) Then
                    Dim subFilterRange As TextRange = FindWordFromPosition(tbNew.ContentStart, subFilter)
                    If subFilterRange IsNot Nothing Then
                        ApplyStrikeOutStyle(subFilterRange)
                    End If
                End If
            End If
            Return tbNew
        End Function
    
    
    
        Private Function ApplyStrikeOutStyle(result As TextRange) As TextRange
            result.ApplyPropertyValue(Inline.TextDecorationsProperty,
                                 TextDecorations.Strikethrough)
            Return result
        End Function
    
    
        Private Function FindWordFromPosition(position As TextPointer, word As String) As TextRange
            While position IsNot Nothing
                If position.GetPointerContext(LogicalDirection.Forward) = TextPointerContext.Text Then
                    Dim textRun As String = position.GetTextInRun(LogicalDirection.Forward)
    
                    ' Find the starting index of any substring that matches "word".
                    Dim indexInRun As Integer = textRun.IndexOf(word)
                    If indexInRun >= 0 Then
                        Dim start As TextPointer = position.GetPositionAtOffset(indexInRun)
                        Dim [end] As TextPointer = start.GetPositionAtOffset(word.Length)
                        Return New TextRange(start, [end])
                    End If
                End If
    
                position = position.GetNextContextPosition(LogicalDirection.Forward)
            End While
    
            ' position will be null if "word" is not found.
            Return Nothing
        End Function