WPF Image Pan, Zoom and Scroll with layers on a canvas
Asked Answered
A

3

16

I'm hoping someone can help me out here. I'm building a WPF imaging application that takes live images from a camera allowing users to view the image, and subsequently highlight regions of interest (ROI) on that image. Information about the ROIs (width, height, location relative to a point on the image, etc) is then sent back to the camera, in effect telling/training the camera firmware where to look for things like barcodes, text, liquid levels, turns on a screw, etc. on the image). A desired feature is the ability to pan and zoom the image and it's ROIs, as well as scroll when the image is zoomed larger than the viewing area. The StrokeThickness and FontSize of the ROI's need to keep there original scale, but the width and height of the shapes within an ROI need to scale with the image (this is critical to capture exact pixel locations to transmit to the camera). I've got most of this worked out with the exception of scrolling and a few other issues. My two areas of concern are:

  1. When I introduce a ScrollViewer I don't get any scroll behavior. As I understand it I need to introduce a LayoutTransform to get the correct ScrollViewer behavior. However when I do that other areas start to break down (e.g. ROIs don't hold their correct position over the image, or the mouse pointer begins to creep away from the selected point on the image when panning, or the left corner of my image bounces to the current mouse position on MouseDown .)

  2. I can't quite get the scaling of my ROI's the way I need them. I have this working, but it is not ideal. What I have doesn't retain the exact stroke thickness, and I haven't looked into ignoring scale on the textblocks. Hopefully you'll see what I'm doing in the code samples.

I'm sure my issue has something to do with my lack of understanding of Transforms and their relationship to the WPF layout system. Hopefully a rendition of the code that exhibits what I've accomplished so far will help (see below).

FYI, if Adorners are the suggestion, that may not work in my scenario because I could end up with more adorners than are supported (rumor 144 adorners is when things start breaking down).

First off, below is a screenshot showing an image with to ROI's (text and a shape). The rectangle, ellipse and text need to follow the area on the image in scale and rotation, but not they shouldn't scale in thickness or fontsize.

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

The code should work in a .NET 4.0 or 4.5 project and solution, assuming no cut/paste errors.

Any thoughts? Suggestions are welcome.

Adams answered 5/6, 2013 at 1:20 Comment(3)
Applying RenderTransforms only affects rendering. It does not affect Layout, that's why you're not getting any scrollbars.Hellas
Ok, I did a complete refactor of your sample in an MVVM fashion (which is the RIGHT way to do WPF). I'll post it tomorrow. I'm going to sleep right now.Hellas
HighCore, yes I realize the reason I am not seeing scrollbars is because I'm not applying a LayoutTransform. As I stated in my original post, when I introduce that into the mix the pieces that are working (Pan, Zoom, ROI positioning) start to exhibit strange behavior. I've tried putting both my Scale and Translate transforms into a LayoutTransform, and I've tried separating the Scale into a RenderTransform and the Translate into a LayoutTransform. Each time things start to break down pretty quickly. I'm interested in a code sample if you ideas.Adams
H
25

Ok. This is my take on what you described.

It looks like this:

