yotiky Tech Blog

とあるエンジニアの備忘録

HoloLens2 で入力方法として QRコード を使う

前回「HoloLens2 でQRコードのスキャンを実装する」では、QRコードスキャンの実装方法を紹介しました。

QRコードのスキャンが使えると、QRコードを介してアプリに文字列を流し込めるようになります。

試しに、入力フィールドの横にQRコードを読み込むボタンを設置し、QRコードをスキャンするとその内容を入力フィールドに反映するデモを作成しました。

目次

テキスト入力

Name、ID、Passwordを順番に入力するだけのデモアプリです。HoloLensとPCを順に操作するので素早くやろうとすると少し混乱してますが、実際の場面で素早さを求められることは少ないので問題ないでしょう。

ここで使用しているのは、自作のQR生成アプリです。 文字列を入力し、QRボタンを押すと即座にQRコードを生成してくれます。

f:id:yotiky:20201024015416p:plain:w400

Json 入力

もちろん1回1回文字列を入力する場合にも使えますが、中身が文字列なので Json がそのまま使えるため汎用性がぐっと高まります。

HoloLens を実行環境に合わせるため現地で初期設定などをすることがありますが、複雑で大量な入力データでもノートPCなどを使えるのでスムーズに設定できそうです。 アプリには簡易的にテンプレートなどを保存する機能も付いています。

注意点

ディスプレイに対してQRコードをスキャンする場合、反射の強いディスプレイや明るさが強い設定だとQRコードを全く認識しません。 反射しないディスプレイなのにQRコードを認識しない場合は、暗くすると認識することがあります。 ディスプレイによっては15~20cmくらいまで顔を近づける必要があります。

QRSetter

自作のQR生成アプリのリポジトリです。

github.com

関連記事

yotiky.hatenablog.com

yotiky.hatenablog.com

HoloLens2 でQRコードのスキャンを実装する

目次

検証環境

  • Unity:2019.4.3f1
  • VisualStudio:2019
  • MRTK 2.5 (設定、UI、操作などに)
  • UniRx

手順

導入

  • NuGetForUnity のパッケージをDLして Import する
  • NuGet > Manage NuGet Packages を開いて、「Microsoft.MixedReality.QR」を検索して Install する

f:id:yotiky:20201022211436p:plain

実装

最低限の実装であれば、 QRCodeWatcher クラスだけで実装できます。 QRCodeWatcher に、Added、Updated、Removed のイベントがあるので、イベントハンドラを登録します。Start メソッドを呼び出すと、スキャンが開始されイベントが発行されるようになります。停止したい時は Stop メソッドで停止します。

一部抜粋した実装例を以下に示します。

    private QRCodeWatcher qRCodeWatcher;
    private readonly ConcurrentQueue<ActionData> pendingActions = new ConcurrentQueue<ActionData>();

    async void Start()
    {
        IsSupported = QRCodeWatcher.IsSupported();
        await QRCodeWatcher.RequestAccessAsync();

        qRCodeWatcher = new QRCodeWatcher();
        qRCodeWatcher.Added += QRCodeWatcher_Added;
        qRCodeWatcher.Updated += QRCodeWatcher_Updated;
        qRCodeWatcher.Removed += QRCodeWatcher_Removed;

        IsReady.Value = true;

        this.UpdateAsObservable()
            .Subscribe(_ =>
            {
                if(pendingActions.TryDequeue(out var action))
                {
                    if (action.Type == ActionData.EventType.Added 
                        || action.Type == ActionData.EventType.Updated)
                    {
                        if (DateTimeOffset.Compare(StartTime, action.QRCode.LastDetectedTime) < 0)
                        {
                            onScanned.OnNext(action.QRCode);
                        }
                    }
                }

            })
            .AddTo(this);
    }

    public void StartScan()
    {
        if (!IsReady.Value) { return; }

        StartTime = DateTimeOffset.Now;
        qRCodeWatcher.Start();
    }

    public void StopScan()
    {
        if (!IsReady.Value) { return; }

        qRCodeWatcher.Stop();
    }
}

イベントハンドラはキューに ActionData を追加するだけなので割愛してます。

StartScan メソッドでは、StartTime を保存していますが、HoloLens は過去に認識したQRのデータを保持しています。スキャンを開始した日時よりも古いデータは無視するような処理が必要になってきます。

ビルドする前に、Capabilities で WebCam を有効にしてください。

f:id:yotiky:20201022231223g:plain

上の録画では後述する自作のQRアプリを使っています。このアプリでは ZXing を使用しており、CharacterSet を設定できますが、デフォルトと言われているISO-8859-1だとHoloLensが認識しません。そのためUTF-8を使用しています。

サンプルプロジェクト

ソースコード一式は以下においてあります。

github.com

参考

公式ページは英語のみ、C++が9割と参考なるのかならないのか。 ただ、QRスキャンに関するベストプラクティスが載っていますのでそこは必読です。(日本語に抄訳されている記事も)

docs.microsoft.com

また、以下のQRラッキングのサンプルを参考にさせて頂きました。 github.com

関連記事

yotiky.hatenablog.com

yotiky.hatenablog.com

WPF - MahApps の覚書

目次

前提

  • MahApps.Metro
  • MahApps.Metro.IconPacks

App.xaml でリソースを読み込む。

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
            <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Cyan.xaml" />
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

各コントロール名前空間を参照。

xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks"

WindowCommands

<metro:MetroWindow.LeftWindowCommands>
    <metro:WindowCommands>
        <Button ToolTip="GitHub" Command="{Binding GitHubCommand}">
            <icon:PackIconEvaIcons Kind="Github"/>
        </Button>
    </metro:WindowCommands>
</metro:MetroWindow.LeftWindowCommands>
<metro:MetroWindow.RightWindowCommands>
    <metro:WindowCommands>
        <Button ToolTip="設定" Command="{Binding SettingsCommand}">
            <StackPanel Orientation="Horizontal">
                <icon:PackIconModern Width="22" Height="22" Kind="Settings" />
                <TextBlock Text="設定" VerticalAlignment="Center" Margin="5,0"/>
            </StackPanel>
        </Button>
    </metro:WindowCommands>
