web-dev-qa-db-ja.com

キャンバス上のレイヤーを使用したWPF画像のパン、ズーム、スクロール

誰かがここで私を助けてくれることを願っています。私は、カメラからライブ画像を取得して、ユーザーが画像を表示し、その後、その画像の関心領域(ROI)を強調表示できるWPFイメージングアプリケーションを構築しています。次に、ROIに関する情報(幅、高さ、画像上のポイントに対する相対的な位置など)がカメラに返送され、バーコード、テキスト、液面、ターンなどを探す場所をカメラファームウェアに通知/トレーニングします。画像のネジ等)。必要な機能は、画像とそのROIをパンおよびズームする機能と、画像が表示領域よりも大きくズームされたときにスクロールする機能です。 ROIのStrokeThicknessとFontSizeは元のスケールを維持する必要がありますが、ROI内の形状の幅と高さは画像に合わせてスケーリングする必要があります(これは、カメラに送信する正確なピクセル位置をキャプチャするために重要です)。スクロールと他のいくつかの問題を除いて、私はこれのほとんどを解決しました。私の2つの懸念事項は次のとおりです。

  1. ScrollViewerを導入すると、スクロール動作が発生しません。私が理解しているように、正しいScrollViewerの動作を取得するには、LayoutTransformを導入する必要があります。ただし、そうすると、他の領域が壊れ始めます(たとえば、ROIが画像上で正しい位置を保持していない、パン時にマウスポインターが画像上の選択したポイント、または画像の左隅から離れ始めます) MouseDownの現在のマウス位置にバウンスします。)

  2. ROIのスケーリングを必要な方法で取得することはできません。私はこれを機能させていますが、理想的ではありません。私が持っているものは正確なストロークの太さを保持しておらず、テキストブロックのスケールを無視することを検討していません。うまくいけば、コードサンプルで私が何をしているのかがわかるでしょう。

私の問題は、変換とWPFレイアウトシステムとの関係についての理解の欠如に関係していると確信しています。うまくいけば、私がこれまでに達成したことを示すコードの表現が役立つでしょう(以下を参照)。

参考までに、Adornersが提案である場合、サポートされているよりも多くのAdornerが発生する可能性があるため、私のシナリオでは機能しない可能性があります(噂では、144人のAdornerが崩壊し始めます)。

まず、以下はROI(テキストと形状)の画像を示すスクリーンショットです。長方形、楕円、テキストは、拡大縮小と回転で画像上の領域に従う必要がありますが、太さやフォントサイズで拡大縮小するべきではありません。

Screen shot showing sample image with ROIs

上の画像を表示しているXAMLと、ズーム用のスライダー(マウスホイールのズームは後で表示されます)

<Window x:Class="PanZoomStackOverflow.MainWindow"
    xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.Microsoft.com/expression/blend/2008"
    mc:Ignorable="d"
    Title="MainWindow" Height="768" Width="1024">

<DockPanel>
  <Slider x:Name="_ImageZoomSlider" DockPanel.Dock="Bottom"
          Value="2"
          HorizontalAlignment="Center" Margin="6,0,0,0" 
          Width="143" Minimum=".5" Maximum="20" SmallChange=".1" 
          LargeChange=".2" TickFrequency="2" 
          TickPlacement="BottomRight" Padding="0" Height="23"/>

  <!-- This resides in a user control in my solution -->
  <Grid x:Name="LayoutRoot">
    <ScrollViewer Name="border" HorizontalScrollBarVisibility="Auto" 
                  VerticalScrollBarVisibility="Auto">
      <Grid x:Name="_ImageDisplayGrid">
        <Image x:Name="_DisplayImage" Margin="2" Stretch="None"
               Source="Untitled.bmp"
               RenderTransformOrigin ="0.5,0.5"
               RenderOptions.BitmapScalingMode="NearestNeighbor"
               MouseLeftButtonDown="ImageScrollArea_MouseLeftButtonDown"
               MouseLeftButtonUp="ImageScrollArea_MouseLeftButtonUp"
               MouseMove="ImageScrollArea_MouseMove">                            
           <Image.LayoutTransform>
             <TransformGroup>
               <ScaleTransform />
               <TranslateTransform />
             </TransformGroup>
           </Image.LayoutTransform>
         </Image>
         <AdornerDecorator> <!-- Using this Adorner Decorator for Move, Resize and Rotation and feedback adornernments -->
           <Canvas x:Name="_ROICollectionCanvas"
                   Width="{Binding ElementName=_DisplayImage, Path=ActualWidth, Mode=OneWay}"
                   Height="{Binding ElementName=_DisplayImage, Path=ActualHeight, Mode=OneWay}"
                   Margin="{Binding ElementName=_DisplayImage, Path=Margin, Mode=OneWay}">

             <!-- This is a user control in my solution -->
             <Grid IsHitTestVisible="False" Canvas.Left="138" Canvas.Top="58" Height="25" Width="186">
               <TextBlock Text="Rectangle ROI" HorizontalAlignment="Center" VerticalAlignment="Top" 
                          Foreground="Orange" FontWeight="Bold" Margin="0,-15,0,0"/>
                 <Rectangle StrokeThickness="2" Stroke="Orange"/>
             </Grid>

             <!-- This is a user control in my solution -->
             <Grid IsHitTestVisible="False" Canvas.Left="176" Canvas.Top="154" Height="65" Width="69">
               <TextBlock Text="Ellipse ROI" HorizontalAlignment="Center" VerticalAlignment="Top" 
                          Foreground="Orange" FontWeight="Bold" Margin="0,-15,0,0"/>
               <Ellipse StrokeThickness="2" Stroke="Orange"/>
             </Grid>
           </Canvas>
         </AdornerDecorator>
       </Grid>
     </ScrollViewer>
  </Grid>
