wpf scroll zoom scrollviewer pan

WPF Image Pan, Zoom y Desplazamiento con capas en un lienzo



scroll scrollviewer (2)

Espero que alguien pueda ayudarme aquí. Estoy construyendo una aplicación de generación de imágenes WPF que toma imágenes en vivo de una cámara que permite a los usuarios ver la imagen, y posteriormente resaltar las regiones de interés (ROI) en esa imagen. La información sobre los ROI (ancho, alto, ubicación relativa a un punto de la imagen, etc.) se envía de vuelta a la cámara, indicando / entrenando el firmware de la cámara donde buscar cosas como códigos de barras, texto, niveles de líquidos, giros en un tornillo, etc. en la imagen). Una característica deseada es la capacidad de desplazarse y acercar o alejar la imagen y sus ROI, así como desplazarse cuando la imagen se amplía más allá del área de visualización. StrokeThickness y FontSize de los ROI necesitan mantener su escala original, pero el ancho y la altura de las formas dentro de un ROI necesitan escalar con la imagen (esto es crítico para capturar las ubicaciones exactas de los píxeles para transmitirlas a la cámara). Tengo todo esto resuelto con la excepción del desplazamiento y algunos otros problemas. Mis dos áreas de preocupación son:

  1. Cuando presento ScrollViewer, no obtengo ningún comportamiento de desplazamiento. Según lo entiendo, necesito presentar un LayoutTransform para obtener el comportamiento correcto de ScrollViewer. Sin embargo, cuando lo hago, otras áreas comienzan a descomponerse (por ejemplo, los ROI no mantienen su posición correcta sobre la imagen, o el puntero del mouse comienza a alejarse del punto seleccionado en la imagen al desplazarse, o la esquina izquierda de mi imagen rebota en la posición actual del mouse en MouseDown.)

  2. No puedo obtener la escala de mi ROI como los necesito. Tengo esto funcionando, pero no es ideal. Lo que tengo no retiene el grosor exacto del trazo, y no he buscado ignorar la escala en los bloques de texto. Espero que veas lo que estoy haciendo en las muestras de código.

Estoy seguro de que mi problema tiene algo que ver con mi falta de comprensión de Transforms y su relación con el sistema de diseño de WPF. Afortunadamente, una versión del código que muestre lo que he logrado hasta ahora ayudará (vea más abajo).

FYI, si Adorners es la sugerencia, eso puede no funcionar en mi escenario porque podría terminar con más adornos de los que se admiten (el rumor de los adornos es cuando las cosas empiezan a descomponerse).

En primer lugar, a continuación se muestra una captura de pantalla que muestra una imagen con los ROI (texto y forma). El rectángulo, la elipse y el texto deben seguir el área de la imagen en escala y rotación, pero no deben escalar en grosor o tamaño de fuente.

Aquí está el XAML que muestra la imagen de arriba, junto con un control deslizante para hacer zoom (el zoom de la rueda vendrá después)

<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>

Aquí está el C # que maneja pan y zoom.

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; } } } }

El código debería funcionar en un proyecto y solución .NET 4.0 o 4.5, suponiendo que no haya errores de corte / pegado.

¿Alguna idea? Sugerencias son bienvenidas.


De acuerdo. Esta es mi opinión sobre lo que describes.

Se parece a esto:

  • Como no estoy aplicando ningún RenderTransforms , obtengo la funcionalidad deseada Scrollbar / ScrollViewer.
  • MVVM, que es EL camino a seguir en WPF. La interfaz de usuario y los datos son independientes, por lo tanto, los elementos de datos solo tienen propiedades double e int para X, Y, ancho, alto, etc. que puede usar para cualquier propósito o incluso almacenarlos en una base de datos.
  • Agregué todo el contenido dentro de un Thumb para manejar el paneo. Todavía tendrá que hacer algo acerca de la panorámica que se produce cuando está arrastrando / cambiando el tamaño de un ROI a través del ResizerControl. Supongo que puedes verificar Mouse.DirectlyOver o algo así.
  • De hecho, utilicé un ListBox para manejar los ROI para que pueda tener 1 ROI seleccionado en un momento dado. Esto alterna la funcionalidad de cambio de tamaño. De modo que si haces clic en un retorno de la inversión, obtendrás el cambio de tamaño visible.
  • El Scaling se maneja en el nivel de ViewModel, eliminando así la necesidad de Panels personalizados o cosas por el estilo (aunque la solución de @Clemens también es agradable)
  • Estoy usando un Enum y algunos DataTriggers para definir las Formas. Ver la parte DataTemplate DataType={x:Type local:ROI} .
  • Rocas WPF. Simplemente copie y pegue mi código en un File -> New Project -> WPF Application y vea los resultados usted mismo.

    <Window x:Class="MiscSamples.PanZoom_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="PanZoom_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>

Código detrás:

public partial class PanZoom_MVVM : Window { public PanZoomViewModel ViewModel { get; set; } public PanZoom_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; } } }

Main ViewModel:

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; } } }

Shapes Enum:

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

PropertyChangedBase (clase MVVM Helper):

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)); })); } }

Control de Resizer

<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>

Código detrás:

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; } }


Para transformar formas sin cambiar su grosor de trazo, puede usar objetos Path con geometrías transformadas.

El siguiente XAML pone una imagen y dos rutas en un lienzo. La imagen se escala y se traduce mediante RenderTransform. La misma transformación también se usa para la propiedad Transform de las geometrías de las dos Rutas.

<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>

Ahora su aplicación puede simplemente cambiar el objeto de transform en respuesta a eventos de entrada como MouseMove o MouseWheel.

Las cosas se vuelven un poco más complicadas cuando se trata de transformar TextBlocks u otros elementos que no deberían escalarse, sino que solo se moverán a una ubicación adecuada.

Puede crear un Panel especializado que pueda aplicar este tipo de transformación a sus elementos secundarios. Tal Panel definiría una propiedad adjunta que controla la posición de un elemento hijo, y aplicaría la transformación a esta posición en lugar de RenderTransform o LayoutTransform del elemento secundario.

Esto puede darle una idea de cómo se podría implementar dicho Panel:

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); } } }

Se usaría en XAML de esta manera:

<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>