RichTextBox for UWP
C1TextPointer の理解
C1Document オブジェクトの使い方 > C1TextPointer の理解

C1TextPointer クラスは、C1Document 内の位置を表します。その目的は、C1Document の走査と操作を容易にすることです。この機能は WPF の TextPointer クラスに似ていますが、オブジェクトモデルには多くの違いがあります。

C1TextPointer は、C1TextElement およびその内部のオフセットによって定義されます。ここでは、この図を例にして説明します。

上の図の青色のテキストのノードは、C1TextElement です。3つのオフセット位置が2つの C1Paragraph 要素の前、要素の間、および要素の後にマークされていることもわかります。マークされたオフセット位置は、C1Document 要素内の C1TextPointer の位置を示します。

C1Run 要素では、そのテキスト内の各文字がその要素の子と見なされ、オフセットはテキスト内の位置を示します。

C1InlineUIContainer は子を1つだけ持つ(それが表示する UIElement)と見なされ、その子の前と後の2つのオフセット位置があります。

ドキュメントを一連のシンボルとして可視化することもできます。ここで、シンボルは要素タグまたは何らかのタイプのコンテンツになります。要素タグは、要素の開始または終了を示します。そのため、上の図を XML で再作成すると、以下のようになります。

XAML
コードのコピー
<C1Document>
<C1Paragraph>
<C1Run>CAT</C1Run>
<C1InlineUIContainer><UI/></C1InlineUIContainer>
</C1Paragraph>
<C1Paragraph>
<C1Run>DOG</C1Run>
</C1Paragraph>
</C1Document>

このようにドキュメントを表示する場合、C1TextPointer はタグやコンテンツの間の位置をポイントします。このビューは、C1TextPointer に明確な順序を提供します。実際、C1TextPointer は IComparable を実装し、便宜のために比較演算子もオーバーロードします。

C1TextPointer の後にあるシンボルは、C1TextPointer.Symbol プロパティを使用して取得できます。このプロパティは、StartTagEndTagcharUIElement のいずれかのタイプのオブジェクトを返します。

ドキュメント内の位置を反復処理する場合は、GetPositionAtOffset と Enumerate という2つのメソッドを使用できます。GetPositionAtOffset は、低レベルのメソッドです。これは、SyntaxHighlight サンプルの次のコードでわかるように、単に指定された整数オフセットの位置を返します。

C#
コードのコピー
C1TextRange GetRangeAtTextOffset(C1TextPointer pos, Capture capture)
        {
            var start = pos.GetPositionAtOffset(capture.Index, C1TextRange.TextTagFilter);
            var end = start.GetPositionAtOffset(capture.Length, C1TextRange.TextTagFilter);
            return new C1TextRange(start, end);
        }

Enumerate は、位置を反復処理する場合にお勧めする方法です。このメソッドは、指定された方向にすべての位置に対して反復処理を行う IEnumerable<C1TextPointer> を返します。たとえば、次のコードはドキュメントのすべての位置を返します。

C#
コードのコピー
document.ContentStart.Enumerate()

ContentStart は C1TextElement の最初の C1TextPointer を返します。最後の位置を返す ContentEnd プロパティもあります。

Enumerate の興味深い点は、必要に応じて列挙を返すことです。つまり、IEnumerable が反復処理される場合にのみ C1TextPointer  オブジェクトが作成されます。これにより、フィルタ処理、検索、選択などのための LINQ 拡張メソッドを効率的に使用できます。たとえば、C1TextPointer の下に含まれる単語に対応する C1TextRange を取得するとします。次の手順を実行します。

Visual Basic コードの書き方

Visual Basic
コードのコピー
Private Function ExpandToWord(pos As C1TextPointer) As C1TextRange
    ' 単語の先頭を探します
    Dim wordStart = If(pos.IsWordStart, pos, pos.Enumerate(LogicalDirection.Backward).First(Function(p) p.IsWordStart))
 
    ' 単語の末尾を探します
    Dim wordEnd = If(pos.IsWordEnd, pos, pos.Enumerate(LogicalDirection.Forward).First(Function(p) p.IsWordEnd))
 
    ' 単語の先頭から末尾までの新しい範囲を返します
    Return New C1TextRange(wordStart, wordEnd)
End Function

C# コードの書き方

C#
コードのコピー
C1TextRange ExpandToWord(C1TextPointer pos)
{
    // 単語の先頭を探します
    var wordStart = pos.IsWordStart
        ? pos
        : pos.Enumerate(LogicalDirection.Backward).First(p => p.IsWordStart);
 
    // 単語の末尾を探します
    var wordEnd = pos.IsWordEnd
        ? pos
        : pos.Enumerate(LogicalDirection.Forward).First(p => p.IsWordEnd);
 
    // 単語の先頭から末尾までの新しい範囲を返します
    return new C1TextRange(wordStart, wordEnd);
}

