none
[WPF]RichTextBoxへの入力時に適用されるフォントスタイルについて RRS feed

  • 質問

  • [環境]
    .NET 3.0

    [質問内容]
    RichTextBoxにて、フォントスタイル(注1)の異なる文字列の間にカーソルがある状態でキー入力をした際
    次の通り動作します。

    ///////////////////////////////////////////////////////////////////////////////

    例)
    1.フォントスタイルの異なる「あいう」と「かきく」がRichTextBoxに表示されている
    2.「う」と「か」の間にカーソルをあてる
    3.キー入力する
     (1)半角英数モードで入力した場合(「a」を入力)
      ・カーソルの左の文字「う」のフォントスタイルで「a」は入力(確定)される

     (2)半角英数モード以外で入力した場合
      i)変換前の場合
       例)
        - 「あ」を入力するための「a」キー押下時
        - 「か」を入力するための「k」キー押下時
        - 子音の英字や数字、記号キー押下時 など

       ・カーソルの右の文字「か」のフォントスタイルで入力される(まだ確定されていないがEnter押下でそのフォントスタイルで確定される) ★問題点★

      i)変換後の場合
       例)
        - 「か」を入力するための(「k」キー押下の後の)「a」キー押下時(自動的に「ka」→「か」に変換される)
        - 「あ」を「阿」にするための「変換」キー押下時 など

       ・カーソルの左の文字「う」のフォントスタイルでは入力される(まだ確定されていないがEnter押下でそのフォントスタイルで確定される)

    ///////////////////////////////////////////////////////////////////////////////

    (2)のi)の時点でもカーソルの左の文字のフォントスタイルで表示させたいのですが
    うまいこといきません。
    同じような問題をした経験がある方など、ご教示ください。

    注1)
    ・フォントファミリー
    ・フォントサイズ
    ・背景色
    ・文字色 など
    2012年11月7日 0:59

回答

  • 正攻法ではないんでアレかもしれませんが、WindowsFormsHostを使ってWinForm用のRichTextBoxを使うという手段はどうでしょうか?
    さっき試してみたら、IME2010/ATOKとも問題無く入力できてました。

    • 回答としてマーク KiKoKiKoMiMi 2012年11月16日 4:40
    2012年11月9日 4:32