enter image description here

  • Since I'm not applying any RenderTransforms, I get the desired Scrollbar / ScrollViewer functionality.
  • MVVM, which is THE way to go in WPF. UI and data are independent thus the DataItems only have double and int properties for X,Y, Width,Height, etc that you can use for whatever purposes or even store them in a Database.
  • I added the whole stuff inside a Thumb to handle the panning. You will still need to do something about the Panning that occurs when you are dragging / resizing a ROI via the ResizerControl. I guess you can check for Mouse.DirectlyOver or something.
  • I actually used a ListBox to handle the ROIs so that you may have 1 selected ROI at any given time. This toggles the Resizing Functionality. So that if you click on a ROI, you will get the resizer visible.
  • The Scaling is handled at the ViewModel level, thus eliminating the need for custom Panels or stuff like that (though @Clemens' solution is nice as well)
  • I'm using an Enum and some DataTriggers to define the Shapes. See the DataTemplate DataType={x:Type local:ROI} part.
  • WPF Rocks. Just Copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself.
<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>
</Window>

Code Behind:

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

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 (MVVM Helper class):

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

Resizer Control:

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

Code Behind:

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;
    }
}
Hellas answered 5/6, 2013 at 18:17 Comment(17)
This looks interesting, I'm going to give it a try. Just an FYI, the middle image showing Homer's eye with an ROI over it. When I zoom, pan, or rotate the image I want that rectangle to remain over his eye. Think of a quality control situation where the camera is snapping pictures of Homer posters going by on a conveyor. The camera will examine the pixels within that ROI ensuring that the eye is drawn in correct size, color, etc. If anomalies are detected the UI gets signaled so we can alert someone to reject that poster, or to shut down the line and correct the problem.Adams
Oh, and yes we intend to use MVVM. We're in early proof of concept stages right now. As for MVVM being the only way to do WPF, you might stir up a religious discussion with some folks with that statement. I agree it's an awesome pattern for WPF.Adams
FYI, It looks like your ROI template/style example (the first XAML snippet) isn't complete. Looks like a cut/paste error at the beginning of the XAML.Adams
@kylelib Corrected. SO was not taking that markup as Code and didn't show it properly.Hellas
@kylelib regarding the first comment about the ROIs staying in their place. Check my example that's exactly what it does, even when you zoom / pan. Though I did not implement any rotation capabilities.Hellas
I'm good with rotation, move and resize of the ROIs. I will try your sample in a day or so. I am off on other things at the moment. I suspect part of the problem with my original posted code is the size of my panel contained within the ScrollViewer and how I have things arranged within the contained panel. As stated previously, I'm just not comfortable with all of the capabilities of the WPF layout system. I try to avoid absolute sizing whenever possible, but even then I get myself bound up on occasion. I will report back with my findings eventually. Thanks again for your help!!!!Adams
Finally, I was able to spend a few minutes taking a close look at the code in your answer. This works very nicely...Thanks!!! The last thing I'm struggling with is the origin of everything with respect to zoom (both in my original code and in yours). For example, consider the ellipse in my original screen shot. The very first time I zoom in I want that ellipse to be remain in the center of the zoomed area. When I pan around and zoom again, I want the visible center of the image to be my zoom origin. Is this possible?Adams
@Adams sure, you will have to play with the ScrollViewer offset as you zoom, maybe doing some proportional calculations taking the X and Y of the desired center into account?Hellas
HighCore, I've implemented some of your ideas with success. Though I am running into an issue when selecting and moving a shape (ROI) on the initial mouse down due to the fact that each shape lives in a ListBoxItem. When I hold the mouse down and drag a shape, that shape does not move and the selection moves to any shape that I happen to cross over while dragging the mouse. Perhaps you or @Clemens can shed some light here. Thanks.Adams
@Adams You need to play around with the IsHitTestVisible property of several UI elements there. But I think that deserves a separate question, don't you? Maybe you can create a new question specifically about that, and then send me the link =)Hellas
Been playing around with IsHitTestVisible, but with limited success. Let me see if I can come up with some sample code to illustrate the issue and I'll post as another question. I'll keep you posted.Adams
Regarding zoom and resizing the shapes, if I zoom in and out a few times, then resize a shape the ActualHeight property starts throwing binding errors stating that the ActualHeight on ROI view model is invalid for the target property and is showing large negative values for ActualHeight. Subsequently the ROI disappears from view. I haven't looked into why just yet, but was wondering if you had any ideas.Adams
@Adams force the ViewModels' properties to always have valid values. That is, put an value > 0 validation in the settersHellas
Hmm, that gets rid of the binding errors, but the shape being moved or resized still disappears because the coordinates are growing to astronomical sizes when I move the mouse. This happens when I zoom way in on the image and then move or resize a shape (roi). Appears to have something to do with the ScaleFactor, but it's not yet obvious to me why. Thanks for sticking with me on this.Adams
@Adams if you find the error, let me know, so I can also fix it in my sample project =)Hellas
I was able to overcome the scale issues by factoring the ScaleFactor out of the Horizontal and Vertical change for all move and resize events. Essentially I added a ScaleFactor DP to ResizerControl and then added the following code: e.HorizontalChange / (ScaleFactor == 0 ? 1 : ScaleFactor) and e.VerticalChange / (ScaleFactor == 0 ? 1 : ScaleFactor) everywhere they were used.Adams
the </window> closing tag does not show up in the post although it's in the post's source. I'm not sure why it's not showing up.Pentha
W
2

