none
リッチテキストボックス内の要素をグループとしてまとめる RRS feed

  • 質問

  • お世話になります。

    リッチテキストボックス(FlowDocument)内に「内部にRun要素とTextBlock要素を複数個持つ要素」を複数個作成しようと思っています。
    (いずれ動的にRunやTextBlockの追加を行う予定です。)
    そこでSpanという「Inline要素を複数持つことができる要素」をParagraph内に配置し、そのSpanを複数配置してみようと考えました。
    (※Spanの代わりにParagraphを使ってみようかなとも考えたのですがParagraphは自動で改行してしまうのでやめました。)

    ↓Spanの内部にRunとTextBlockを配置

            <RichTextBox x:Name="rtb">
                <FlowDocument>
                    <Paragraph>
                        <Span>
                            <Run>
                                hoge
                            </Run>
                            <InlineUIContainer>
                                <TextBlock Text="piyo"/>
                            </InlineUIContainer>
                        </Span>
                    </Paragraph>
                </FlowDocument>
            </RichTextBox>

    このxamlを実行し、Run「hoge」の'o'と'g'の間にカレットを入れた状態で"geho"と入力しました。その結果

            <RichTextBox x:Name="rtb">
                <FlowDocument>
                    <Paragraph>
                        <Span>
                            <Run>
                                ho
                            </Run>
                        </Span>
                        <Run>
                            geho
                        </Run>
                        <Span>
                            <Run>
                                ge
                            </Run>
                            <InlineUIContainer>
                                <TextBlock Text="piyo"/>
                            </InlineUIContainer>
                        </Span>
                    </Paragraph>
                </FlowDocument>
            </RichTextBox>

    というようにSpanが分解された構造になってしまいました。
    しばらくググったのですが何が起こっているのかさっぱりわかりません。

    お伺いしたいことは
    ①複数個のRunとTextBlockをまとめるのにSpanを使うという方向性は正しいのか
    ②上記の例でなぜSpanは分解されたのか
    の二つです。

    どうかよろしくお願いします。



    • 編集済み 指計算機 2016年3月28日 14:49 誤字修正により
    2016年3月28日 14:45

回答

  • 以下を実行してやれば何が起きているか理解できると思います。
    RichTextBoxは各文字など色やサイズを変更できますが、挿入するときに同じプロパティが連続していれば連結されて、異なるプロパティの要素が挿入されるときは元の文字に適用されているプロパティを維持するため分割したうえで挿入します。。
    そのため、Runの途中に異なるプロパティとなる文字を挿入すると前後に分割されます。
    この挙動により二つのSpanに分割されたのち、Runがその間に挿入されることになります。

    で、なんで同じプロパティではなくなっているかというと、わかりにくいのですがLanguageが未定義の状態になっている要素にたいして、Languageが適用された文字を挿入しようとすると、異なるプロパティを持つために分割される挙動をすることになります。
    たまたまLanguageが違っていたからですが、他のプロパティが違う文字を挿入しようとすれば同様に分割されることになるでしょう。

    分割されないようにするには挿入する要素のプロパティが異ならないようにしてやればいいことになります。

    <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="800" Width="600">
        <Grid>
            <DockPanel>
                <StackPanel DockPanel.Dock="Top">
                    <StackPanel Orientation="Horizontal" >
                        <TextBlock Text= "RichTextBoxLanguage="/>
                        <ComboBox SelectedItem="{Binding Path=Language,ElementName=rich,Mode=TwoWay}" x:Name="comboLanguages" />
                    </StackPanel>
                    <TextBlock>
                        <Run Text="SelectionLanguage=" />
                        <Run x:Name="selLanguage" />
                    </TextBlock>
                </StackPanel>
                <RichTextBox
                         Width="200" x:Name="rich" 
                         SelectionChanged="rich_SelectionChanged">
                    <FlowDocument>
                        <Paragraph>
                            <Span><!-- Languageは親から継承 -->
                                <Span >
                                    <Run >hoge</Run>
                                </Span>
                                <InlineUIContainer > <TextBlock Text="piyo" /> </InlineUIContainer>
                            </Span>
                            <LineBreak />
                            <Span Language="en-us">
                                <Span Language="en-us"><Run Language="en-us" Text="AAA"/></Span>
                                <Span Language="en-us"><Run Language="ja-jp" Text="BBB"/></Span>
                                <Span Language="ja-jp"><Run Language="en-us" Text="CCC"/></Span>
                                <Span Language="ja-jp"><Run Language="ja-jp" Text="DDD"/></Span>
                            </Span>
                            <LineBreak />
                            <Span Language="ja-jp">
                                <Span Language="en-us"><Run Language="en-us" Text="EEE"/></Span>
                                <Span Language="en-us"><Run Language="ja-jp" Text="FFF"/></Span>
                                <Span Language="ja-jp"><Run Language="en-us" Text="GGG"/></Span>
                                <Span Language="ja-jp"><Run Language="ja-jp" Text="HHH"/></Span>
                            </Span>   
                        </Paragraph>
                    </FlowDocument>
                </RichTextBox>
    
                <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>
        </Grid>
    </Window>
    namespace WpfApplication1
    {
    	using System;
    	using System.Collections.Generic;
    	using System.Windows;
    	using System.Windows.Documents;
    	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, 1);
    			timer.Tick += new EventHandler(timer_Tick);
    			timer.Start();
    
    			System.Windows.Markup.XmlLanguage[] languages
    				={
    					 rich.Language,
    					 System.Windows.Markup.XmlLanguage.GetLanguage("ja-jp"),
    					 System.Windows.Markup.XmlLanguage.GetLanguage("en-us"),
    					 System.Windows.Markup.XmlLanguage.GetLanguage("fr-fr"),
    				 };
    			comboLanguages.ItemsSource = languages;
    
    		}
    		private System.Windows.Threading.DispatcherTimer timer;
    
    		void timer_Tick(object sender, EventArgs e)
    		{
    			this.treeView1.DataContext = new Item[] { new Item(rich.Document) };
    		}
    
    		private void rich_SelectionChanged(object sender, RoutedEventArgs e)
    		{
    
    			this.selLanguage.Text = string.Empty;
    			var sel = rich.Selection as TextSelection;
    			if (sel != null)
    			{
    				var pos = sel.Start.GetInsertionPosition(LogicalDirection.Backward);
    				var element = pos.Parent as TextElement;
    				this.selLanguage.Text = (element != null) ? element.Language.ToString() : string.Empty;
    			}
    		}
    	}
    
    	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 + " : " + run.Language.ToString(); 
    			}
    			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 + " : " + element.Language.ToString();
    			}
    		}
    
    		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; }
    	}
    }


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

    • 回答としてマーク 指計算機 2016年3月30日 0:21
    • 回答としてマークされていない 指計算機 2016年3月30日 0:21
    • 回答としてマーク 指計算機 2016年3月30日 0:23
    2016年3月29日 3:21

