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

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

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

上の青色の角かっこで囲まれたノードは C1TextElement で、C1TextPointer のオフセットは、その位置がどの子の間にあるかを示します。たとえば、上の C1Document をオフセット0でポイントする位置は最初の C1Paragraph の直前、オフセット1は2つの段落の間、オフセット2は2番目の段落の後を示します。C1TextPointer が C1Run をポイントする場合は、そのテキスト内の各文字が C1Run の子と見なされ、オフセットはテキスト内の位置を示します。C1InlineUIContainer は子を1つだけ持つ(それが表示する UIElement)と見なされ、その子の前と後という2つの位置があります。

ドキュメントを一連のシンボルとして可視化する方法もあります。ここで、シンボルは要素タグまたは何らかのタイプのコンテンツになります。要素タグは、要素の開始または終了を示します。XML では、上のドキュメントは次のように記述されます。

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

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

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

ドキュメント内の位置を反復処理する場合は、GetPositionAtOffsetEnumerate という2つのメソッドを使用できます。GetPositionAtOffset は低レベルのメソッドで、単に指定された整数オフセットの位置を返します。Enumerate は、位置を反復処理する場合にお勧めする方法です。このメソッドは、指定された方向にすべての位置に対して反復処理を行う IEnumerable を返します。たとえば、次のコードはドキュメントのすべての位置を返します。

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

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

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

コードのコピー
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
コードのコピー
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 拡張メソッドは、位置を操作する際に大いに役立ちます。別の例として、次のようにするとドキュメント内の単語をカウントできます。

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

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

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

コードのコピー
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
コードのコピー
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 を使用するのはそのためです。これは、内部ロジックでドキュメントツリーをテキストに変換する際に使用されるフィルタメソッドと同じです。最後の手順として、テキストが検索対象の単語に一致する範囲を検索します。

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

コードのコピー
Dim wordRange = FindWordFromPosition(document.ContentStart, "cat")
If wordRange IsNot Nothing Then
   wordRange.Text = "dog"
End If
コードのコピー
var wordRange = FindWordFromPosition(document.ContentStart, "cat");
if (wordRange != null)
{
   wordRange.Text = "dog";
}

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