すべての返信

  • IMEがどうこうというのは関係無くて、RichTextBox.CaretPositionに紐付いている要素が「あいう」の要素なのか「かきく」の要素なのかによってその後の動作が変わってきます。
    「あいう」の要素の末尾にあるなら次の文字は「あいう○」というようになりますし、「かきく」の要素の先頭になるなら「○かきく」となるわけです。
    ですから(2)のi)の状態というのは、「かきく」の要素の先頭が選択されている状態になります。

    で、対処法ですが、CaretPositionが要素の境にあって後の要素が選択されている場合に、前の要素を格納するようにしてやればOKです。

            private void richTextBox_SelectionChanged(object sender, RoutedEventArgs e)
            {
                if (richTextBox.Selection.Text == "" && richTextBox.CaretPosition.LogicalDirection == LogicalDirection.Forward)
                {
                    Inline inline = richTextBox.CaretPosition.Parent as Inline;
    
                    if (inline != null && inline.PreviousInline != null && richTextBox.CaretPosition.CompareTo(inline.ContentStart) == 0)
                    {
                        richTextBox.CaretPosition = inline.PreviousInline.ElementEnd;
                    }
                }
            }

    以上、参考になれば幸いです。

    • 編集済み みっと 2012年11月7日 5:50
    2012年11月7日 5:46
  • みっとさん

    返信ありがとうございます。

    勉強になりました。そういうことなんですね。

    ただ、ご教示いただいた内容で、「CaretPositionが要素の境にあって後の要素が選択されている場合に、前の要素を格納する」ようにしてみたのですが

    動きは変わりませんでした。

    変換が掛かる前までは、結局後の要素のフォントスタイルで表示されます。

    2012年11月7日 8:10
  • >> ただ、ご教示いただいた内容で、「CaretPositionが要素の境にあって後の要素が選択されている場合に、前の要素を格納する」ようにしてみたのですが
    >> 動きは変わりませんでした。
    >> 変換が掛かる前までは、結局後の要素のフォントスタイルで表示されます。

    あー・・・もしやと思いIMEをATOKからIME2010に変えた所、確かに現象が再現できましたorz
    ということはText Service Framwork絡みの問題ですね。
    一応前後の要素の値をウォッチしたところ、CaretPositionはそのままですが文字の格納先の要素がTextInputStartとそれ以外で変わるようです。
    こうなると(私には)ちょっとお手上げですね・・・確定した文字列自体は変換終了イベント(TextInput)あたりで入れ直せばどうにかできると思いますが、入力途中の見た目はどうにもならない気がします。

    ・・・ていうか変換の1手目と2手目以降で格納先が異なるとか、そこはかとなくバグ臭い・・・(苦笑)

    2012年11月7日 9:21
  • TextChangedイベントで、変換前の文字列をTextRangeで取得して、FontFamilyプロパティなどを更新しているのですが、反映されないですね。

    確定前の文字列に対して、フォントスタイルを設定するのはできないようです。

    みっとさんの言われるとおり、変換前のフォントスタイルは「なすがまま」状態です。

    実は、カーソル位置の左の文字のフォントスタイルで表示(入力)されるようにするのは第1段階で、

    第2段階としては、ワードのようにカーソル位置(未選択の状態)にフォントスタイルを設定できて、その内容で入力(表示)できる

    とこまで考えていたのですが・・・

    2012年11月9日 0:38
  • 正攻法ではないんでアレかもしれませんが、WindowsFormsHostを使ってWinForm用のRichTextBoxを使うという手段はどうでしょうか?
    さっき試してみたら、IME2010/ATOKとも問題無く入力できてました。

    • 回答としてマーク KiKoKiKoMiMi 2012年11月16日 4:40
    2012年11月9日 4:32
  • こんな

    //<RichTextBox TextCompositionManager.TextInputUpdate="Composition_TextInputUpdate" />
    
    private void Composition_TextInputUpdate(object sender, TextCompositionEventArgs e)
    {
        Action<RichTextBox> act = UpdateCompositionStyle;
        Dispatcher.BeginInvoke(act, System.Windows.Threading.DispatcherPriority.Loaded, sender as RichTextBox);
    }
    private void UpdateCompositionStyle(RichTextBox rich)
    {
        if (rich != null)
        {
            var compositionInline = rich.Selection.Start.Parent as Run;
            if (compositionInline != null && compositionInline.PreviousInline != null)
            {
                var previousInline = compositionInline.PreviousInline;
                compositionInline.FontFamily = previousInline.FontFamily;
                compositionInline.FontSize = previousInline.FontSize;
                compositionInline.FontStyle = previousInline.FontStyle;
                compositionInline.FontWeight = previousInline.FontWeight;
                compositionInline.Foreground = previousInline.Foreground;
                compositionInline.Background = previousInline.Background;
            }
        }
    }

    ポイント

    • IME入力中の文字列はRichTextBoxが表示している。
    • RichTextBoxはIME入力が開始されると新しいInline(Run)を挿入して、そこに入力中の文字列を表示している。
    • 入力イベント直後ではInlineが挿入されていないので、遅延させて検出する必要がある。
    • 入力中のInlineが捕捉できればInlineに対して好き勝手し放題。

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

    2012年11月9日 14:54
  • みっとさん

    返信ありがとうございます。

    気にはなっていたのですが、WindowsForm用だと問題ないんですね。

    ただ、すでにWPF用のRichTextBoxでシステムは動いています。

    WindowsFormのRichTextBoxに置きかえることで、予想もしない動きの違いなどが出てきそうで

    WindowsFormの案は手を出せずにいます。

    2012年11月10日 16:19
  • gekkaさん、返信ありがとうございます。

    たいへん勉強になりました。

    ただ、「RichTextBoxはIME入力が開始されると新しいInline(Run)を挿入して、そこに入力中の文字列を表示している。」 についてですが

    デバッグした感じだと、Inlineは挿入されず、カーソルの右または左のInlineに文字が追加されていってるような気がします。

    そもそも自由にInlineが追加できれば、ワードのようにカーソル位置(未選択の状態)にフォントスタイルを設定できるようになると思うのですが。。。

    2012年11月10日 16:46
  • 目的の動作を実現できませんでしたか?
    デバッグした感じだと、Inlineは挿入されず、カーソルの右または左のInlineに文字が追加されていってるような気がします。

    こんな風にしてFlowDocumentの構造を監視してみてください。
    (RichTextBoxのドキュメントはWordPadなどから貼り付けてください)

    <Window x:Class="WpfApplication1.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" Height="350" Width="525">
        <DockPanel>
            <RichTextBox TextCompositionManager.TextInputUpdate="Composition_TextInputUpdate"
                         Width="200" x:Name="rich"/>
            <TreeView ItemsSource="{Binding}" x:Name="treeView1">
                <TreeView.ItemContainerStyle>
                    <Style TargetType="{x:Type TreeViewItem}">
                        <Setter Property="IsExpanded" Value="True"/>
                    </Style>
                </TreeView.ItemContainerStyle>
    
                <TreeView.ItemTemplate>
                    <HierarchicalDataTemplate ItemsSource="{Binding Items}">
                        <TextBlock Text="{Binding Name}" />
                    </HierarchicalDataTemplate>
                </TreeView.ItemTemplate>
            </TreeView>
        </DockPanel>
    </Window>
    namespace WpfApplication1
    {
        using System;
        using System.Collections.Generic;
        using System.Windows;
        using System.Windows.Controls;
        using System.Windows.Documents;
        using System.Windows.Input;
        using System.Windows.Media;
        using System.Windows.Threading;
    
        public partial class MainWindow : Window
        {
            public MainWindow()
            {
                InitializeComponent();
    
                timer = new DispatcherTimer(DispatcherPriority.Background);
                timer.Interval = new TimeSpan(0, 0, 5);
                timer.Tick += new EventHandler(timer_Tick);
                timer.Start();
    
            }
            private System.Windows.Threading.DispatcherTimer timer;
    
            void timer_Tick(object sender, EventArgs e)
            {
                this.treeView1.DataContext = new Item[] { new Item(rich.Document) };
            }
    
            private void Composition_TextInputUpdate(object sender, TextCompositionEventArgs e)
            {
                Action<RichTextBox> act = UpdateCompositionStyle;
                Dispatcher.BeginInvoke(act, DispatcherPriority.Loaded, sender as RichTextBox);
            }
            private void UpdateCompositionStyle(RichTextBox rich)
            {
                if (rich != null)
                {
                    var compositionInline = rich.Selection.Start.Parent as Inline;
                    if (compositionInline != null && compositionInline.PreviousInline != null)
                    {
                        var previousInline = compositionInline.PreviousInline;
                        compositionInline.FontFamily = previousInline.FontFamily;
                        compositionInline.FontSize = previousInline.FontSize;
                        compositionInline.FontStyle = previousInline.FontStyle;
                        compositionInline.FontWeight = previousInline.FontWeight;
                        compositionInline.Foreground = previousInline.Foreground;
                        compositionInline.Background = previousInline.Background;
                    }
                }
            }
    
    
        }
    
        class Item
        {
            public Item(FlowDocument doc)
            {
                this.Items = new List<Item>();
                foreach (Block block in doc.Blocks)
                {
                    this.Items.Add(new Item(block));
                }
                this.Name = "FlowDocument";
            }
    
            public Item(TextElement element)
            {
    
                this.Items = new List<Item>();
    
                Run run = element as Run;
                if (run != null)
                {
                    this.Name = "Run : " + run.Text;
                    return;
                }
                else
                {
                    foreach (object o in LogicalTreeHelper.GetChildren(element))
                    {
                        Visual visual = o as Visual;
                        if (visual != null)
                        {
                            this.Items.Add(new Item(visual));
                        }
                        TextElement elem = o as TextElement;
                        if (elem != null)
                        {
                            this.Items.Add(new Item(elem));
                        }
                    }
    
                    this.Name = element.GetType().Name;
                }
            }
    
            public Item(Visual visual)
            {
                this.Items = new List<Item>();
                int count = VisualTreeHelper.GetChildrenCount(visual);
                for (int i = 0; i < count; i++)
                {
                    var child = VisualTreeHelper.GetChild(visual, i) as Visual;
                    if (child != null)
                    {
                        this.Items.Add(new Item(child));
                    }
                }
                Name = visual.GetType().Name;
            }
    
            public List<Item> Items { get; private set; }
            public string Name { get; private set; }
        }
    }

    そもそも自由にInlineが追加できれば、ワードのようにカーソル位置(未選択の状態)にフォントスタイルを設定できるようになると思うのですが。。。

    UpdateCompositionStyle内で、例えばFontSizeなどに自由なサイズを設定してみてください。前後のFontSizeに無関係なサイズで表示されるようになるはずです。さらに確定された文字も見てください。異なったフォントスタイルを設定できていませんか?


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

    2012年11月11日 3:38
  • gekkaさん、返信ありがとうございます。
    FrowDocumentの内容が見やすく助かっています。

    >目的の動作を実現できませんでしたか?
    やり方が正しくないのでしょうか。
    以下のとおりやっています。

    「あいうかきく」("あいう"が赤字、"かきく"が青字)と言う文字が表示されている状態で
    「う」と「か」の間にカーソルをあて、追加した文字色コンボボックスで「緑」を選択し、文字「お」を入力するとします。
    文字色コンボボックスで選択した色は、保持しておき、UpdateCompositionStyleメソッドで
    Inlineに対して設定します。
    デバッグしてみるとUpdateCompositionStyleメソッドの「compositionInline」は"おかきく"が取得され、
    上記のとおり、compositionInline.Foregroundに緑を設定すると
    結果、"あいう"が赤字、"おかきく"が緑字になります。
    本当は、"あいう"が赤字、"お"が緑字、"かきく"が青字にしたいのです。

    「compositionInline」で"お"だけを取得したいので
    まず、"お"に対してApplyPropertyValuedeで緑字を設定し、その後「rich.Selection.Start.Parent as Inline」をすると
    "お"だけを含むRunが取得できました。
    これでcompositionInline.Foregroundに緑を設定すると"お"だけが緑字になりました。

    喜びもつかの間、ブレークしたりしてるとうまいこと動くのですが、普通に動かすと高確率で
    ExecutionEngineExceptionが発生します。
    2012年11月11日 10:49