</metro:MetroWindow.RightWindowCommands>

f:id:yotiky:20201109214912p:plain

ListBox

<ListBox Height="180" Width="120"
    metro:MultiSelectorHelper.SelectedItems="{Binding SelectedItems}"
    metro:ScrollViewerHelper.EndOfVerticalScrollReachedCommand="{Binding EndOfScrollReachedCmdWithParameter, Mode=OneWay}"
    BorderThickness="1"
    DisplayMemberPath="Name"
    ItemsSource="{Binding Items}"
    SelectionMode="Multiple"
    Style="{StaticResource MahApps.Styles.ListBox.Virtualized}" />
public class SampleViewModel 
{
    public ObservableCollection<SampleItemViewModel> Items { get; } = new ObservableCollection<SampleItemViewModel>();
    public ObservableCollection<SampleItemViewModel> SelectedItems { get; } = new ObservableCollection<SampleItemViewModel>();
}

public class SampleItemViewModel 
{
    public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}

HamburgerMenu

Item のテンプレート。(公式サイトより)

HamburgerMenuGlyphItem は画像、HamburgerMenuIconItem はアイコンを表示する。画像はうまく表示できなかったのでスルー。

<Window.Resources>
    <!--  This is the template for all menu items. In this sample we use the glyph items.  -->
    <DataTemplate x:Key="HamburgerMenuGlyphItem" DataType="{x:Type metro:HamburgerMenuGlyphItem}">
        <DockPanel Height="48" LastChildFill="True">
            <Grid x:Name="IconPart"
                      Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type metro:HamburgerMenu}}, Path=CompactPaneLength}"
                      DockPanel.Dock="Left">
                <Image Margin="12"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Source="{Binding Glyph}" />
            </Grid>
            <TextBlock x:Name="TextPart"
                           VerticalAlignment="Center"
                           FontSize="16"
                           Text="{Binding Label}" />
        </DockPanel>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type metro:HamburgerMenu}}, Path=PanePlacement}" Value="Right">
                <Setter TargetName="IconPart" Property="DockPanel.Dock" Value="Right" />
                <Setter TargetName="TextPart" Property="Margin" Value="8 0 0 0" />
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>
    
    <!--  This is the template for the option menu item  -->
    <DataTemplate x:Key="HamburgerMenuIconItem" DataType="{x:Type metro:HamburgerMenuIconItem}">
        <DockPanel Height="48" LastChildFill="True">
            <ContentControl x:Name="IconPart"
                                Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type metro:HamburgerMenu}}, Path=CompactPaneLength}"
                                Content="{Binding Icon}"
                                DockPanel.Dock="Left"
                                Focusable="False"
                                IsTabStop="False" />
            <TextBlock x:Name="TextPart"
                           VerticalAlignment="Center"
                           FontSize="16"
                           Text="{Binding Label}" />
        </DockPanel>
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type metro:HamburgerMenu}}, Path=PanePlacement}" Value="Right">
                <Setter TargetName="IconPart" Property="DockPanel.Dock" Value="Right" />
                <Setter TargetName="TextPart" Property="Margin" Value="8 0 0 0" />
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>
</Window.Resources>
<metro:HamburgerMenu x:Name="HamburgerMenuControl"
                    DisplayMode="CompactOverlay"
                    HamburgerWidth="48"
                    IsPaneOpen="{Binding IsHamburgerMenuPaneOpen}"
                    ItemTemplate="{StaticResource HamburgerMenuIconItem}"
                    OptionsItemTemplate="{StaticResource HamburgerMenuIconItem}"
                    SelectedIndex="0"
                    OpenPaneLength="180"
                    VerticalScrollBarOnLeftSide="False">

    <!--  Items  -->
    <metro:HamburgerMenu.ItemsSource>
        <metro:HamburgerMenuItemCollection>
            <metro:HamburgerMenuIconItem Label="最新情報">
                <metro:HamburgerMenuIconItem.Icon>
                    <icon:PackIconJamIcons Width="22" Height="22" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="BellF" />
                </metro:HamburgerMenuIconItem.Icon>
            </metro:HamburgerMenuIconItem>
            <metro:HamburgerMenuIconItem Label="チャット">
                <metro:HamburgerMenuIconItem.Icon>
                    <icon:PackIconMaterialDesign Width="22" Height="22" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="Chat" />
                </metro:HamburgerMenuIconItem.Icon>
            </metro:HamburgerMenuIconItem>
            <metro:HamburgerMenuIconItem Label="チーム">
                <metro:HamburgerMenuIconItem.Icon>
                    <icon:PackIconPicolIcons Width="22" Height="22" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="GroupHalf" />
                </metro:HamburgerMenuIconItem.Icon>
            </metro:HamburgerMenuIconItem>
            <metro:HamburgerMenuIconItem Label="予定表">
                <metro:HamburgerMenuIconItem.Icon>
                    <icon:PackIconBoxIcons Width="22" Height="22" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="SolidCalendar" />
                </metro:HamburgerMenuIconItem.Icon>
            </metro:HamburgerMenuIconItem>
        </metro:HamburgerMenuItemCollection>
    </metro:HamburgerMenu.ItemsSource>

    <!--  Options  -->
    <metro:HamburgerMenu.OptionsItemsSource>
        <metro:HamburgerMenuItemCollection>
            <metro:HamburgerMenuIconItem Label="ヘルプ">
                <metro:HamburgerMenuIconItem.Icon>
                    <icon:PackIconEntypo Width="22" Height="22" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="Help" />
                </metro:HamburgerMenuIconItem.Icon>
            </metro:HamburgerMenuIconItem>
        </metro:HamburgerMenuItemCollection>
    </metro:HamburgerMenu.OptionsItemsSource>

    <!--  Content  -->
    <metro:HamburgerMenu.Content>
        <StackPanel>
            <TextBlock Text="Hamburger Menu Sample" FontSize="24" Margin="10,0,0,100"/>
            <Button Content="Button" Width="120" Height="40"/>
            <Button Content="Button" Width="120" Height="40"/>
        </StackPanel>
    </metro:HamburgerMenu.Content>