</DockPanel>

これがパンとズームを管理するC#です。

public partial class MainWindow : Window
{
private Point Origin;
private Point start;
private Slider _slider;

public MainWindow()
{
    this.InitializeComponent();

    //Setup a transform group that we'll use to manage panning of the image area
    TransformGroup group = new TransformGroup();
    ScaleTransform st = new ScaleTransform();
    group.Children.Add(st);
    TranslateTransform tt = new TranslateTransform();
    group.Children.Add(tt);
    //Wire up the slider to the image for zooming
    _slider = _ImageZoomSlider;
    _slider.ValueChanged += _ImageZoomSlider_ValueChanged;
    st.ScaleX = _slider.Value;
    st.ScaleY = _slider.Value;
    //_ImageScrollArea.RenderTransformOrigin = new Point(0.5, 0.5);
    //_ImageScrollArea.LayoutTransform = group;
    _DisplayImage.RenderTransformOrigin = new Point(0.5, 0.5);
    _DisplayImage.RenderTransform = group;
    _ROICollectionCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
    _ROICollectionCanvas.RenderTransform = group;
}

//Captures the mouse to prepare for panning the scrollable image area
private void ImageScrollArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    _DisplayImage.ReleaseMouseCapture();
}

//Moves/Pans the scrollable image area  assuming mouse is captured.
private void ImageScrollArea_MouseMove(object sender, MouseEventArgs e)
{
    if (!_DisplayImage.IsMouseCaptured) return;

    var tt = (TranslateTransform)((TransformGroup)_DisplayImage.RenderTransform).Children.First(tr => tr is TranslateTransform);

    Vector v = start - e.GetPosition(border);
    tt.X = Origin.X - v.X;
    tt.Y = Origin.Y - v.Y;
}

//Cleanup for Move/Pan when mouse is released
private void ImageScrollArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    _DisplayImage.CaptureMouse();
    var tt = (TranslateTransform)((TransformGroup)_DisplayImage.RenderTransform).Children.First(tr => tr is TranslateTransform);
    start = e.GetPosition(border);
    Origin = new Point(tt.X, tt.Y);
}

//Zoom according to the slider changes
private void _ImageZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    //Panel panel = _ImageScrollArea;
    Image panel = _DisplayImage;

    //Set the scale coordinates on the ScaleTransform from the slider
    ScaleTransform transform = (ScaleTransform)((TransformGroup)panel.RenderTransform).Children.First(tr => tr is ScaleTransform);
    transform.ScaleX = _slider.Value;
    transform.ScaleY = _slider.Value;


    //Set the zoom (this will affect rotate too) Origin to the center of the panel
    panel.RenderTransformOrigin = new Point(0.5, 0.5);

    foreach (UIElement child in _ROICollectionCanvas.Children)
    {
        //Assume all shapes are contained in a panel
        Panel childPanel = child as Panel;

        var x = childPanel.Children;

        //Shape width and heigh should scale, but not StrokeThickness
        foreach (var shape in childPanel.Children.OfType<Shape>())
        {
            if (shape.Tag == null)
            {
                //Hack: This is be a property on a usercontrol in my solution
                shape.Tag = shape.StrokeThickness;
            }
            double orignalStrokeThickness = (double)shape.Tag;

            //Attempt to keep the underlying shape border/stroke from thickening as well
            double newThickness = shape.StrokeThickness - (orignalStrokeThickness / transform.ScaleX);

            shape.StrokeThickness -= newThickness;
        }
    }
}
}

