WPF で画面端に張り付かせるビヘイビアを書いた。


当日に慌ててアドベントカレンダー記事を書くという体たらく

前提

必要なデータ

  • SystemParameters.VirtualScreenWidth
    画面全体の大きさ
  • SystemParameters.VirtualScreenHeight
    画面全体の高さ
  • Window の現在地 (Left, Top)

何がしたかったのか

Left, Top の値をバインディングしながら、ウィンドウ座標を変えたかった。

しかし、直接 Left Top に値をバインディングすると、ウィンドウ読み込み後にバインディングが切れる。

解決した方法

XAML で簡単にバインディングできるようにしたかったので、ビヘイビアにした。
書いたコードは以下

public class WindowLocationBehavior : Behavior<Window>
{
    public double Left
    {
        get => (double)this.GetValue(LeftProperty);
        set => this.SetValue(LeftProperty, value);
    }

    public static readonly DependencyProperty LeftProperty =
        DependencyProperty.Register("Left", typeof(double), typeof(WindowLocationBehavior), new UIPropertyMetadata((double)0.0));

    public double Top
    {
        get => (double)this.GetValue(TopProperty);
        set => this.SetValue(TopProperty, value);
    }

    public static readonly DependencyProperty TopProperty =
        DependencyProperty.Register("Top", typeof(double), typeof(WindowLocationBehavior), new UIPropertyMetadata(
            (double)0.0));

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.Top = this.Top;
        AssociatedObject.Left = this.Left;
        AssociatedObject.LocationChanged += (sender, e) =>
        {
            var windowRightEdge = SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth;
            if (windowRightEdge - ((Window)sender).Width < ((Window)sender).Left) ((Window)sender).Left = windowRightEdge - ((Window)sender).Width;
            else if (SystemParameters.VirtualScreenLeft > ((Window)sender).Left) ((Window)sender).Left = SystemParameters.VirtualScreenLeft;
            if (SystemParameters.VirtualScreenHeight - ((Window)sender).Height < ((Window)sender).Top) ((Window)sender).Top = SystemParameters.VirtualScreenHeight - ((Window)sender).Height;
            else if (0 > ((Window)sender).Top) ((Window)sender).Top = 0;
            this.Left = ((Window)sender).Left;
            this.Top = ((Window)sender).Top;
        };
    }
}

解説

CSharp 側

public double Left
{
    get => (double)this.GetValue(LeftProperty);
    set => this.SetValue(LeftProperty, value);
}

public static readonly DependencyProperty LeftProperty =
    DependencyProperty.Register("Left", typeof(double),
    typeof(WindowLocationBehavior),
    new UIPropertyMetadata((double)0.0));

public double Top
{
    get => (double)this.GetValue(TopProperty);
    set => this.SetValue(TopProperty, value);
}

public static readonly DependencyProperty TopProperty =
    DependencyProperty.Register("Top", typeof(double),
    typeof(WindowLocationBehavior),
    new UIPropertyMetadata((double)0.0));

ここらへんはよくある添付プロパティと同じ。
依存関係プロパティの宣言

protected override void OnAttached()
{
    base.OnAttached();

    AssociatedObject.Top = this.Top;
    AssociatedObject.Left = this.Left;
    var windowRightEdge = SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth;
    AssociatedObject.LocationChanged += (sender, e) =>
    {
        if (windowRightEdge - ((Window)sender).Width < ((Window)sender).Left)
            ((Window)sender).Left = windowRightEdge - ((Window)sender).Width;
        else if (SystemParameters.VirtualScreenLeft > ((Window)sender).Left)
            ((Window)sender).Left = SystemParameters.VirtualScreenLeft;
        if (SystemParameters.VirtualScreenHeight - ((Window)sender).Height < ((Window)sender).Top)
            ((Window)sender).Top = SystemParameters.VirtualScreenHeight - ((Window)sender).Height;
        else if (0 > ((Window)sender).Top)
            ((Window)sender).Top = 0;
        this.Left = ((Window)sender).Left;
        this.Top = ((Window)sender).Top;
    };
}

アタッチされた時に、アタッチされたウィンドウの座標をバンディングされてる値で書き換える。

ウィンドウの右端の座標を計算する。(左側のモニターの座標がマイナス値だったりするので重要。)

ウィンドウの座標変化時のイベントを購読して以下の処理をする。

  • 右端のx座標値からウィンドウ幅を引いた値 が ウィンドウの左端上座標より小さい場合
    右端に張り付かせる。
  • 左端のx座標値 がウィンドウの左端上x座標値よりも大きい場合
    左端に張り付かせる

XAML 側

こんな感じで書く

<Window x:Class="Test.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:Test.Views"
        xmlns:be="clr-namespace:Test.Behaviors"
        mc:Ignorable="d"
        Title="Sample" SizeToContent="WidthAndHeight">
    <i:Interaction.Behaviors>
        <be:WindowLocationBehavior Left="{Binding WindowManager.WindowData.Left, Mode=TwoWay}"
            Top="{Binding WindowManager.WindowData.Top, Mode=TwoWay}"/>
    </i:Interaction.Behaviors>
</Window>

Interaction.Behaviors に登録、Left と Top の値にそれぞれバインディングする値を書いて終わり。

まとめ

ビヘイビア自作をすれば、バインディング値を使いながらイベント購読して値を書き換えたりできる。
Microsoft.Xaml.Behaviors はいいぞ。