UWPのRichTextBlockとInlineUIContainer周りでなんだか微妙な気持ちになりました。せっかくなのでエントリしておきます。
TL;DR
- RichTextBlockに追加したUIElementはOverflowしても非表示にならない
- InlineUIContainerから
GetCharacterRect
で矩形を取得して表示判定をする - SizeChangedとかで表示非表示コードをいい感じに実行する
いい感じに動いてほしいコード
UWPのRichTextBlockはInlineUIContainerクラスを経由すると任意のUIElementを子要素として持つことができます。 たとえば、RichTextBlockの中に画像をインライン表示したい。とかがよくある要件かと思います。
これを簡単に実現するとこんな風なXAMLになります。
<RichTextBlock x:Name="Text" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap" IsTextSelectionEnabled="False"> <Paragraph> <Run Text="aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa"></Run> <Run Text="aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa aaaa"></Run> <InlineUIContainer> <Border Background="Blue"> <Image Source="Assets/Square150x150Logo.png" Height="{Binding ElementName=Text, Path=FontSize}" Stretch="Uniform" MinWidth="5"/> </Border> </InlineUIContainer> </Paragraph> </RichTextBlock>
なんかうじゃうじゃ書いてますが、実行するとこういう画面が表示されます。
期待する結果は表示しきれなくなって文字列が"..."で省略された時にこの画像部分が非表示になってほしいのですが、 何もしないとこうなります。
釈然としませんが、SizeChangedあたりであふれたかどうか判定をしたうえで、Opacity = 0にするようなコードを書くと期待した結果になります。
うまく動かすコード
このコードをSizeChangedあたりで実行すると、なんかいい感じになります。
foreach (var inline in ((Paragraph)Text.Blocks[0]).Inlines) { if(!(inline is InlineUIContainer container)) continue; var uiElement = (FrameworkElement)container.Child; var elementStart = container.ElementStart; var elementEnd = container.ElementEnd; var rect1 = elementStart.GetCharacterRect(elementStart.LogicalDirection); var rect2 = elementEnd.GetCharacterRect(elementEnd.LogicalDirection); uiElement.Opacity = rect1.Left == rect2.Left ? 0 : 1; }
ただし、このコードはいくつかの決め打ち要素が含まれています。
- TextというRichTextBlockがある
- RichTextBlockのBlocksは1個だけ、しかもそれはParagraph
- Paragraphの中にネストしたParagraphは存在しない
RichTextBlockが決め打ちなのは各自使いやすくしていただくとして、
Blocksの中身が2個以上だったり、Paragraphじゃない場合があったり、 Windows Runtime環境下では、Blockの派生クラスとして実装されているのはParagraph
のみでした。
Paragraph直下以外でInlineUIContainerを含む場合の対応が必要な場合は
各自カスタムして使ってください。
これを実装して、実行するとこんな結果になります。
UIElementが非表示になっていい感じの結果です。見えないだけで水色っぽいところに配置されています。
ちょっとだけ解説
TextPointer.GetCharacterRect
は隣接するテキスト境界のバウンディングボックスを返すような関数らしいです*1。
これを呼び出したとき、コード中のrect1.Left
とrect2.Left
は内包するUIElementがあふれていない場合異なる値を返します*2。
しかし、あふれている場合に呼び出すとrect1.Left
とrect2.Left
は同じ値になります。以降どの文字を調べても同じ値が出てきます。
どうやらこの値は、RichTextBlock.ActualWidth - "...の幅"
くらいの値になってるようです*3。
という挙動から、2つのRectのLeftプロパティが同値の場合はOpacity = 0とすることで、UIElementを非表示にしています。 ここで、Visibility = Hiddenにすると、2つのLeftプロパティが同値になってしまい再度表示する場合の判定ができなくなります。 また、UIElementのActualWidthが0になると同様に判定に失敗します。今回はMinWidth = 5とし、最低5px確保することでActualWidthが0になることを回避しました。
気が向いたらBehaviorにするかもしれません。
*1:https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.documents.textpointer.getcharacterrect#Windows_UI_Xaml_Documents_TextPointer_GetCharacterRect_Windows_UI_Xaml_Documents_LogicalDirection_
*2:だいたい rect1.Left + UIElement.ActualWidth == rect2.Left になります。厳密にはちょっと違う
*3:内部的には"..."の手前に全部幅0で表示してますよ。という感じなのかもしれない