</metro:HamburgerMenu>

Items は上部、Options は下部に配置される。 タブのようにクリックすると選択状態になる。

f:id:yotiky:20201110113417g:plain

参考

elf-mission.net

ボタンとして使用する

タブではなくボタンとして使用するには、選択状態にならないようする。

<metro:HamburgerMenu x:Name="HamburgerMenuControl"
                    DisplayMode="CompactOverlay"
                    HamburgerWidth="48"
                    IsPaneOpen="{Binding IsHamburgerMenuPaneOpen}"
                    ItemTemplate="{StaticResource HamburgerMenuIconItem}"
                    OptionsItemTemplate="{StaticResource HamburgerMenuIconItem}"
                    SelectedIndex="-1"
                    OpenPaneLength="180"
                    VerticalScrollBarOnLeftSide="False"
                    ItemClick="HamburgerMenuControl_ItemClick"
                    OptionsItemClick="HamburgerMenuControl_ItemClick">
private void HamburgerMenuControl_ItemClick(object sender, MahApps.Metro.Controls.ItemClickEventArgs args)
{
    HamburgerMenuControl.SelectedIndex = -1;
    HamburgerMenuControl.SelectedOptionsIndex = -1;
}

f:id:yotiky:20201110120314g:plain

ToolTipを表示する

閉じている時だけ表示する場合。

<DataTemplate x:Key="HamburgerMenuIconItem" DataType="{x:Type metro:HamburgerMenuIconItem}">
    <DockPanel x:Name="RootDock" Height="48" LastChildFill="True">
        <!-- 省略 -->
    </DockPanel>
    <DataTemplate.Triggers>
        <!-- 省略 -->
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type metro:HamburgerMenu}}, Path=IsPaneOpen}" Value="False">
            <Setter TargetName="RootDock" Property="ToolTip" Value="{Binding ToolTip, Mode=OneWay}" />
        </DataTrigger>
    </DataTemplate.Triggers>
</DataTemplate>
<metro:HamburgerMenuIconItem Label="最新情報" ToolTip="最新情報をお届けします">

f:id:yotiky:20201110122958p:plain

常に表示する場合。

    <DockPanel Height="48" LastChildFill="True" ToolTip="{Binding ToolTip}">

ZXing を使ってQRコードを動的に生成して表示する

ZXing は "zebra crossing" の略らしく、ゼブラクロッシングと読むのが正解っぽい。

検証環境

導入

nuget で ZXing.NET をインストールします。 System.Drawing.Common も使うので入っていなければ一緒に入れるか、実装する際に要求されます。

github.com

実装

xaml 側に Image を追加します。

<Image x:Name="QrImage" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"/>

今回はコードビハインドに処理を追加します。

    var qrCode = new BarcodeWriter
    {
        Format = BarcodeFormat.QR_CODE,
        Options = new QrCodeEncodingOptions
        {
            ErrorCorrection = ErrorCorrectionLevel.M,
            CharacterSet = "ISO-8859-1", // default
            //CharacterSet = "UTF-8", // japanese
            Width = 300,
            Height = 300,
            Margin = 5,
        },
    };

    using (var bmp = qrCode.Write("https://yotiky.hatenablog.com/"))
    using (var ms = new MemoryStream())
    {
        bmp.Save(ms, ImageFormat.Bmp);
        var source = BitmapFrame.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
        QrImage.Source = source;
    }

QRコードの内容に日本語が含まれる場合は、"Shift_JIS" を指定する記事が多くありますが、 Not a supported と言われてしまったので、"UTF-8" を使っておきましょう。

パラメータ

BarcodeFormat

バーコードのフォーマットを取得または設定します。この値は、MultiFormatWriterを使用している場合にのみ有効です。

QR_CODE 以外に、ITF(日本でよく見る所謂バーコード)や一次元、二次元のもの10個以上定義されています。

QrCodeEncodingOptions

プロパティ
Hints すべてのオプションのデータコンテナを取得します。キーとなる EncodeHintType がすべてのプロパティと対になっている。
Height バーコード画像の高さを指定します。
Width バーコード画像の幅を指定します。
PureBarcode 出力画像に内容文字列を入れないでください。
Margin バーコードを生成する際に使用するマージンをピクセル単位で指定します。意味はフォーマットによって異なります。例えば、ほとんどの1Dフォーマットではバーコードの前後のマージンを水平方向に制御します。
GS1Format データを GS1 標準にエンコードするかどうかを指定します。
ErrorCorrectionLevel QRコードなどで使用する誤り訂正の程度を指定します。タイプはエンコーダに依存します。例えば、QRコードの場合はZXing.QrCode.Internal.ErrorCorrectionLevelとなります。
CharacterSet 適用可能な場合に使用する文字エンコーディングを指定します (System.String 型)。
DisableECI QRコード生成時にECIセグメントを明示的に無効にする QRコードの仕様に反していますが、ECIセグメントが必要な状態でISO-8859-1(デフォルト)からUTF-8文字コードが切り替わってしまうと、一部の読者に問題が発生します。このプロパティをtrueに設定すると、UTF-8エンコーディングでECIセグメントが省略されます。
QrVersion エンコードするQRコードの正確なバージョンを指定します。1から40までの整数値。指定したデータが必要なバージョンに収まらない場合は、WriterExceptionがスローされます。

ErrorCorrectionLevel ですが、QRコードは汚れていたり破損していてもデータを復元できる機能を持っていて、どの程度の誤りを訂正できるか指定できるようです。

L = ~7%、M = ~15%、Q = ~25%、H = ~30%、となっており、レベルが高いほど訂正能力が上がり、コードのサイズが大きくなります。

