首页 文章

WPF图像平移,缩放和在画布上滚动图层

提问于
浏览
12

我希望有人可以帮助我 . 我正在构建一个WPF成像应用程序,它从相机中获取实时图像,允许用户查看图像,然后突出显示该图像上的感兴趣区域(ROI) . 然后将关于ROI的信息(宽度,高度,相对于图像上的点的位置等)发送回相机,实际上告知/训练相机固件在哪里寻找条形码,文本,液位,转弯等内容在图像上的螺丝等) . 所需的功能是能够平移和缩放图像及其ROI,以及在图像缩放比观看区域大时滚动 . ROI的StrokeThickness和FontSize需要保持原始比例,但ROI内形状的宽度和高度需要与图像一起缩放(这对于捕获传输到相机的精确像素位置至关重要) . 除了滚动和其他一些问题之外,我已经解决了大部分问题 . 我关注的两个方面是:

  • 当我介绍ScrollViewer时,我没有得到任何滚动行为 . 据我所知,我需要引入一个LayoutTransform来获得正确的ScrollViewer行为 . 然而,当我这样做时,其他区域开始崩溃(例如,ROI在图像上没有保持正确的位置,或者当平移时鼠标指针开始从图像上的选定点开始蠕动,或者我的图像的左角弹回到MouseDown上的当前鼠标位置 . )

  • 我无法按照我需要的方式调整投资回报率 . 我有这个工作,但它并不理想 . 我所拥有的并没有保留精确的笔划厚度,我没有考虑忽略文本块上的比例 . 希望你能看到我在代码示例中正在做的事情 .

我确定我的问题与我对Transforms及其与WPF布局系统的关系缺乏了解有关 . 希望展示我迄今为止所取得成就的代码将有所帮助(见下文) .

仅供参考,如果Adorners是建议,可能在我的场景中不起作用,因为我最终会得到比支持更多的装饰(谣言144装饰者是事情开始崩溃的时候) .

首先,下面是显示带有ROI(文本和形状)的图像的屏幕截图 . 矩形,椭圆和文本需要按照比例和旋转方式跟随图像上的区域,但不能在厚度或字体大小上进行缩放 .

Screen shot showing sample image with ROIs

Here's the XAML that is showing the above image, along with a Slider for zooming (mousewheel zoom will come later)

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

Here's the C# that manages pan and 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;
        }
    }
}
}

假设没有剪切/粘贴错误,代码应该在.NET 4.0或4.5项目和解决方案中工作 .

有什么想法吗?欢迎提出建议 .

2 回答

  • 1

    好 . 这是我对你描述的内容的看法 .

    它看起来像这样:

    enter image description here

    • 由于我没有应用任何 RenderTransforms ,因此我获得了所需的Scrollbar / ScrollViewer功能 .

    • MVVM,这是WPF的方法 . UI和数据是独立的,因此DataItems只有X,Y,Width,Height等的 doubleint 属性,您可以将它们用于任何目的,甚至将它们存储在数据库中 .

    • 我在 Thumb 中添加了所有内容来处理平移 . 当您通过ResizerControl拖动/调整ROI大小时,您仍需要对平移进行一些操作 . 我想你可以检查 Mouse.DirectlyOver 或其他什么 .

    • 我实际上使用了 ListBox 来处理投资回报率,这样您在任何给定时间都可以获得1个选定的投资回报率 . 这会切换调整大小功能 . 因此,如果您点击ROI,您将看到缩放器可见 .

    • Scaling在ViewModel级别处理,因此无需自定义 Panels 或类似的东西(尽管@Clemens的解决方案也很好)

    • 我正在使用 Enum 和一些 DataTriggers 来定义形状 . 请参阅 DataTemplate DataType={x:Type local:ROI} 部分 .

    • WPF Rocks . 只需将我的代码复制并粘贴到 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 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));
                                                                         }));
            }
        }
    

    调整器控制:

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

    为了在不改变笔触粗细的情况下变换形状,可以使用具有变换几何的 Path 对象 .

    以下XAML在Canvas上放置一个Image和两个Path . 图像由RenderTransform缩放和翻译 . 相同的变换也用于几何的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>
    

    您的应用程序现在可以简单地更改 transform 对象以响应MouseMove或MouseWheel等输入事件 .

    在转换TextBlocks或其他不应缩放的元素时,事情变得有点棘手,但只能移动到正确的位置 .

    您可以创建一个专门的Panel,它能够将这种变换应用于其子元素 . 这样的Panel将定义一个控制子元素位置的附加属性,并将变换应用于此位置而不是子项的 RenderTransformLayoutTransform .

    这可以让您了解如何实施这样一个小组:

    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>
    

相关问题