コードは、カットアンドペーストエラーがないと仮定して、.NET4.0または4.5プロジェクトおよびソリューションで機能するはずです。

何かご意見は?提案は大歓迎です。

14
KyleLib

OK。これはあなたが説明したことに対する私の見解です。

次のようになります。

enter image description here

  • RenderTransformsを適用していないので、必要なScrollbar/ScrollViewer機能を取得できます。
  • MVVM、これはWPFに移行する方法です。 UIとデータは独立しているため、DataItemにはX、Y、Width、Heightなどのdoubleプロパティとintプロパティのみがあり、目的に応じて使用したり、データベースに保存したりすることもできます。
  • パンを処理するために、すべてのものをThumb内に追加しました。 ResizerControlを介してROIをドラッグ/サイズ変更するときに発生するパンについては、引き続き何かを行う必要があります。 Mouse.DirectlyOverか何かをチェックできると思います。
  • 私は実際にListBoxを使用してROIを処理したので、いつでも1つのROIを選択できます。これにより、サイズ変更機能が切り替わります。そのため、ROIをクリックすると、リサイザーが表示されます。
  • スケーリングはViewModelレベルで処理されるため、カスタムPanelsなどの必要がなくなります(@Clemensのソリューションも優れています)
  • シェイプを定義するためにEnumといくつかのDataTriggersを使用しています。 DataTemplate DataType={x:Type local:ROI}の部分を参照してください。
  • WPFロックス。コードをコピーしてFile -> New Project -> WPF Applicationに貼り付けるだけで、結果を自分で確認できます。

    <Window x:Class="MiscSamples.PanZoomStackOverflow_MVVM"
            xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
            xmlns:local="clr-namespace:MiscSamples"
            Title="PanZoomStackOverflow_MVVM" Height="300" Width="300">
       <Window.Resources>
        <DataTemplate DataType="{x:Type local:ROI}">
            <Grid Background="#01FFFFFF">
                <Path x:Name="Path" StrokeThickness="2" Stroke="Black"
                      Stretch="Fill"/>
                <local:ResizerControl Visibility="Collapsed" Background="#30FFFFFF"
                                      X="{Binding X}" Y="{Binding Y}"
                                      ItemWidth="{Binding Width}"
                                      ItemHeight="{Binding Height}"
                                      x:Name="Resizer"/>
            </Grid>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="True">
                    <Setter TargetName="Resizer" Property="Visibility" Value="Visible"/>
                </DataTrigger>
                <DataTrigger Binding="{Binding Shape}" Value="{x:Static local:Shapes.Square}">
                    <Setter TargetName="Path" Property="Data">
                        <Setter.Value>
                            <RectangleGeometry Rect="0,0,10,10"/>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
    
                <DataTrigger Binding="{Binding Shape}" Value="{x:Static local:Shapes.Round}">
                    <Setter TargetName="Path" Property="Data">
                        <Setter.Value>
                            <EllipseGeometry RadiusX="10" RadiusY="10"/>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    
        <Style TargetType="ListBox" x:Key="ROIListBoxStyle">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <Canvas/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <ItemsPresenter/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style TargetType="ListBoxItem" x:Key="ROIItemStyle">
            <Setter Property="Canvas.Left" Value="{Binding ActualX}"/>
            <Setter Property="Canvas.Top" Value="{Binding ActualY}"/>
            <Setter Property="Height" Value="{Binding ActualHeight}"/>
            <Setter Property="Width" Value="{Binding ActualWidth}"/>
    
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ListBoxItem">
                        <ContentPresenter ContentSource="Content"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    
    </Window.Resources>
    
    <DockPanel>
        <Slider VerticalAlignment="Center" 
                Maximum="2" Minimum="0" Value="{Binding ScaleFactor}" SmallChange=".1"
                DockPanel.Dock="Bottom"/>
    
        <ScrollViewer VerticalScrollBarVisibility="Visible"
                      HorizontalScrollBarVisibility="Visible" x:Name="scr"
                      ScrollChanged="ScrollChanged">
            <Thumb DragDelta="Thumb_DragDelta">
                <Thumb.Template>
                    <ControlTemplate>
                        <Grid>
                            <Image Source="/Images/Homer.jpg" Stretch="None" x:Name="Img"
                                    VerticalAlignment="Top" HorizontalAlignment="Left">
                                <Image.LayoutTransform>
                                    <TransformGroup>
                                        <ScaleTransform ScaleX="{Binding ScaleFactor}" ScaleY="{Binding ScaleFactor}"/>
                                    </TransformGroup>
                                </Image.LayoutTransform>
                            </Image>
    
                            <ListBox ItemsSource="{Binding ROIs}"
                                     Width="{Binding ActualWidth, ElementName=Img}"
                                     Height="{Binding ActualHeight,ElementName=Img}"
                                     VerticalAlignment="Top" HorizontalAlignment="Left"
                                     Style="{StaticResource ROIListBoxStyle}"
                                     ItemContainerStyle="{StaticResource ROIItemStyle}"/>
                        </Grid>
                    </ControlTemplate>
                </Thumb.Template>
            </Thumb>
        </ScrollViewer>
    </DockPanel>
    

