none
RichTextBoxにおけるCaretPositionの左のInline要素を取得する RRS feed

  • 質問

  • 環境: .NET Framework 4.6.1

    お世話になります。

    Run要素とInlineUIContainer要素が混在したRichTextBoxでカーソルの左の要素を取得したく思います。
    以下のコードを書きましたが、要素を取得することができませんでした。

            private void RichTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
            {
                switch (e.Key)
                {
                    case Key.F2:
                        var rtb = (RichTextBox)sender;
                        var crt = rtb.CaretPosition;
    
                        var targetInline =
                            from prg in rtb.Document.Blocks.OfType<Paragraph>()
                            select prg.Inlines.Single(inline => inline.ContentEnd.CompareTo(crt) == 0);
    
                        Inline i = targetInline.First();
    
                        MessageBox.Show(i.ToString());
    
                        break;
                }
    
                return;
            }

    カーソルの左の要素がRunであるときは正常に動作しますが、InlineUIContainerであるときは一致する要素が存在しない旨の例外が発生します。
    (カーソルがRun内に存在するときもエラーが発生しますがここでは考えません)

    カーソルの左のInlineUIContainerを取得するにはどうしたらよいでしょうか?

    2016年9月19日 13:19

回答

  • 左の要素というのがInlineであるという意味であれば。
    (Blockも含めるのであれば、たとえばParagraph内で先頭のRunの左の要素はParagraphの先頭になるので)

    InlineUIContainerは例えば以下のようにRunの間にInlineUIContainerを配置していても、実際に実行されるときにはInlineUIConterだけを包むParagraphに配置されてしまうので、Runとは同レベルには存在しないです。
    つまり、

    <Run>A</Run><InlineUIContainer ><TextBox Text="T" /></InlineUIContainer><Run>◆B</Run>

    <Paragraph><Run>A</Run></Paragraph><Paragraph><InlineUIContainer ><TextBox Text="T" /></InlineUIContainer></Paragraph><Paragraph><Run>◆B</Run></Paragraph>

    になってしまいます。
    そのため◆にカーソルがあるときの左には、Runの先頭、Paragraphの末尾、Inlineの末尾の順にあることになります。

    というわけで、Documentの先頭方向へ順番に位置をずらしていき、最初にぶつかるInlineを取得してみる。

    private void RichTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        switch (e.Key)
        {
            case Key.F2:
                var rtb = (RichTextBox)sender;
                var crt = rtb.CaretPosition;
    
                //crt = rtb.Selection.Start;//文字選択中などはCaretと選択開始位置は異なる場合あり
    
                var elem = GetLeftInline(rtb, crt.GetInsertionPosition(LogicalDirection.Backward));
                if (elem is Run)
                {
                    MessageBox.Show(((Run)elem).Text);
                }
                else if (elem is InlineUIContainer)
                {
                    MessageBox.Show("InlineUIContainer");
                }
                break;
        }
    
        return;
    }
    
    private Inline GetLeftInline(RichTextBox rtb, TextPointer p)
    {
        while (p != null)
        {
            switch (p.GetPointerContext(LogicalDirection.Backward))
            {
                case TextPointerContext.ElementStart://Runの先頭
                    break;
                case TextPointerContext.ElementEnd:
                    if (p.Parent is Run)
                    {//Runの末尾
                        return (Run)p.Parent;
                    }
                    else
                    {//Paragraphの末尾など
                    }
                    break;
                case TextPointerContext.EmbeddedElement://埋め込み要素
                    return p.Parent as Inline;
                case TextPointerContext.None:
                    //左には何もなかった(つまりドキュメントの先頭)
                    break;
                case TextPointerContext.Text://Runの中
                    return (Run)p.Parent;
            }
            p = p.GetPositionAtOffset(-1);//Documentの先頭に向かってずらして再調査
        }
        return null;//Documentの先頭に到達
    }

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2016年9月19日 15:40
    • 回答としてマーク 指計算機 2016年9月20日 11:24
    2016年9月19日 15:02

すべての返信

  • 左の要素というのがInlineであるという意味であれば。
    (Blockも含めるのであれば、たとえばParagraph内で先頭のRunの左の要素はParagraphの先頭になるので)

    InlineUIContainerは例えば以下のようにRunの間にInlineUIContainerを配置していても、実際に実行されるときにはInlineUIConterだけを包むParagraphに配置されてしまうので、Runとは同レベルには存在しないです。
    つまり、

    <Run>A</Run><InlineUIContainer ><TextBox Text="T" /></InlineUIContainer><Run>◆B</Run>

    <Paragraph><Run>A</Run></Paragraph><Paragraph><InlineUIContainer ><TextBox Text="T" /></InlineUIContainer></Paragraph><Paragraph><Run>◆B</Run></Paragraph>

    になってしまいます。
    そのため◆にカーソルがあるときの左には、Runの先頭、Paragraphの末尾、Inlineの末尾の順にあることになります。

    というわけで、Documentの先頭方向へ順番に位置をずらしていき、最初にぶつかるInlineを取得してみる。

    private void RichTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        switch (e.Key)
        {
            case Key.F2:
                var rtb = (RichTextBox)sender;
                var crt = rtb.CaretPosition;
    
                //crt = rtb.Selection.Start;//文字選択中などはCaretと選択開始位置は異なる場合あり
    
                var elem = GetLeftInline(rtb, crt.GetInsertionPosition(LogicalDirection.Backward));
                if (elem is Run)
                {
                    MessageBox.Show(((Run)elem).Text);
                }
                else if (elem is InlineUIContainer)
                {
                    MessageBox.Show("InlineUIContainer");
                }
                break;
        }
    
        return;
    }
    
    private Inline GetLeftInline(RichTextBox rtb, TextPointer p)
    {
        while (p != null)
        {
            switch (p.GetPointerContext(LogicalDirection.Backward))
            {
                case TextPointerContext.ElementStart://Runの先頭
                    break;
                case TextPointerContext.ElementEnd:
                    if (p.Parent is Run)
                    {//Runの末尾
                        return (Run)p.Parent;
                    }
                    else
                    {//Paragraphの末尾など
                    }
                    break;
                case TextPointerContext.EmbeddedElement://埋め込み要素
                    return p.Parent as Inline;
                case TextPointerContext.None:
                    //左には何もなかった(つまりドキュメントの先頭)
                    break;
                case TextPointerContext.Text://Runの中
                    return (Run)p.Parent;
            }
            p = p.GetPositionAtOffset(-1);//Documentの先頭に向かってずらして再調査
        }
        return null;//Documentの先頭に到達
    }

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2016年9月19日 15:40
    • 回答としてマーク 指計算機 2016年9月20日 11:24
    2016年9月19日 15:02
  • gekka様

    素早い回答ありがとうございます。

    >実際に実行されるときにはInlineUIConterだけを包むParagraphに配置されてしまう
    そんなことになっていたのですね・・・
    ライブビジュアルツリーを見ても「フロードキュメントの概要」を読んでも何ともわかりませんでした。

    取得するコードまで書いていただきありがとうございます!早速試してみたいと思います!

    2016年9月20日 11:09