すべての返信

  • >このxamlを実行し、Run「hoge」の'o'と'g'の間にカレットを入れた状態で"geho"と入力しました。

    確認です。「実行し」とありますが、Xamlデザイナーでデザイン時に変更しているのでしょうか?
    もしそうであれば、Xamlデザイナーは「geho」が入力された際に、新たなinline要素が加わったと判断し、それをRunで囲ったものを単に追加した動作をしたただけだと思われます。Spanが分割されたのは、Runで囲った「geho」を純粋に追加する際にSpanが邪魔になったので、「geho」の前後でSpanを分割し、何もない状態にしておいてから追加したのだと思います。
    普通に考えればXamlデザイナーのこの動作は妥当だと思います。「geho」と打っただけでは、タグなどどのような形でXamlを再構成して良いのか、Xamlデザイナーでは判断が付かないからです。判断が付かない以上、Runで囲って単に追加する動作は妥当だと思います。
    ちなみに試してみましたが、文字を削除する際には、削除した結果、Runで囲まれた同士が隣接するような場合は、1つのRunにまとめられるという気の利いた動作をしました。これは、削除する際にはXamlデザイナーで何のタグにしようかと迷うことなく、Runというタグを使うという判断ができているからだと思われます。

    また、最初のご質問のSpanですが、これはinline要素をグループ化するものですので、目的として合っているように思います。


    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/

    2016年3月29日 1:43
    モデレータ
  • 返信ありがとうございます。

    言葉足らずな部分があり申し訳ありません。「実行」とはいわゆる「デバッグ」を指しています。
    Visual Studio上でF5を押下した際の動作です。

    ※実行中の(xamlの)構造取得は以下のサイトのクラス「PrintLogicalTree」を参考にさせていただきました。http://blogs.wankuma.com/kazuki/archive/2008/02/19/123929.aspx


    >新たなinline要素が加わったと判断し、それをRunで囲ったものを単に追加した動作をしたただけだと思われます。

    そうですね・・・ただSpanが割れるということに驚きました。お前(カレット)はSpanの中のRunの中の文字の間にいるんじゃないかと。

    今コードを打てる状態じゃないので確認はできないのですが & 直接私のケースとは関係しませんが、
    Spanでフォントを設定した文字の間に文字を入力した場合、フォントは設定されないってことになるのではないかと思います。
    Officeソフトの動作を考えると驚き最大です・・・

    Span内のRun内に明示的にカレットを置く方法はあるのでしょうか?

    2016年3月29日 2:18
  • 以下を実行してやれば何が起きているか理解できると思います。
    RichTextBoxは各文字など色やサイズを変更できますが、挿入するときに同じプロパティが連続していれば連結されて、異なるプロパティの要素が挿入されるときは元の文字に適用されているプロパティを維持するため分割したうえで挿入します。。
    そのため、Runの途中に異なるプロパティとなる文字を挿入すると前後に分割されます。
    この挙動により二つのSpanに分割されたのち、Runがその間に挿入されることになります。

    で、なんで同じプロパティではなくなっているかというと、わかりにくいのですがLanguageが未定義の状態になっている要素にたいして、Languageが適用された文字を挿入しようとすると、異なるプロパティを持つために分割される挙動をすることになります。
    たまたまLanguageが違っていたからですが、他のプロパティが違う文字を挿入しようとすれば同様に分割されることになるでしょう。

    分割されないようにするには挿入する要素のプロパティが異ならないようにしてやればいいことになります。

    <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="800" Width="600">
        <Grid>
            <DockPanel>
                <StackPanel DockPanel.Dock="Top">
                    <StackPanel Orientation="Horizontal" >
                        <TextBlock Text= "RichTextBoxLanguage="/>
                        <ComboBox SelectedItem="{Binding Path=Language,ElementName=rich,Mode=TwoWay}" x:Name="comboLanguages" />
                    </StackPanel>
                    <TextBlock>
                        <Run Text="SelectionLanguage=" />
                        <Run x:Name="selLanguage" />
                    </TextBlock>
                </StackPanel>
                <RichTextBox
                         Width="200" x:Name="rich" 
                         SelectionChanged="rich_SelectionChanged">
                    <FlowDocument>
                        <Paragraph>
                            <Span><!-- Languageは親から継承 -->
                                <Span >
                                    <Run >hoge</Run>
                                </Span>
                                <InlineUIContainer > <TextBlock Text="piyo" /> </InlineUIContainer>
                            </Span>
                            <LineBreak />
                            <Span Language="en-us">
                                <Span Language="en-us"><Run Language="en-us" Text="AAA"/></Span>
                                <Span Language="en-us"><Run Language="ja-jp" Text="BBB"/></Span>
                                <Span Language="ja-jp"><Run Language="en-us" Text="CCC"/></Span>
                                <Span Language="ja-jp"><Run Language="ja-jp" Text="DDD"/></Span>
                            </Span>
                            <LineBreak />
                            <Span Language="ja-jp">
                                <Span Language="en-us"><Run Language="en-us" Text="EEE"/></Span>
                                <Span Language="en-us"><Run Language="ja-jp" Text="FFF"/></Span>
                                <Span Language="ja-jp"><Run Language="en-us" Text="GGG"/></Span>
                                <Span Language="ja-jp"><Run Language="ja-jp" Text="HHH"/></Span>
                            </Span>   
                        </Paragraph>
                    </FlowDocument>
                </RichTextBox>
    
                <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>
        </Grid>
    </Window>
    namespace WpfApplication1
    {
    	using System;
    	using System.Collections.Generic;
    	using System.Windows;
    	using System.Windows.Documents;
    	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, 1);
    			timer.Tick += new EventHandler(timer_Tick);
    			timer.Start();
    
    			System.Windows.Markup.XmlLanguage[] languages
    				={
    					 rich.Language,
    					 System.Windows.Markup.XmlLanguage.GetLanguage("ja-jp"),
    					 System.Windows.Markup.XmlLanguage.GetLanguage("en-us"),
    					 System.Windows.Markup.XmlLanguage.GetLanguage("fr-fr"),
    				 };
    			comboLanguages.ItemsSource = languages;
    
    		}
    		private System.Windows.Threading.DispatcherTimer timer;
    
    		void timer_Tick(object sender, EventArgs e)
    		{
    			this.treeView1.DataContext = new Item[] { new Item(rich.Document) };
    		}
    
    		private void rich_SelectionChanged(object sender, RoutedEventArgs e)
    		{
    
    			this.selLanguage.Text = string.Empty;
    			var sel = rich.Selection as TextSelection;
    			if (sel != null)
    			{
    				var pos = sel.Start.GetInsertionPosition(LogicalDirection.Backward);
    				var element = pos.Parent as TextElement;
    				this.selLanguage.Text = (element != null) ? element.Language.ToString() : string.Empty;
    			}
    		}
    	}
    
    	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 + " : " + run.Language.ToString(); 
    			}
    			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 + " : " + element.Language.ToString();
    			}
    		}
    
    		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; }
    	}
    }


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

    • 回答としてマーク 指計算機 2016年3月30日 0:21
    • 回答としてマークされていない 指計算機 2016年3月30日 0:21
    • 回答としてマーク 指計算機 2016年3月30日 0:23
    2016年3月29日 3:21
  • 返信ありがとうございます。

    ご提示していただいたコードの、実施による確認は明日以降になってしまうのですが回答内容をだいたい把握いたしました。
    参考コードまで用意していただきありがとうございます。

    確認次第再度返信いたします。

    2016年3月30日 1:08
  • コードを実行してみました。
    なにやらもう教材レベルのわかりやすさで感動しました。

    おかげさまで何が問題だったのかは完全に理解できたのですがコードはまだ理解しきれていないので
    ひきつづき確認したいと思います。

    本当にありがとうございました。

    2016年3月31日 15:16