www.qrcode.com

実行

f:id:yotiky:20201011034958p:plain

WPF - FluentWPF の使い方

Fluent Design System に関しては次のページの通り。

docs.microsoft.com

FluentWPF の基本的な使い方はREADMEにあります。

github.com

作者のブログも少し古めですが参考になります。

sourcechord.hatenablog.com

ただし、現状 MahApps.Metro との併用は MetroWindow の添付プロパティを設定すると例外が発生してうまく動作しないようです。 なお、検証時のバージョンは次の通りです。

  • .NET Core 3.1 の WPF プロジェクト
  • FluentWPF : 0.8.0
  • MahApps.Metro : 2.2.0

README の補足記事

AcrylicMenuStyle と TextBox / PassswordBox

f:id:yotiky:20200904212851p:plain:w250

f:id:yotiky:20200904213258p:plain:w250

sourcechord.hatenablog.com

sourcechord.hatenablog.com

Button の Reveal エフェクト・カスタマイズ

f:id:yotiky:20200904214222g:plain:w300

sourcechord.hatenablog.com

Unity - スクリプトで LineRenderer を使って線を引く

        var head = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        head.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
        head.transform.parent = transform;
        gazeLine = head.AddComponent<LineRenderer>();
        gazeLine.material = new Material(Shader.Find("Sprites/Default"));
        gazeLine.startWidth = 0.01f;
        gazeLine.endWidth = 0.01f;
        gazeLine.startColor = Color.blue;
        gazeLine.endColor = new Color(1, 0, 0, 0);
        gazeLine.SetPosition(0, head.transform.position);
        gazeLine.SetPosition(1, head.transform.forward * 2f);
        var point = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        point.transform.localScale = new Vector3(0.02f, 0.02f, 0.02f);
        point.transform.position = head.transform.forward * 2f;
        point.transform.parent = transform;
        point.GetComponent<Renderer>().material.color = Color.red;

f:id:yotiky:20200903203614p:plain

WPF - XAML ほかの覚書

目次

リソースを使う

外部リソースを定義する

プロジェクトに ResourceDictionary を追加する。

f:id:yotiky:20200903200632p:plain

外部リソースを読み込む。

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary Source="/Styles.xaml"/>
        </ResourceDictionary>
    </UserControl.Resources>

複数ある場合は、MergedDictionaries を使う。

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Styles1.xaml"/>
                <ResourceDictionary Source="/Styles2.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>

App.xaml で設定するとアプリケーションスコープで利用できるようになる。

方法: アプリケーション スコープのリソース ディクショナリを使用する - WPF .NET Framework | Microsoft Docs

プリミティブ型の利用

.NET Core (3.1) の宣言。

    xmlns:system="clr-namespace:System;assembly=System.Runtime"

.NET Framework は多分これ。(試してないけど古い検索結果は大体こっち)

    xmlns:system="clr-namespace:System;assembly=mscorlib"

あとはリソースを定義する。

    <system:Double x:Key="FontSize">18</system:Double>

Grid の RowDefinitions / ColumnDefinitions の長さを定義する

ピクセルの場合

    <GridLength x:Key="RowHeight">100</GridLength>

パーセントの場合

    <GridLength x:Key="RowHeight">1*</GridLength>

自動の場合

    <GridLength x:Key="RowHeight">Auto</GridLength>

利用方法

    <Grid.RowDefinitions>
        <RowDefinition Height="{StaticResource RowHeight}"/>
        <RowDefinition Height="{StaticResource RowHeight}"/>
    </Grid.RowDefinitions>

スタイル

使い方

<Window.Resources>
    <Style TargetType="RadioButton">
        <Setter Property="VerticalAlignment" Value="Center"/>
        <Setter Property="HorizontalAlignment" Value="Center"/>
        <Setter Property="Margin" Value="10"/>
        <Setter Property="LayoutTransform">
            <Setter.Value>
                <ScaleTransform ScaleX="2.0" ScaleY="2.0"/>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

継承

BasedOn を使う。

<Window.Resources>
    <Style x:Key="BaseStyle" TargetType="Button">
        <Setter Property="Margin" Value="0,5"/>
        <Setter Property="Width" Value="180"/>
        <Setter Property="Height" Value="80"/>
    </Style>
    <Style x:Key="NewStyle" TargetType="Button" BasedOn="{StaticResource BaseStyle}">
        <Setter Property="Background" Value="Azure"/>
    </Style>
</Window.Resources>

f:id:yotiky:20200904112346p:plain:w280

別の ResouceDictionary に定義してあるスタイルを継承する場合は、 MergedDictionaries で継承元を先に指定してあげると良さそう。

一般

RelativeSource

Mode=は省略可能。 FindAncestor で祖先をたどり、AncestorType で型を特定する。AncestorLevel で何番目にマッチした祖先かを決定する。

  <Border Background="AliceBlue" Margin="50">
    <Border Background="Aqua" Margin="50">
        <StackPanel>
            <TextBlock Background="SkyBlue" 
                       Text="{Binding ActualHeight, RelativeSource={RelativeSource Mode=Self}, StringFormat=Self: {0}}"/>
            <TextBlock Background="SkyBlue" 
                       Text="{Binding ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Border}, StringFormat=Ancestor Border1: {0}}"/>
            <TextBlock Background="SkyBlue" 
                       Text="{Binding ActualHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Border, AncestorLevel=2}, StringFormat=Ancestor Border2: {0}}"/>
        </StackPanel>
    </Border>
</Border>

f:id:yotiky:20200909180506p:plain:w350

Parameter などに struct を渡す

タグのネストを分解して定義する。

    <RowDefinition>
        <RowDefinition.Height>
            <Binding Path="IsVisible" Converter="{StaticResource Converter}">
                <Binding.ConverterParameter>
                    <GridLength>2*</GridLength>
                </Binding.ConverterParameter>
            </Binding>
        </RowDefinition.Height>
    </RowDefinition>