In order to transform shapes without changing their stroke thickness, you may use Path objects with transformed geometries.

The following XAML puts an Image and two Paths on a Canvas. The Image is scaled and translated by a RenderTransform. The same transform is also used for the Transform property of the geometries of the two Paths.

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

Your application may now simply change the transform object in response to input events like MouseMove or MouseWheel.

Things get a little bit trickier when it comes to also transforming TextBlocks or other element that should not be scaled, but only be moved to a proper location.

You may create a specialized Panel which is able to apply this kind of transform to its child elements. Such a Panel would define an attached property that controls the position of a child element, and would apply the transform to this position instead of the RenderTransform or LayoutTransform of the child.

This may give you an idea of how such a Panel could be implemented:

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

It would be used in XAML like this:

<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>
Willemstad answered 5/6, 2013 at 11:15 Comment(8)
Clemens, thanks for the ideas. I tried your Path suggestions and was able to see the correct behavior. Though ultimately I need the shapes to be part of a custom control with properties, methods, events and other controls, plus adorners and thumbs for resize, move and rotate. Once I start wrapping my Path elements in some Panel the strokes will scale again. I'm not sure if implementing a custom Path is the correct way to go.Adams
Your Panel must not scale the Path object by either RenderTransform or LayoutTransform, or by settings like HorizontalAlignment or VerticalAlignment. The transform must only be applied to the Geometry.Transform property.Willemstad
Clemens, Are you saying that I can be selective about which transform to apply to which elements? Is there a way to selectively apply, or for that matter, ignore a transform. I need all layers to act as if they are one image with the exception that Path thickness and Text do not scale, plus I need to be able to move, resize, rotate, select each Path over the image. I have all of this working today. It's the scrollviewer and stroke thickness eluding me. I'll try your custom Panel idea to see if something stands out and report back.Adams
Yes of course. When you look at my example XAML with the local:TransformPanel you'll see that each Path's geometry transform is bound to the Transform property of the Panel. That transform is not applied to the Panel itself, but only to selected children.Willemstad
Yes, I did see that in your TransformPanel example, and the resize behavior on the geometries is exactly what I'm looking for. Now I need to figure out how to get them to stick with the pixels that they surround on the image when zoom and pan occur. In the screenshot above, if I pan or zoom the image I need the rectangle to follow and size with the word "Appointment" as if it were part of the image, yet still be able to manipulate the rectangle independent of the image. The ultimate goal is to tell the camera where to look for the word "Appointment". BTW, Thanks for sticking with me on this.Adams
I guess what I'm trying to say is that my ROIs (rectangle/ellipse/ geometry, text and adornments, etc.) need to be placed over the image with absolute coordinates and retain their location and size (with the exceptions I noted) during transformation. I need to be able to interrogate the ROI to get it's location and boundaries so that the camera can inspect the image and look for anomalies in the area denoted by the ROI. In your Path example (which might be useful) I'm having trouble positioning the Path's were they need to be and retain the behavior that your local:TransformPanel provides.Adams
@kylelib That's exactly what I did in my example. Please give me 15 minutes and I'll post it here. I'm too busy right now =(Hellas
Sweet @HighCore, I'll take a look.Adams
R
1

Well this answer doesn't really help OP with his more specified issue but in general, camera panning, zooming in and out, and looking around (using the mouse) is quite difficult and so I just wanted to give some insight on how I implemented camera movement into my viewport scene (like Blender or Unity etc.)