Enumerate メソッドは、指定された方向で位置を検索して返しますが、現在の位置は含まれません。したがって、コードはパラメータの位置が単語の先頭かどうかを最初にチェックし、そうでない場合は逆方向に単語の先頭を検索します。単語の末尾についても同様に、パラメータの位置をチェックしてから順方向に検索します。パラメータの位置を含む単語を探しているので、順方向に移動して最初の単語の末尾を求め、逆方向に移動して最初の単語の先頭を求めます。C1TextPointer には、周囲のシンボルに基づいてその位置が単語の先頭または末尾かどうかを判別する IsWordStart プロパティと IsWordEnd プロパティが既に用意されています。ここでは、First LINQ 拡張メソッドを使用して、必要な述語を満たす最初の位置を探しています。最後に、2つの位置から C1TextRange を作成します。

LINQ 拡張メソッドは、位置を操作する際に大いに役立ちます。別の例として、次のようにするとドキュメント内の単語をカウントできます。

Visual Basic
コードのコピー
document.ContentStart.Enumerate().Count(Function(p) p.IsWordStart AndAlso TypeOf p.Symbol Is Char)
C#
コードのコピー
document.ContentStart.Enumerate().Count(p => p.IsWordStart && p.Symbol is char)

IsWordStart は、正確には単語の先頭ではない位置に対しても True を返すため、単語の先頭に続くシンボルが char かどうかをチェックする必要があることに注意してください。たとえば、C1Run の最初の位置が単語の先頭の場合、C1Run の開始タグの直前の位置は単語の先頭と見なされます。

もう1つの例として、Find メソッドを実装してみます。

Visual Basic コードの書き方

Visual Basic
コードのコピー
Private Function FindWordFromPosition(position As C1TextPointer, word As String) As C1TextRange
    ' テキストの長さが word.Length に等しいすべての範囲を取得します
    Dim ranges = position.Enumerate().[Select](Function(pos)
    ' word.Length オフセットにある位置を取得します
    ' ただし、テキストフローを変更しないタグは無視します
    Dim [end] = pos.GetPositionAtOffset(word.Length, C1TextRange.TextTagFilter)
    Return New C1TextRange(pos, [end])
 
End Function)
    ' 単語が見つからない場合の戻り値は null です
    Return ranges.FirstOrDefault(Function(range) range.Text = word)
End Function

C# コードの書き方

C#
コードのコピー
C1TextRange FindWordFromPosition(C1TextPointer position, string word)
{
    // テキストの長さが word.Length に等しいすべての範囲を取得します
    var ranges = position.Enumerate().Select(pos =>
    {
        // word.Length オフセットにある位置を取得します
        // ただし、テキストフローを変更しないタグは無視します
        var end = pos.GetPositionAtOffset(word.Length, C1TextRange.TextTagFilter);
        return new C1TextRange(pos, end);
    });
    // 単語が見つからない場合の戻り値は null です
    return ranges.FirstOrDefault(range => range.Text == word);
}

指定された位置から単語を見つけるために、順方向にすべての位置を列挙し、テキストの長さが word.Length のすべての範囲を選択します。それぞれの位置に対して、word.Length の距離にある位置を探します。そのために、GetPositionAtOffset メソッドを使用します。このメソッドは、指定されたオフセットの位置を返しますが、すべてのインラインタグも有効な位置として返します。ここでは、単語が2つの C1Run 要素の間で分割されている場合を考慮するために、これを無視する必要があります。C1TextRange.TextTagFilter を使用するのはそのためです。これは、内部ロジックでドキュメントツリーをテキストに変換する際に使用されるフィルタメソッドと同じです。最後の手順として、テキストが検索対象の単語に一致する範囲を検索します。

最後の例として、最初に見つかった単語を置換します。

Visual Basic コードの書き方

Visual Basic
コードのコピー
Dim wordRange = FindWordFromPosition(document.ContentStart, "cat")
If wordRange IsNot Nothing Then
    wordRange.Text = "dog"
End If

C# コードの書き方

C#
コードのコピー
var wordRange = FindWordFromPosition(document.ContentStart, "cat");
if (wordRange != null)
{
    wordRange.Text = "dog";
}

この例では、まず単語を検索し、次に C1TextRange.Text プロパティを割り当ててテキストを置換します。