ショートカットキー

<Window.InputBindings>
    <KeyBinding Gesture="Ctrl+Shift+V" Command="{Binding NewPasteCommand}"/>
</Window.InputBindings>

ModifiersKey を使う場合、Modifiers は1個しか指定できない。

<Window.InputBindings>
    <KeyBinding Modifiers="Ctrl" Key="V" Command="{Binding PasteCommand}"/>
</Window.InputBindings>

Loadedイベントでコマンド呼び出す

xmlns:b="http://schemas.microsoft.com/xaml/behaviors"

<b:Interaction.Triggers>
    <b:EventTrigger EventName="Loaded">
        <b:InvokeCommandAction Command="{Binding LoadedCommand}"/>
    </b:EventTrigger>
</b:Interaction.Triggers>

デザイン時のみ~

.NET Core になって使えなくなったっぽい。コードビハインドも動かなかった。

.NET Core 3.0でWPFしてみました。Nekoni.DataValidation - 猫に.NET

Window

起動時のWindowのサイズ

<Window ...
    WindowState="Normal" />
WindowState
Maximized 最大化
Minimized 最小化
Normal 元のサイズ

グラデーションのかかった線を引く

    <Rectangle Height="5" Width="300">
        <Rectangle.Fill>
            <LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
                <GradientStop Color="Aqua" Offset="0.0" />
                <GradientStop Color="BlueViolet" Offset="1.0" />
            </LinearGradientBrush>
        </Rectangle.Fill>
    </Rectangle>

f:id:yotiky:20200908194404p:plain

Painting with Solid Colors and Gradients Overview - WPF .NET Framework | Microsoft Docs

RadioButton

グルーピング

    <GroupBox Header="Fruits" Margin="10">
        <StackPanel Margin="10">
            <RadioButton GroupName="Fruits" Content="Apple" />
            <RadioButton GroupName="Fruits" Content="Banana" />
        </StackPanel>
    </GroupBox>
    <GroupBox Header="Vegetables" Margin="10">
        <StackPanel Margin="10">
            <RadioButton GroupName="Vegetables" Content="Pumpkin" />
            <RadioButton GroupName="Vegetables" Content="SweetPotato" />
        </StackPanel>
    </GroupBox>

f:id:yotiky:20201011012305p:plain

enumをバインドするValueConverter

ラジオボタンを大きくする

<RadioButton GroupName="Fruits" Content="Apple">
    <RadioButton.LayoutTransform>
        <ScaleTransform ScaleX="2.0" ScaleY="2.0"/>
    </RadioButton.LayoutTransform>
</RadioButton>

f:id:yotiky:20201106174655p:plain

ToggleButtonなラジオボタン

RadioButton は ToggleButton を継承しているので、ToggleButtonのスタイルを適用できる。

    <StackPanel>
        <RadioButton Content="AAA"/>
        <RadioButton Content="BBB"/>
        <RadioButton Style="{StaticResource {x:Type ToggleButton}}" Content="CCC"/>
        <RadioButton Style="{StaticResource {x:Type ToggleButton}}" Content="DDD"/>
    </StackPanel>

f:id:yotiky:20200909162835g:plain

Button

Enterキーでボタンのクリックを処理する

Button に IsDefault="True" を設定する。

    <StackPanel FocusManager.FocusedElement="{Binding ElementName=textBox}">
        <TextBox x:Name="textBox" Margin="20"/>
        <Button Width="50" Height="30" Content="OK" Click="Button_Click" IsDefault="True"/>
    </StackPanel>

github.com

ボタンに画像を表示する

<Button Width="320" Height="180" Margin="10">
    <Grid>
        <Image Source="/Resources/ButtonImage.jpg" />
        <Rectangle Fill="Gray" Opacity="0.5" Visibility="{
Binding IsEnabled, 
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button}, 
Converter={StaticResource boolInvertToVisibilityConverter}}"/>
        <Label Content="Click" Foreground="White" FontSize="18" HorizontalAlignment="Center" VerticalAlignment="Bottom"/>
    </Grid>
</Button>

画像は Resources フォルダに追加して、プロパティのビルドアクションをリソースにする。Converter については後述

f:id:yotiky:20201124223836p:plain:w200 f:id:yotiky:20201124223524p:plain:w200

Grid

Row番号を表示する

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" Text="{Binding (Grid.Row), RelativeSource={RelativeSource Self}}"/>
        <TextBlock Grid.Row="1" Text="{Binding (Grid.Row), RelativeSource={RelativeSource Self}}"/>
        <TextBlock Grid.Row="2" Text="{Binding (Grid.Row), RelativeSource={RelativeSource Self}}"/>
    </Grid>

f:id:yotiky:20200909172339p:plain

Visibility をRow/Columnの長さに変換する ValueConverter

TextBlock

書式

        <TextBlock Text="{Binding Text1, StringFormat=Bindingの値 : {0}}"/>

f:id:yotiky:20200909174819p:plain

マルチバインディングで文字列を連結する

     <TextBlock>
        <Run Text="{Binding Text1}"/>
        <Run Text="{Binding Text2}"/>
        <Run Text="{Binding Text3}"/>
    </TextBlock>
    <TextBlock>
        <TextBlock.Text>
            <MultiBinding StringFormat="{}{0}{1}{2}">
                <Binding Path="Text1"/>
                <Binding Path="Text2"/>
                <Binding Path="Text3"/>
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>

f:id:yotiky:20200909174221p:plain

文字に縁取りを付ける

改行する

  • &#xa;&#10;&#13; を使う
    <TextBlock Text="aaa&#xa;bbb"/>
    <TextBlock Text="aaa&#10;bbb"/>
    <TextBlock Text="aaa&#13;bbb"/>
  • <LineBreak/> を使う
    <TextBlock >
        aaa<LineBreak/>bbb
    </TextBlock>
  • xml:space="preserve" を使う
    <TextBlock xml:space="preserve">aaa