コードビハインド:

public partial class PanZoomStackOverflow_MVVM : Window
    {
        public PanZoomViewModel ViewModel { get; set; }

        public PanZoomStackOverflow_MVVM()
        {
            InitializeComponent();
            DataContext = ViewModel = new PanZoomViewModel();

            ViewModel.ROIs.Add(new ROI() {ScaleFactor = ViewModel.ScaleFactor, X = 150, Y = 150, Height = 200, Width = 200, Shape = Shapes.Square});

            ViewModel.ROIs.Add(new ROI() { ScaleFactor = ViewModel.ScaleFactor, X = 50, Y = 230, Height = 102, Width = 300, Shape = Shapes.Round });
        }

        private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            //TODO: Detect whether a ROI is being resized / dragged and prevent Panning if so.
            IsPanning = true;
            ViewModel.OffsetX = (ViewModel.OffsetX + (((e.HorizontalChange/10) * -1) * ViewModel.ScaleFactor));
            ViewModel.OffsetY = (ViewModel.OffsetY + (((e.VerticalChange/10) * -1) * ViewModel.ScaleFactor));

            scr.ScrollToVerticalOffset(ViewModel.OffsetY);
            scr.ScrollToHorizontalOffset(ViewModel.OffsetX);

            IsPanning = false;
        }

        private bool IsPanning { get; set; }

        private void ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            if (!IsPanning)
            {
                ViewModel.OffsetX = e.HorizontalOffset;
                ViewModel.OffsetY = e.VerticalOffset;
            }
        }
    }

メインビューモデル:

public class PanZoomViewModel:PropertyChangedBase
{
    private double _offsetX;
    public double OffsetX
    {
        get { return _offsetX; }
        set
        {
            _offsetX = value;
            OnPropertyChanged("OffsetX");
        }
    }

    private double _offsetY;
    public double OffsetY
    {
        get { return _offsetY; }
        set
        {
            _offsetY = value;
            OnPropertyChanged("OffsetY");
        }
    }

    private double _scaleFactor = 1;
    public double ScaleFactor
    {
        get { return _scaleFactor; }
        set
        {
            _scaleFactor = value;
            OnPropertyChanged("ScaleFactor");
            ROIs.ToList().ForEach(x => x.ScaleFactor = value);
        }
    }

    private ObservableCollection<ROI> _rois;
    public ObservableCollection<ROI> ROIs
    {
        get { return _rois ?? (_rois = new ObservableCollection<ROI>()); }
    }
}

ROI ViewModel:

public class ROI:PropertyChangedBase
{
    private Shapes _shape;
    public Shapes Shape
    {
        get { return _shape; }
        set
        {
            _shape = value;
            OnPropertyChanged("Shape");
        }
    }

    private double _scaleFactor;
    public double ScaleFactor
    {
        get { return _scaleFactor; }
        set
        {
            _scaleFactor = value;
            OnPropertyChanged("ScaleFactor");
            OnPropertyChanged("ActualX");
            OnPropertyChanged("ActualY");
            OnPropertyChanged("ActualHeight");
            OnPropertyChanged("ActualWidth");
        }
    }

    private double _x;
    public double X
    {
        get { return _x; }
        set
        {
            _x = value;
            OnPropertyChanged("X");
            OnPropertyChanged("ActualX");
        }
    }

    private double _y;
    public double Y
    {
        get { return _y; }
        set
        {
            _y = value;
            OnPropertyChanged("Y");
            OnPropertyChanged("ActualY");
        }
    }