This is the class called CameraPan, which contains some variables you can customize to edit the zooming in and out distance/speed, pan speed and camera look sensitivity. At the bottom of the class there is some hashed out code that represents the basic implementation into any scene. You first need to create a viewport and assign it to a 'Border' (which is a UI element that can handle mouse events since Viewport can't) and also create a camera alongside a couple of other public variables that are accessed from the CameraPan Class:

public partial class CameraPan
{
    Point TemporaryMousePosition;
    Point3D PreviousCameraPosition;

    Quaternion QuatX;
    Quaternion PreviousQuatX;
    Quaternion QuatY;
    Quaternion PreviousQuatY;

    private readonly float PanSpeed = 4f;
    private readonly float LookSensitivity = 100f;
    private readonly float ZoomInOutDistance = 1f;
    
    private readonly MainWindow mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();

    public Vector3D LookDirection(PerspectiveCamera camera, Point3D pointToLookAt) // Calculates vector direction between two points (LookAt() method)
    {
        Point3D CameraPosition = camera.Position;

        Vector3D VectorDirection = new Vector3D
            (pointToLookAt.X - CameraPosition.X,
            pointToLookAt.Y - CameraPosition.Y,
            pointToLookAt.Z - CameraPosition.Z);

        return VectorDirection;
    }

    public void PanLookAroundViewport_MouseMove(object sender, MouseEventArgs e) // Panning the viewport using the camera
    {
        if (e.MiddleButton == MouseButtonState.Pressed)
        {               
            Point mousePos = e.GetPosition(sender as Border); // Gets the current mouse pos
            Point3D newCamPos = new Point3D(
                ((-mousePos.X + TemporaryMousePosition.X) / mainWindow.Width * PanSpeed) + PreviousCameraPosition.X,
                ((mousePos.Y - TemporaryMousePosition.Y) / mainWindow.Height * PanSpeed) + PreviousCameraPosition.Y,
                mainWindow.MainCamera.Position.Z); // Calculates the proportional distance to move the camera, 
                                                  //can be increased by changing the variable 'PanSpeed'

            if (Keyboard.IsKeyDown(Key.LeftCtrl)) // Pan viewport
            {                
                mainWindow.MainCamera.Position = newCamPos;
            }
            else // Look around viewport
            {
                double RotY = (e.GetPosition(sender as Label).X - TemporaryMousePosition.X) / mainWindow.Width * LookSensitivity; // MousePosX is the Y axis of a rotation
                double RotX = (e.GetPosition(sender as Label).Y - TemporaryMousePosition.Y) / mainWindow.Height * LookSensitivity; // MousePosY is the X axis of a rotation

                QuatX = Quaternion.Multiply(new Quaternion(new Vector3D(1, 0, 0), -RotX), PreviousQuatX);
                QuatY = Quaternion.Multiply(new Quaternion(new Vector3D(0, 1, 0), -RotY), PreviousQuatY);
                Quaternion QuaternionRotation = Quaternion.Multiply(QuatX, QuatY); // Composite Quaternion between the x rotation and the y rotation
                mainWindow.camRotateTransform.Rotation = new QuaternionRotation3D(QuaternionRotation); // MainCamera.Transform = RotateTransform3D 'camRotateTransform'
            }
        }
    }

    public void MiddleMouseButton_MouseDown(object sender, MouseEventArgs e) // Declares some constants when mouse button 3 is first held down
    {
        if (e.MiddleButton == MouseButtonState.Pressed)
        {
            var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
            TemporaryMousePosition = e.GetPosition(sender as Label);
            PreviousCameraPosition = mainWindow.MainCamera.Position;
            PreviousQuatX = QuatX;
            PreviousQuatY = QuatY;
        }
    }

    public void MouseUp(object sender, MouseEventArgs e)
    {
        mainWindow.CameraCenter = new Point3D(
            mainWindow.CameraCenter.X + mainWindow.MainCamera.Position.X - mainWindow.OriginalCamPosition.X,
            mainWindow.CameraCenter.Y + mainWindow.MainCamera.Position.Y - mainWindow.OriginalCamPosition.Y,
            mainWindow.CameraCenter.Z + mainWindow.MainCamera.Position.Z - mainWindow.OriginalCamPosition.Z);
        // Sets the center of rotation of cam to current mouse position
    } // Declares some constants when mouse button 3 is first let go

    public void ZoomInOutViewport_MouseScroll(object sender, MouseWheelEventArgs e)
    {
        var cam = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault().MainCamera;

        if (e.Delta > 0) // Wheel scrolled forwards - Zoom In
        {
            cam.Position = new Point3D(cam.Position.X, cam.Position.Y, cam.Position.Z - ZoomInOutDistance);
        }
        else // Wheel scrolled forwards - Zoom Out
        {
            cam.Position = new Point3D(cam.Position.X, cam.Position.Y, cam.Position.Z + ZoomInOutDistance);
        }
    }

    // -----CODE IN 'public MainWindow()' STRUCT-----
    /*
        public PerspectiveCamera MainCamera = new PerspectiveCamera();
        public AxisAngleRotation3D MainCamAngle;
        public RotateTransform3D camRotateTransform;
        public Point3D CameraCenter = new Point3D(0, 0, 0);
        public Point3D OriginalCamPosition;

        public MainWindow()
        {
            Viewport3D Viewport = new Viewport3D();
            CameraPan cameraPan = new CameraPan(); // Initialises CameraPan class

            MainCamera.Position = new Point3D(0, 2, 10);
            MainCamera.FieldOfView = 60;
            MainCamera.LookDirection = cameraPan.LookDirection(MainCamera, new Point3D(0, 0, 0));
            // Some custom camera settings

            OriginalCamPosition = MainCamera.Position;
            // Saves the MainCamera's first position

            camRotateTransform = new RotateTransform3D() // Rotation of camera
            {
                CenterX = CameraCenter.X,
                CenterY = CameraCenter.Y,
                CenterZ = CameraCenter.Z,            
            };
            MainCamAngle = new AxisAngleRotation3D() // Rotation value of camRotateTransform
            {
                Axis = new Vector3D(1, 0, 0),
                Angle = 0
            };
            camRotateTransform.Rotation = MainCamAngle;
            MainCamera.Transform = camRotateTransform;

            Border viewportHitBG = new Border() { Width = Width, Height = Height, Background = new SolidColorBrush(Colors.White) };
            // UI Element to detect mouse click events

            viewportHitBG.MouseMove += cameraPan.PanLookAroundViewport_MouseMove;
            viewportHitBG.MouseDown += cameraPan.MiddleMouseButton_MouseDown;
            viewportHitBG.MouseWheel += cameraPan.ZoomInOutViewport_MouseScroll;
            viewportHitBG.MouseUp += cameraPan.MouseUp;
            // Mouse Event handlers

            // Assign the camera to the viewport
            Viewport.Camera = MainCamera;
            
            // Assign Viewport as the child of the UI Element that detects mouse events
            viewportHitBG.Child = Viewport;
        }
     */
}

The mouse event handlers run the specified camera pan functions depending on mouse and key events. The setup is similar to Unity Viewport controls (middle mouse to look around, middle mouse + CTRL to pan around, scroll wheel to zoom).

Here is my full implementation of the camera pan if you want it. It includes a scene that draws a red cube and lets you pan around the scene using the camera:

public partial class MainWindow : Window
{      
    private readonly TranslateTransform3D Position;
    private readonly RotateTransform3D Rotation;
    private readonly AxisAngleRotation3D Transform_Rotation;
    private readonly ScaleTransform3D Scale;

    public PerspectiveCamera MainCamera = new PerspectiveCamera();
    public AxisAngleRotation3D MainCamAngle;
    public RotateTransform3D camRotateTransform;
    public Point3D CameraCenter = new Point3D(0, 0, 0);
    public Point3D OriginalCamPosition;

    public MainWindow()
    {
        InitializeComponent();

        Height = SystemParameters.PrimaryScreenHeight;
        Width = SystemParameters.PrimaryScreenWidth;
        WindowState = WindowState.Maximized;

        #region Initialising 3D Scene Objects

        // Declare scene objects.
        Viewport3D Viewport = new Viewport3D();
        Model3DGroup ModelGroup = new Model3DGroup();
        GeometryModel3D Cube = new GeometryModel3D();
        ModelVisual3D CubeModel = new ModelVisual3D();

        #endregion

        #region UI Grid Objects

        Grid grid = new Grid();

        Slider AngleSlider = new Slider()
        {
            Height = 50,
            VerticalAlignment = VerticalAlignment.Top,
        };
        AngleSlider.ValueChanged += AngleSlider_MouseMove;

        grid.Children.Add(AngleSlider);

        #endregion

        #region Camera Stuff
       
        CameraPan cameraPan = new CameraPan();

        MainCamera.Position = new Point3D(0, 2, 10);
        MainCamera.FieldOfView = 60;
        MainCamera.LookDirection = cameraPan.LookDirection(MainCamera, new Point3D(0, 0, 0));
        OriginalCamPosition = MainCamera.Position;

        camRotateTransform = new RotateTransform3D()
        {
            CenterX = CameraCenter.X,
            CenterY = CameraCenter.Y,
            CenterZ = CameraCenter.Z,            
        };
        MainCamAngle = new AxisAngleRotation3D()
        {
            Axis = new Vector3D(1, 0, 0),
            Angle = 0
        };
        camRotateTransform.Rotation = MainCamAngle;
        MainCamera.Transform = camRotateTransform;

        Border viewportHitBG = new Border() { Width = Width, Height = Height, Background = new SolidColorBrush(Colors.White) };

        viewportHitBG.MouseMove += cameraPan.PanLookAroundViewport_MouseMove;
        viewportHitBG.MouseDown += cameraPan.MiddleMouseButton_MouseDown;
        viewportHitBG.MouseWheel += cameraPan.ZoomInOutViewport_MouseScroll;
        viewportHitBG.MouseUp += cameraPan.MouseUp;

        // Asign the camera to the viewport
        Viewport.Camera = MainCamera;

        #endregion

        #region Directional Lighting

        // Define the lights cast in the scene. Without light, the 3D object cannot
        // be seen. Note: to illuminate an object from additional directions, create
        // additional lights.

        AmbientLight ambientLight = new AmbientLight
        {
            Color = Colors.WhiteSmoke,
        };

        ModelGroup.Children.Add(ambientLight);

        #endregion

        #region Mesh Of Object

        Vector3DCollection Normals = new Vector3DCollection
        {
            new Vector3D(0, 0, 1),
            new Vector3D(0, 0, 1),
            new Vector3D(0, 0, 1),
            new Vector3D(0, 0, 1),
            new Vector3D(0, 0, 1),
            new Vector3D(0, 0, 1)
        };

        PointCollection TextureCoordinates = new PointCollection
        {
            new Point(0, 0),
            new Point(1, 0),
            new Point(1, 1),
            new Point(0, 1),
        };

        Point3DCollection Positions = new Point3DCollection
        {
            new Point3D(-0.5, -0.5, 0.5), // BL FRONT 0
            new Point3D(0.5, -0.5, 0.5), // BR FRONT 1
            new Point3D(0.5, 0.5, 0.5), // TR FRONT 2
            new Point3D(-0.5, 0.5, 0.5), // TL FRONT 3
            new Point3D(-0.5, -0.5, -0.5), // BL BACK 4
            new Point3D(0.5, -0.5, -0.5), // BR BACK 5
            new Point3D(0.5, 0.5, -0.5), // TR BACK 6
            new Point3D(-0.5, 0.5, -0.5) // TL BACK 7
        };

        MeshGeometry3D Faces = new MeshGeometry3D()
        {
            Normals = Normals,
            Positions = Positions,
            TextureCoordinates = TextureCoordinates,
            TriangleIndices = new Int32Collection
            {
                0, 1, 2, 2, 3, 0,
                6, 5, 4, 4, 7, 6,
                4, 0, 3, 3, 7, 4,
                2, 1, 5, 5, 6, 2,
                7, 3, 2, 2, 6, 7,
                1, 0, 4, 4, 5, 1
            },
        };

        // Apply the mesh to the geometry model.
        Cube.Geometry = Faces;

        #endregion

        #region Material Of Object

        // The material specifies the material applied to the 3D object.

        // Define material and apply to the mesh geometries.
        Material myMaterial = new DiffuseMaterial(new SolidColorBrush(Color.FromScRgb(255, 255, 0, 0)));
        Cube.Material = myMaterial;

        #endregion

        #region Transform Of Object

        // Apply a transform to the object. In this sample, a rotation transform is applied, rendering the 3D object rotated.
        Transform_Rotation = new AxisAngleRotation3D()
        {
            Angle = 0,
            Axis = new Vector3D(0, 0, 0)
        };
        Position = new TranslateTransform3D
        {
            OffsetX = 0,
            OffsetY = 0,
            OffsetZ = 0
        };
        Scale = new ScaleTransform3D
        {
            ScaleX = 1,
            ScaleY = 1,
            ScaleZ = 1
        };

        Rotation = new RotateTransform3D
        {
            Rotation = Transform_Rotation
        };

        Transform3DGroup transformGroup = new Transform3DGroup();
        transformGroup.Children.Add(Rotation);
        transformGroup.Children.Add(Scale);
        transformGroup.Children.Add(Position);

        Cube.Transform = transformGroup;

        #endregion

        #region Adding Children To Groups And Parents

        // Add the geometry model to the model group.
        ModelGroup.Children.Add(Cube);
        CubeModel.Content = ModelGroup;
        Viewport.Children.Add(CubeModel);
        viewportHitBG.Child = Viewport;
        grid.Children.Add(viewportHitBG);

        #endregion

        Content = grid;
    }

    private void AngleSlider_MouseMove(object sender, RoutedEventArgs e)
    {
        Slider slider = (Slider)sender;
        Transform_Rotation.Angle = slider.Value * 36;
        Transform_Rotation.Axis = new Vector3D(0, 1, 0);
        Scale.ScaleX = slider.Value / 5; Scale.ScaleY = slider.Value / 5; Scale.ScaleZ = slider.Value / 5;
        Position.OffsetX = slider.Value / 5; Position.OffsetY = slider.Value / 5; Position.OffsetZ = slider.Value / 5;
    }
}

public partial class CameraPan
{
    Point TemporaryMousePosition;
    Point3D PreviousCameraPosition;

    Quaternion QuatX;
    Quaternion PreviousQuatX;
    Quaternion QuatY;
    Quaternion PreviousQuatY;

    private readonly float PanSpeed = 4f;
    private readonly float LookSensitivity = 100f;
    private readonly float ZoomInOutDistance = 1f;
    
    private readonly MainWindow mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();

    public Vector3D LookDirection(PerspectiveCamera camera, Point3D pointToLookAt) // Calculates vector direction between two points (LookAt() method)
    {
        Point3D CameraPosition = camera.Position;

        Vector3D VectorDirection = new Vector3D
            (pointToLookAt.X - CameraPosition.X,
            pointToLookAt.Y - CameraPosition.Y,
            pointToLookAt.Z - CameraPosition.Z);

        return VectorDirection;
    }

    public void PanLookAroundViewport_MouseMove(object sender, MouseEventArgs e) // Panning the viewport using the camera
    {
        if (e.MiddleButton == MouseButtonState.Pressed)
        {               
            Point mousePos = e.GetPosition(sender as Border); // Gets the current mouse pos
            Point3D newCamPos = new Point3D(
                ((-mousePos.X + TemporaryMousePosition.X) / mainWindow.Width * PanSpeed) + PreviousCameraPosition.X,
                ((mousePos.Y - TemporaryMousePosition.Y) / mainWindow.Height * PanSpeed) + PreviousCameraPosition.Y,
                mainWindow.MainCamera.Position.Z); // Calculates the proportional distance to move the camera, 
                                                  //can be increased by changing the variable 'PanSpeed'

            if (Keyboard.IsKeyDown(Key.LeftCtrl)) // Pan viewport
            {                
                mainWindow.MainCamera.Position = newCamPos;
            }
            else // Look around viewport
            {
                double RotY = (e.GetPosition(sender as Label).X - TemporaryMousePosition.X) / mainWindow.Width * LookSensitivity; // MousePosX is the Y axis of a rotation
                double RotX = (e.GetPosition(sender as Label).Y - TemporaryMousePosition.Y) / mainWindow.Height * LookSensitivity; // MousePosY is the X axis of a rotation

                QuatX = Quaternion.Multiply(new Quaternion(new Vector3D(1, 0, 0), -RotX), PreviousQuatX);
                QuatY = Quaternion.Multiply(new Quaternion(new Vector3D(0, 1, 0), -RotY), PreviousQuatY);
                Quaternion QuaternionRotation = Quaternion.Multiply(QuatX, QuatY); // Composite Quaternion between the x rotation and the y rotation
                mainWindow.camRotateTransform.Rotation = new QuaternionRotation3D(QuaternionRotation); // MainCamera.Transform = RotateTransform3D 'camRotateTransform'
            }
        }
    }

    public void MiddleMouseButton_MouseDown(object sender, MouseEventArgs e) // Declares some constants when mouse button 3 is first held down
    {
        if (e.MiddleButton == MouseButtonState.Pressed)
        {
            var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
            TemporaryMousePosition = e.GetPosition(sender as Label);
            PreviousCameraPosition = mainWindow.MainCamera.Position;
            PreviousQuatX = QuatX;
            PreviousQuatY = QuatY;
        }
    }

    public void MouseUp(object sender, MouseEventArgs e)
    {
        mainWindow.CameraCenter = new Point3D(
            mainWindow.CameraCenter.X + mainWindow.MainCamera.Position.X - mainWindow.OriginalCamPosition.X,
            mainWindow.CameraCenter.Y + mainWindow.MainCamera.Position.Y - mainWindow.OriginalCamPosition.Y,
            mainWindow.CameraCenter.Z + mainWindow.MainCamera.Position.Z - mainWindow.OriginalCamPosition.Z);
        // Sets the center of rotation of cam to current mouse position
    } // Declares some constants when mouse button 3 is first let go

    public void ZoomInOutViewport_MouseScroll(object sender, MouseWheelEventArgs e)
    {
        var cam = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault().MainCamera;

        if (e.Delta > 0) // Wheel scrolled forwards - Zoom In
        {
            cam.Position = new Point3D(cam.Position.X, cam.Position.Y, cam.Position.Z - ZoomInOutDistance);
        }
        else // Wheel scrolled forwards - Zoom Out
        {
            cam.Position = new Point3D(cam.Position.X, cam.Position.Y, cam.Position.Z + ZoomInOutDistance);
        }
    }

    // -----CODE IN 'public MainWindow()' STRUCT-----
    /*
        public PerspectiveCamera MainCamera = new PerspectiveCamera();
        public AxisAngleRotation3D MainCamAngle;
        public RotateTransform3D camRotateTransform;
        public Point3D CameraCenter = new Point3D(0, 0, 0);
        public Point3D OriginalCamPosition;

        public MainWindow()
        {
            Viewport3D Viewport = new Viewport3D();
            CameraPan cameraPan = new CameraPan(); // Initialises CameraPan class

            MainCamera.Position = new Point3D(0, 2, 10);
            MainCamera.FieldOfView = 60;
            MainCamera.LookDirection = cameraPan.LookDirection(MainCamera, new Point3D(0, 0, 0));
            // Some custom camera settings

            OriginalCamPosition = MainCamera.Position;
            // Saves the MainCamera's first position

            camRotateTransform = new RotateTransform3D() // Rotation of camera
            {
                CenterX = CameraCenter.X,
                CenterY = CameraCenter.Y,
                CenterZ = CameraCenter.Z,            
            };
            MainCamAngle = new AxisAngleRotation3D() // Rotation value of camRotateTransform
            {
                Axis = new Vector3D(1, 0, 0),
                Angle = 0
            };
            camRotateTransform.Rotation = MainCamAngle;
            MainCamera.Transform = camRotateTransform;

            Border viewportHitBG = new Border() { Width = Width, Height = Height, Background = new SolidColorBrush(Colors.White) };
            // UI Element to detect mouse click events

            viewportHitBG.MouseMove += cameraPan.PanLookAroundViewport_MouseMove;
            viewportHitBG.MouseDown += cameraPan.MiddleMouseButton_MouseDown;
            viewportHitBG.MouseWheel += cameraPan.ZoomInOutViewport_MouseScroll;
            viewportHitBG.MouseUp += cameraPan.MouseUp;
            // Mouse Event handlers

            // Assign the camera to the viewport
            Viewport.Camera = MainCamera;
            
            // Assign Viewport as the child of the UI Element that detects mouse events
            viewportHitBG.Child = Viewport;
        }
     */
}

I hope it helps someone in the future!

Rube answered 17/10, 2020 at 7:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.