bbb
    </TextBlock>

TextBox

複数行入力する

    <TextBox Margin="20" AcceptsReturn="True"/>

GroupBox

コンテンツを枠で囲む

    <GroupBox Header="Fruits" Margin="10">
        <StackPanel Margin="10">
            <RadioButton GroupName="Fruits" Content="Apple" />
            <RadioButton GroupName="Fruits" Content="Banana" />
        </StackPanel>
    </GroupBox>

f:id:yotiky:20201011011828p:plain

ScrollViewer

基本的な使い方

デフォルトだとコンテンツのサイズに関係なくどこでもスクロールの位置を決定できる。(参考

      <ScrollViewer>
        <StackPanel>
            <Button Content="ボタン"/>
            <Button Content="ボタン"/>
            <Button Content="ボタン"/>
        </StackPanel>
    </ScrollViewer>

f:id:yotiky:20201019201853p:plain f:id:yotiky:20201019201910p:plain

CanContentScroll のオプションでコンテンツのTOPにフィットするようにスクロールを制限できる。

CanContentScroll="True"

f:id:yotiky:20201019202600g:plain

VerticalScrollBarVisibility のオプションを Auto にすると、スクロールが不要なサイズの場合スクロールバー自体を非表示にできる。

VerticalScrollBarVisibility="Auto"

f:id:yotiky:20201019203001g:plain

ListBox

スクロールバーを表示する

    <ListBox ScrollViewer.CanContentScroll="True">
            <Button Content="ボタン"/>
            <Button Content="ボタン"/>
            <Button Content="ボタン"/>
    </ListBox>

f:id:yotiky:20201019203246g:plain

複数選択をViewModelにバインドする

Reactive Property を使った例。

<ListBox ItemsSource="{Binding Items}" DisplayMemberPath="Name.Value" SelectionMode="Multiple">
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected.Value}"/>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>
public partial class ListBoxSample : UserControl
{
    public ListBoxSample()
    {
        InitializeComponent();
        var vm = new ListBoxViewModel();
        // ここらでListBoxのデータを生成する処理
        DataContext = vm;
    }
}

public class ListBoxViewModel : ViewModelBase
{
    public ReactiveCollection<ListBoxItemViewModel> Items { get; } = new ReactiveCollection<ListBoxItemViewModel>();
}

public class ListBoxItemViewModel
{
    public ReactiveProperty<bool> IsSelected { get; } = new ReactiveProperty<bool>();
    public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}

f:id:yotiky:20201019204058g:plain

ComboBox

ViewModelとバインディング

<ComboBox ItemsSource="{Binding Items}" 
    DisplayMemberPath="Value" SelectedValuePath="Key"
    SelectedItem="{Binding SelectedItem.Value}"/>
public class ComboBoxSampleViewModel
{
    public ReactiveCollection<KeyValuePair<string, string>> Items { get; set; } = new ReactiveCollection<KeyValuePair<string, string>>();
    public ReactiveProperty<KeyValuePair<string, string>> SelectedItem { get; } = new ReactiveProperty<KeyValuePair<string, string>>();
}

TreeView

ViewModelとバインディング

XAML

<TreeView ItemsSource="{Binding Items}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="local:TreeViewSampleItemViewModel" ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Label.Value}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

ViewModel。

public class TreeViewSampleViewModel
{
    public ReactiveCollection<TreeViewSampleItemViewModel> Items { get; set; } = new ReactiveCollection<TreeViewSampleItemViewModel>();
}

public class TreeViewSampleItemViewModel
{
    public ReactiveProperty<string> Label { get; } = new ReactiveProperty<string>();
    public ReactiveCollection<TreeViewSampleItemViewModel> Children { get; } = new ReactiveCollection<TreeViewSampleItemViewModel>();

    public TreeViewSampleItemViewModel(string label)
    {
        Label.Value = label;
    }
}

データ例。

f:id:yotiky:20201021230321p:plain

コードビハインド。

public class TreeViewSample
{
    public TreeViewSample()
    {
        InitializeComponent();
        var vm = new TreeViewSampleViewModel();
        {
            var item = new TreeViewSampleItemViewModel("くだもの");
            item.Child.Add(new TreeViewSampleItemViewModel("りんご"));
            item.Child.Add(new TreeViewSampleItemViewModel("みかん"));
            item.Child.Add(new TreeViewSampleItemViewModel("バナナ"));
            vm.Items.Add(item);
        }
        {
            var item = new TreeViewSampleItemViewModel("肉");
            item.Child.Add(new TreeViewSampleItemViewModel("牛肉"));
            item.Child.Add(new TreeViewSampleItemViewModel("豚肉"));
            item.Child.Add(new TreeViewSampleItemViewModel("鶏肉"));
            vm.Items.Add(item);
        }
    
        DataContext = vm;
    }
}

その他、スクロールバーを表示するなど。

ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="True"

子のみ選択可能にする

実装例1。

親ノードはフォーカスできないようにする。 また、折り畳んだ時に親ノードが選択状態になるのでイベントで選択を解除する。

ItemContainerStyleEventSetter を使うパターン。コマンドは使えないっぽい。

XAML

<TreeView ItemsSource="{Binding Items}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="local:TreeViewSampleItemViewModel" ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Label.Value}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Style.Triggers>
                <Trigger Property="HasItems" Value="true">
                    <Setter Property="Focusable" Value="False" />
                </Trigger>
            </Style.Triggers>
            <EventSetter Event="Collapsed" Handler="TreeViewItem_Collapsed"/>
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>

コードビハインド。

        private void TreeViewItem_Collapsed(object sender, RoutedEventArgs e)
        {
            (sender as TreeViewItem).IsSelected = false;
        }

実装例2。

IsSelected プロパティを ViewModel とバインドし、変更通知を受けた時に親ノードだったら選択解除する。

XAML

<TreeView ItemsSource="{Binding Items}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="local:TreeViewSampleItemViewModel" ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Label.Value}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected.Value}"/>
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>

