WPFでImageのSourceが変更されたらスクロール位置を初期化するAttached Property

コンピュータ

ImageにはSourceの変更を通知するイベントは存在しませんが、

DependencyPropertyDescriptor.FromProperty()でImageのSourceプロパティを監視し、

変更があった場合SourceChangedEventというイベントが呼び出される仕掛けになっています。

基本的にAttached Propertyはコントロールにプロパティを生やす仕組みですが、

今回の方法を使うと、複数のコントロールを連携させる使い方が出来るようです。

実行例


ImageSourceWatcher.cs

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace DependencyPropertyDescriptorDemo;

public static class ImageSourceWatcher
{
    /* ============================
     * IsEnabled AttachedProperty
     * ============================ */

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached(
            "IsEnabled",
            typeof(bool),
            typeof(ImageSourceWatcher),
            new PropertyMetadata(false, OnIsEnabledChanged));

    public static void SetIsEnabled(DependencyObject obj, bool value)
        => obj.SetValue(IsEnabledProperty, value);

    public static bool GetIsEnabled(DependencyObject obj)
        => (bool)obj.GetValue(IsEnabledProperty);


    /* ============================
     * 変更監視
     * ============================ */

    private static readonly DependencyPropertyDescriptor? _sourceDescriptor =
        DependencyPropertyDescriptor.FromProperty(
            Image.SourceProperty,
            typeof(Image));

    private static void OnIsEnabledChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        if (d is not Image image)
            return;

        if ((bool)e.NewValue)
        {
            _sourceDescriptor?.AddValueChanged(image, OnSourceChanged);
        }
        else
        {
            _sourceDescriptor?.RemoveValueChanged(image, OnSourceChanged);
        }
    }

    private static void OnSourceChanged(object? sender, EventArgs e)
    {
        if (sender is not Image image)
            return;

        var args = new RoutedEventArgs(SourceChangedEvent, image);
        image.RaiseEvent(args);
    }


    /* ============================
     * 独自 RoutedEvent
     * ============================ */

    public static readonly RoutedEvent SourceChangedEvent =
        EventManager.RegisterRoutedEvent(
            "SourceChanged",
            RoutingStrategy.Bubble,
            typeof(RoutedEventHandler),
            typeof(ImageSourceWatcher));

    public static void AddSourceChangedHandler(
        DependencyObject d,
        RoutedEventHandler handler)
    {
        if (d is UIElement u)
            u.AddHandler(SourceChangedEvent, handler);
    }

    public static void RemoveSourceChangedHandler(
        DependencyObject d,
        RoutedEventHandler handler)
    {
        if (d is UIElement u)
            u.RemoveHandler(SourceChangedEvent, handler);
    }
}

MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;

namespace DependencyPropertyDescriptorDemo;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // ウィンドウがロード後、画像の読み込みと表示
        this.Loaded += (s, e) =>
        {
            string path = @"C:\Users\karet\Pictures\20260120\4e1699fd-d672-4fb4-8c72-4cf1f36f0c53~1.jpg";
            Image1.Source = new BitmapImage(new Uri(path));
        };
    }
    // 画像のSourceプロパティが変更されたときのイベントハンドラー
    private void OnImageSourceChanged(object sender, RoutedEventArgs e)
    {
        // 画像のサイズに合わせてCanvasのサイズを調整し、スクロール位置を中央に設定
		if (Image1.Source is not BitmapSource bitmap) return;

		Canvas1.Width = bitmap.PixelWidth;
        Canvas1.Height = bitmap.PixelHeight;

        double x = (Canvas1.Width - Scroll1.ViewportWidth) / 2;
        double y = (Canvas1.Height - Scroll1.ViewportHeight) / 2;
        Scroll1.ScrollToHorizontalOffset(x);
        Scroll1.ScrollToVerticalOffset(y);
    }
}

MainWindow.xaml

<Window x:Class="DependencyPropertyDescriptorDemo.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:local="clr-namespace:DependencyPropertyDescriptorDemo"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="300">
    <Grid>
        <ScrollViewer
            x:Name="Scroll1"
            HorizontalScrollBarVisibility="Visible"
            VerticalScrollBarVisibility="Visible"
            local:ImageSourceWatcher.SourceChanged="OnImageSourceChanged">  <!-- ScrollViewerに画像のSource変更イベントをバインド -->

            <Canvas x:Name="Canvas1">
                <Image
                    x:Name="Image1"
                    Stretch="None"
                    local:ImageSourceWatcher.IsEnabled="True"/> <!-- Imageに画像のSource変更を監視するためのAttached Propertyを設定 -->
            </Canvas>

        </ScrollViewer>
    </Grid>
</Window>

コメント