    private double _height;
    public double Height
    {
        get { return _height; }
        set
        {
            _height = value;
            OnPropertyChanged("Height");
            OnPropertyChanged("ActualHeight");
        }
    }

    private double _width;
    public double Width
    {
        get { return _width; }
        set
        {
            _width = value;
            OnPropertyChanged("Width");
            OnPropertyChanged("ActualWidth");
        }
    }

    public double ActualX { get { return X*ScaleFactor; }}
    public double ActualY { get { return Y*ScaleFactor; }}
    public double ActualWidth { get { return Width*ScaleFactor; }}
    public double ActualHeight { get { return Height * ScaleFactor; } }
}

形状列挙型:

public enum Shapes
{
    Round = 1,
    Square = 2,
    AnyOther
}

PropertyChangedBase(MVVMヘルパークラス):

    public class PropertyChangedBase:INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            Application.Current.Dispatcher.BeginInvoke((Action) (() =>
                                                                     {
                                                                         PropertyChangedEventHandler handler = PropertyChanged;
                                                                         if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
                                                                     }));
        }
    }

レジスト制御:

<UserControl x:Class="MiscSamples.ResizerControl"
             xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.Microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Thumb DragDelta="Center_DragDelta" Height="10" Width="10"
               VerticalAlignment="Center" HorizontalAlignment="Center"/>

        <Thumb DragDelta="UpperLeft_DragDelta" Height="10" Width="10"
               VerticalAlignment="Top" HorizontalAlignment="Left"/>

        <Thumb DragDelta="UpperRight_DragDelta" Height="10" Width="10"
               VerticalAlignment="Top" HorizontalAlignment="Right"/>

        <Thumb DragDelta="LowerLeft_DragDelta" Height="10" Width="10"
               VerticalAlignment="Bottom" HorizontalAlignment="Left"/>

        <Thumb DragDelta="LowerRight_DragDelta" Height="10" Width="10"
               VerticalAlignment="Bottom" HorizontalAlignment="Right"/>

    </Grid>
</UserControl>