CollectionChangedAsObservableIsLeaf をハンドリングするので、コレクションに Add されない子の場合常に初期値となるので、初期値を true にしておかなく必要がある。

ViewModel。

public class TreeViewSampleItemViewModel
{
    public ReactiveProperty<string> Label { get; } = new ReactiveProperty<string>();
    public ReactiveCollection<TreeViewSampleItemViewModel> Children { get; } = new ReactiveCollection<TreeViewSampleItemViewModel>();
    public ReactiveProperty<bool> IsSelected { get; } = new ReactiveProperty<bool>();
    public ReactiveProperty<bool> IsNode { get; }
    public ReactiveProperty<bool> IsLeaf { get; }

    public TreeViewSampleItemViewModel(string label)
    {
        Label.Value = label;
        IsLeaf = Children.CollectionChangedAsObservable().Select(_ => Children.Count == 0).ToReactiveProperty(true);
        IsNode = IsLeaf.Select(x => !x).ToReactiveProperty();

        IsSelected.Subscribe(x =>
        {
            if (x && IsNode.Value)
            {
                IsSelected.Value = false;
            }
        });
    }
}

チェックボックスで複数選択 + 子のみ選択可能

f:id:yotiky:20201021235739p:plain

XAML

<TreeView ItemsSource="{Binding Items}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate DataType="local:TreeViewSampleItemViewModel" ItemsSource="{Binding Children}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Label.Value}" Visibility="{Binding IsNode.Value, Converter={StaticResource BoolToVisibility}}"/>
                <CheckBox Content="{Binding Label.Value}" Visibility="{Binding IsLeaf.Value, Converter={StaticResource BoolToVisibility}}"
                          IsChecked="{Binding IsSelected.Value}"/>
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="False"/>
            <Setter Property="Focusable" Value="False"/>
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>

ViewModel。

    public class TreeViewSampleItemViewModel
    {
        public ReactiveProperty<string> Label { get; } = new ReactiveProperty<string>();
        public ReactiveCollection<TreeViewSampleItemViewModel> Children { get; } = new ReactiveCollection<TreeViewSampleItemViewModel>();
        public ReactiveProperty<bool> IsSelected { get; } = new ReactiveProperty<bool>();
        public ReactiveProperty<bool> IsNode { get; }
        public ReactiveProperty<bool> IsLeaf { get; }

        public TreeViewSampleItemViewModel(string label)
        {
            Label.Value = label;
            IsLeaf = Children.CollectionChangedAsObservable().Select(_ => Children.Count == 0).ToReactiveProperty(true);
            IsNode = IsLeaf.Select(x => !x).ToReactiveProperty();
        }
    }

ValueConverter

BooleanToVisibilityConverter

名前空間を宣言しなくても使えるようになっている。(.NET Core 3.1)

    <UserControl.Resources>
        <BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
    </UserControl.Resources>

enumをバインドするValueConverter

Visibility をRow/Columnの長さに変換する ValueConverter

Visibility を変換する ValueConverter

[ValueConversion(typeof(Visibility), typeof(GridLength))]
public class VisibilityToGridLengthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is Visibility == false)
            throw new ArgumentException("value type is not supported.");
        if (parameter != null && parameter is GridLength == false)
            throw new ArgumentException("parameter type is not supported.");

        var visibility = (Visibility)value;
        var result = parameter != null
            ? (GridLength)parameter
            : new GridLength(1, GridUnitType.Star);

        return visibility == Visibility.Visible ? result : new GridLength(0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

xaml

    <Grid Margin="10">
        <Grid.Resources>
            <local:VisibilityToGridLengthConverter x:Key="Converter"/>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="{Binding Collapsed, Converter={StaticResource Converter}}"/>
            <RowDefinition Height="{Binding Visible, Converter={StaticResource Converter}}"/>
            <RowDefinition>
                <RowDefinition.Height>
                    <Binding Path="Visible" Converter="{StaticResource Converter}">
                        <Binding.ConverterParameter>
                            <!--<GridLength>50</GridLength>-->
                            <!--<GridLength>Auto</GridLength>-->
                            <!--<GridLength>*</GridLength>-->
                            <GridLength>2*</GridLength>
                        </Binding.ConverterParameter>
                    </Binding>
                </RowDefinition.Height>
            </RowDefinition>
        </Grid.RowDefinitions>
        <Label Grid.Row="0" BorderThickness="1" BorderBrush="Black" Content="Height:50"/>
        <Label Grid.Row="1" BorderThickness="1" BorderBrush="Black" Content="Height:Auto"/>
        <Label Grid.Row="2" BorderThickness="1" BorderBrush="Black" Content="Height:*"/>
        <Label Grid.Row="3" BorderThickness="1" BorderBrush="Black" Content="Height:2*"/>
        <Label Grid.Row="4" BorderThickness="1" BorderBrush="Black" Content="Height:Converter Collapsed"/>
        <Label Grid.Row="5" BorderThickness="1" BorderBrush="Black" Content="Height:Converter Visible"/>
        <Label Grid.Row="6" BorderThickness="1" BorderBrush="Black" Content="Height:Converter Visible 2*"/>
    </Grid>

f:id:yotiky:20201011025227p:plain

bool を変換する場合

[ValueConversion(typeof(bool), typeof(GridLength))]
public class BoolToGridLengthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool == false)
            throw new ArgumentException("value type is not supported.");
        if (parameter != null && parameter is GridLength == false)
            throw new ArgumentException("parameter type is not supported.");

        var isVisible = (bool)value;
        var result = parameter != null
            ? (GridLength)parameter
            : new GridLength(1, GridUnitType.Star);

        return isVisible ? result : new GridLength(0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

double を変換する場合

[ValueConversion(typeof(double), typeof(GridLength))]
public class DoubleToGridLengthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is double == false)
            throw new ArgumentException("value's type is not supported.");
        if (parameter != null && parameter is GridLength == false)
            throw new ArgumentException("parameter's type is not supported.");

        var length = (double)value;
        var result = parameter != null ? (GridLength)parameter
            : double.IsNaN(length) ? new GridLength(1, GridUnitType.Star)
            : new GridLength(length);

        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

