tmytのらくがき

個人の日記レベルです

InlineUIContainerで追加したImageがOverflowしたときに非表示にする

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>

なんかうじゃうじゃ書いてますが、実行するとこういう画面が表示されます。

f:id:tmyt:20180702013821p:plain

期待する結果は表示しきれなくなって文字列が"..."で省略された時にこの画像部分が非表示になってほしいのですが、 何もしないとこうなります。

f:id:tmyt:20180702013850p:plain

釈然としませんが、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を含む場合の対応が必要な場合は 各自カスタムして使ってください。

これを実装して、実行するとこんな結果になります。

f:id:tmyt:20180702013934p:plain

UIElementが非表示になっていい感じの結果です。見えないだけで水色っぽいところに配置されています。

ちょっとだけ解説

TextPointer.GetCharacterRectは隣接するテキスト境界のバウンディングボックスを返すような関数らしいです*1。 これを呼び出したとき、コード中のrect1.Leftrect2.Leftは内包するUIElementがあふれていない場合異なる値を返します*2

しかし、あふれている場合に呼び出すとrect1.Leftrect2.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で表示してますよ。という感じなのかもしれない

TaskCompletionSource<T>が覚えられない

.NETのSystem.Threading.Tasks空間に、TaskCompletionSource<T>っていうのがあります。

TaskCompletionSource(TResult) クラス (System.Threading.Tasks)

これを使うと、こういうコードが書けます。

public Task Sleep(int milliseconds)
{
    var source = new TaskCompletionSource<bool>();
    var timer = new Timer(_ =>
    {
        source.SetResult(true);
    }, null, milliseconds, Timeout.Infinite);
    return source.Task;
}

このコードは、実際はTask.Delayと書けばいいので特に意味はないですが、外からTaskをCompleteできる便利なやつです。

最近のECMAScriptだとこう書けるあれです。

function sleep(millis)
{
    return new Promise(done => {
      setTimeout(done, millis);
    });
}

普段使わないけど、1年に1回ぐらい使うことがあって思い出せないのでメモです。

Chrome TimelineのFirefox版を作っておきました

Chrome Timelineを見たひとが、Firefox版ほしいって言ってるを見かけたのでとりあえず作ってみました。

本当は、名前も”Firefox Timeline”にしようと思ったんです。そしたらデベロッパーポリシーに"Firefoxという名前をアドオンに含める場合は「** for Firefox」という表現を使用すること”とあったので、”Timeline for Firefox”になっていますが、中身は一緒です。

最近はChrome Extensionが標準化されてWeb Extensionとして動作するのでほとんどコード修正せずに動くのは楽ちんですね。

Timeline for Firefox – Firefox 向けアドオン

WSLで32bitなELFが動いた(ただしQEMU)

TL;DR

  • binfmtとQEMUで32bitなELFを実行する
  • WSLのbinfmtはOCフラグがサポートされてない
  • sudo update-binfmts --install qemu-i386 /usr/bin/qemu-i386-static --magic '\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x03\x00' --mask '\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'

背景

はぁ…WSLにbinfmt_miscなんで実装されないんだろ…と思いながらUserVoiceを眺めていたところ、よくよく調べたらWSL空間からWin32のバイナリを呼び出す仕組みを実現するためにRS2の時点で実装されていたのでした。なんと1年以上前…当時Win32 PEを実行するのにMZヘッダが見つかったら/initに実行を委譲するよ。みたいな記事を読んだはずなのにすっかり忘れていた。

binfmtが使えるならi386 ELFのmaskを設定してやれば動くんじゃないの…と思ったのが6時前。思い立ってしまったら気になって眠れなくなったのでやってみました。

手順

  1. qemu-user-staticをインストール
  2. update-binfmtsでハンドラを登録

まずqemu-user-staticをインストール。

$ sudo apt-get install qemu-user-static

qemu-user-static用のbinfmtが自動登録されるけど、これにi386はない*1のでハンドラ手動登録。

$ sudo update-binfmts --install qemu-i386 /usr/bin/qemu-i386-static --magic '\x7f\x45\x4c\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x03\x00' --mask '\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'

うじゃうじゃたくさん書いてあるのはELFのヘッダを検出するマジックとマスク。ELFの仕様ともともとあったqemu-user-staticのマジックを眺めながらいい感じにしたらこうなりました。

なぜかこうするとうまく動くようになるんですが、どうやらWSLのbinfmt_miscはOCフラグというのに対応していないらしい。こうやってやるといい感じに動くらしい*2

動作確認

せっかくなので32bit ELFなバイナリを作って実行してみる。

$ sudo apt-get install build-essential libc6-dev-i386
$ vi test.c
#include<stdio.h>

int main(){
  puts("Hello World");
  return 0;
}
$ gcc -m32 test.c
$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1e13e8c9bba4368d713095984853243c85b65768, not stripped
$ ./a.out
Hello World

ちなみにupdate-binfmtsしない場合はこうなる。

$ ./a.out
-bash: ./a.out: cannot execute binary file: Exec format error

おまけ

WSLのインスタンスが全部終了するとbinfmtのエントリが消し飛ぶので何らかの方法で復元してください

*1:普通のx86_64ならi386バイナリが直接実行できるので登録する意味がない

*2:https://github.com/Microsoft/WSL/issues/2620#issuecomment-374490404

ChromeからWindows Timelineにアクティビティを登録するアドオンを公開しました

とくになんの下調べもせず、作ったら面白そうなのでつくっただけです。 もしかしたらすでにもっと出来がいいやつがあるかも。

Windows TimelineってのはApril 2018 Updateで追加された新機能で赤丸のところをクリックすると下のほうに出てくるこれです。

f:id:tmyt:20180524234316p:plain

Edge使ってるとここに表示したWebページが追加されていくんですけど、Chromeとかだと当然そういう機能はないので試しに作ってみました。

検索ヒットしないのでURLからどうぞ。