コードビハインド:

 public partial class ResizerControl : UserControl
    {
        public static readonly DependencyProperty XProperty = DependencyProperty.Register("X", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d,FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
        public static readonly DependencyProperty YProperty = DependencyProperty.Register("Y", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
        public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
        public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

        public double X
        {
            get { return (double) GetValue(XProperty); }
            set { SetValue(XProperty, value); }
        }

        public double Y
        {
            get { return (double)GetValue(YProperty); }
            set { SetValue(YProperty, value); }
        }

        public double ItemHeight
        {
            get { return (double) GetValue(ItemHeightProperty); }
            set { SetValue(ItemHeightProperty, value); }
        }

        public double ItemWidth
        {
            get { return (double) GetValue(ItemWidthProperty); }
            set { SetValue(ItemWidthProperty, value); }
        }

        public ResizerControl()
        {
            InitializeComponent();
        }

        private void UpperLeft_DragDelta(object sender, DragDeltaEventArgs e)
        {
            X = X + e.HorizontalChange;
            Y = Y + e.VerticalChange;

            ItemHeight = ItemHeight + e.VerticalChange * -1;
            ItemWidth = ItemWidth + e.HorizontalChange * -1;
        }

        private void UpperRight_DragDelta(object sender, DragDeltaEventArgs e)
        {
            Y = Y + e.VerticalChange;

            ItemHeight = ItemHeight + e.VerticalChange * -1;
            ItemWidth = ItemWidth + e.HorizontalChange;
        }

        private void LowerLeft_DragDelta(object sender, DragDeltaEventArgs e)
        {
            X = X + e.HorizontalChange;

            ItemHeight = ItemHeight + e.VerticalChange;
            ItemWidth = ItemWidth + e.HorizontalChange * -1;
        }

        private void LowerRight_DragDelta(object sender, DragDeltaEventArgs e)
        {
            ItemHeight = ItemHeight + e.VerticalChange;
            ItemWidth = ItemWidth + e.HorizontalChange;
        }

        private void Center_DragDelta(object sender, DragDeltaEventArgs e)
        {
            X = X + e.HorizontalChange;
            Y = Y + e.VerticalChange;
        }
    }
20

ストロークの太さを変更せずに形状を変換するには、変換されたジオメトリを持つPathオブジェクトを使用できます。

次のXAMLは、画像と2つのパスをキャンバスに配置します。画像はRenderTransformによって拡大縮小および変換されます。同じ変換が、2つのパスのジオメトリの Transform プロパティにも使用されます。

<Canvas>
    <Image Source="C:\Users\Public\Pictures\Sample Pictures\Desert.jpg">
        <Image.RenderTransform>
            <TransformGroup x:Name="transform">
                <ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
                <TranslateTransform X="100" Y="50"/>
            </TransformGroup>
        </Image.RenderTransform>
    </Image>
    <Path Stroke="Orange" StrokeThickness="2">
        <Path.Data>
            <RectangleGeometry Rect="50,100,100,50"
                               Transform="{Binding ElementName=transform}"/>
        </Path.Data>
    </Path>
    <Path Stroke="Orange" StrokeThickness="2">
        <Path.Data>
            <EllipseGeometry Center="250,100" RadiusX="50" RadiusY="50"
                             Transform="{Binding ElementName=transform}"/>
        </Path.Data>
    </Path>
</Canvas>

これで、アプリケーションは、MouseMoveやMouseWheelなどの入力イベントに応答してtransformオブジェクトを変更するだけで済みます。

スケーリングする必要はなく、適切な場所に移動するだけのTextBlockやその他の要素を変換する場合は、少し注意が必要です。

この種の変換を子要素に適用できる特殊なパネルを作成できます。このようなパネルは、子要素の位置を制御する添付プロパティを定義し、子のRenderTransformまたはLayoutTransformの代わりにこの位置に変換を適用します。

これにより、そのようなパネルをどのように実装できるかがわかります。

public class TransformPanel : Panel
{
    public static readonly DependencyProperty TransformProperty =
        DependencyProperty.Register(
            "Transform", typeof(Transform), typeof(TransformPanel),
            new FrameworkPropertyMetadata(Transform.Identity,
                FrameworkPropertyMetadataOptions.AffectsArrange));

    public static readonly DependencyProperty PositionProperty =
        DependencyProperty.RegisterAttached(
            "Position", typeof(Point?), typeof(TransformPanel),
            new PropertyMetadata(PositionPropertyChanged));

    public Transform Transform
    {
        get { return (Transform)GetValue(TransformProperty); }
        set { SetValue(TransformProperty, value); }
    }

    public static Point? GetPosition(UIElement element)
    {
        return (Point?)element.GetValue(PositionProperty);
    }

    public static void SetPosition(UIElement element, Point? value)
    {
        element.SetValue(PositionProperty, value);
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        var infiniteSize = new Size(double.PositiveInfinity,
                                    double.PositiveInfinity);

        foreach (UIElement element in InternalChildren)
        {
            element.Measure(infiniteSize);
        }

        return new Size();
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (UIElement element in InternalChildren)
        {
            ArrangeElement(element, GetPosition(element));
        }

        return finalSize;
    }

    private void ArrangeElement(UIElement element, Point? position)
    {
        var arrangeRect = new Rect(element.DesiredSize);

        if (position.HasValue && Transform != null)
        {
            arrangeRect.Location = Transform.Transform(position.Value);
        }

        element.Arrange(arrangeRect);
    }

    private static void PositionPropertyChanged(
        DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        var element = (UIElement)obj;
        var panel = VisualTreeHelper.GetParent(element) as TransformPanel;

        if (panel != null)
        {
            panel.ArrangeElement(element, (Point?)e.NewValue);
        }
    }
}

XAMLでは次のように使用されます。

<local:TransformPanel>
    <local:TransformPanel.Transform>
        <TransformGroup>
            <ScaleTransform ScaleX="0.5" ScaleY="0.5" x:Name="scale"/>
            <TranslateTransform X="100"/>
        </TransformGroup>
    </local:TransformPanel.Transform>
    <Image Source="C:\Users\Public\Pictures\Sample Pictures\Desert.jpg"
           RenderTransform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
    <Path Stroke="Orange" StrokeThickness="2">
        <Path.Data>
            <RectangleGeometry Rect="50,100,100,50"
                               Transform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
        </Path.Data>
    </Path>
    <Path Stroke="Orange" StrokeThickness="2">
        <Path.Data>
            <EllipseGeometry Center="250,100" RadiusX="50" RadiusY="50"
                             Transform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
        </Path.Data>
    </Path>
    <TextBlock Text="Rectangle" local:TransformPanel.Position="50,150"/>
    <TextBlock Text="Ellipse" local:TransformPanel.Position="200,150"/>
</local:TransformPanel>
1
Clemens