bool を反転する ValueConverter

[ValueConversion(typeof(bool), typeof(bool))]
public class BoolInvertConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null || value is bool == false)
            return false;

        return !(bool)value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null || value is bool == false)
            return false;

        return !(bool)value;
    }
}

複数の ValueConverter を組み合わる ValueConverter

[ContentProperty(nameof(Converters))]
public class CompositeValueConverter : IValueConverter
{
    public Collection<IValueConverter> Converters { get; } = new Collection<IValueConverter>();

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (Converters == null) { return value; }

        var result = value;
        foreach (var conv in Converters)
        {
            result = conv.Convert(result, targetType, parameter, culture);
        }

        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (Converters == null) { return value; }

        var result = value;
        foreach (var conv in Converters.Reverse())
        {
            result = conv.Convert(result, targetType, parameter, culture);
        }

        return result;
    }
}

bool を反転して Visibility に変換するコンバーターの例。

<UserControl.Resources>
    <conv:CompositeValueConverter x:Key="boolInvertToVisibilityConverter">
        <conv:BoolInvertConverter/>
        <BooleanToVisibilityConverter/>
    </conv:CompositeValueConverter>
</UserControl.Resources>

参考)WPF 複数のValueConverterを連結して順番に変換する - OITA: Oika's Information Technological Activities

Command

CommandParameter に enum を指定する

public enum Food
{
    Banana,
    Apple,
}

名前空間を宣言して

xmlns:local="clr-namespace:SampleApp.Data"

x:Static で指定する

<Button CommandParameter="{x:Static local:Food.Apple}" />

CommandParameter にプリミティブ型を使用する

    xmlns:system="clr-namespace:System;assembly=System.Runtime"
<Button Content="はい" Command="{Binding ConfirmCommand}" >
    <Button.CommandParameter>
        <system:Boolean>true</sys:Boolean>
    </Button.CommandParameter>
</Button>

フォーカス

基本的な使い方

XAMLの場合。

<Grid FocusManager.FocusedElement="{Binding ElementName=button}">
    ...
</Grid>

コードビハインドの場合。

button.Focus();

ViewModelから設定

ViewModelからフォーカスを制御するには、FocusBehaviorを作成して設定する。

public static class FocusBehavior
{
    public static bool GetIsFocused(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsFocusedProperty);
    }

    public static void SetIsFocused(DependencyObject obj, bool value)
    {
        obj.SetValue(IsFocusedProperty, value);
    }

    public static readonly DependencyProperty IsFocusedProperty =
        DependencyProperty.RegisterAttached("IsFocused", typeof(bool), typeof(FocusBehavior), new UIPropertyMetadata(false, OnIsFocusedPropertyChanged));

    private static void OnIsFocusedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var ui = (UIElement)d;
        if ((bool)e.NewValue)
        {
            ui.Focus();
        }
    }
}

XAMLとViewModel。任意のタイミングで IsFocused の値を true にするとフォーカスを設定できる。

<Button b:FocusBehavior.IsFocused="{Binding IsFocused.Value}"/>
        public ReactiveProperty<bool> IsFocused { get; } = new ReactiveProperty<bool>();

アニメーション

点滅

Resource に Storyboard を定義する。

<Storyboard x:Key="BlinkAnimation">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" RepeatBehavior="Forever" AutoReverse="True">
        <LinearDoubleKeyFrame KeyTime="0:0:0.2" Value="1" />
        <LinearDoubleKeyFrame KeyTime="0:0:0.8" Value="0" />
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

使い方。

<TextBlock Text="Click me">
    <TextBlock.Triggers>
        <EventTrigger RoutedEvent="TextBlock.Loaded">
            <BeginStoryboard Storyboard="{StaticResource BlinkAnimation}"/>
        </EventTrigger>
    </TextBlock.Triggers>
</TextBlock>

error MC3000: XML 指定されたエンコードに無効な文字があります。

XAMLファイルのエンコードシフトJISなどで保存されており日本語を使うと文字化けしてる可能性があるので、XAMLファイルのエンコードUTF-8にして上書き保存する。

.NET Core クラスライブラリでWPFの機能を使う

<UseWPF>true</UseWPF> をプロジェクトファイルに追加する。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

アプリケーション一般

アプリケーションのバージョンを取得する

var assembly = Assembly.GetExecutingAssembly();
var name = assembly.GetName();
var version = name.Version;
var strVersion = version?.ToString();

二重起動の防止

Mutex を使う場合。

public partial class App : Application
{
    private static Mutex _mutex;

    protected override void OnStartup(StartupEventArgs e)
    {
        _mutex = new Mutex(false, "一意となるName");
        if (!_mutex.WaitOne(0, false))
        {
            MessageBox.Show("既に起動しています");
            _mutex.Close();
            _mutex = null;
            Shutdown();
        }

        base.OnStartup(e);
    }
    protected override void OnExit(ExitEventArgs e)
    {
        if (_mutex != null) 
        {
            _mutex.ReleaseMutex();
            _mutex.Close();
            _mutex = null;
        }
    
        base.OnExit(e);
    }
}

Semaphore を使う場合。

public partial class App : Application
{
    private static Semaphore semaphore;

    protected override void OnStartup(StartupEventArgs e)
    {
        semaphore = new Semaphore(1, 1, "一意となるName", out var createdNew);
        if (!createdNew)
        {
            MessageBox.Show("既に起動しています");
            Shutdown();
        }

        base.OnStartup(e);
    }
    protected override void OnExit(ExitEventArgs e)
    {
        if (semaphore != null)
        {
            semaphore.Dispose();
            semaphore = null;
        }

        base.OnExit(e);
    }
}

未処理例外の捕捉