WPF 3D - How can I save and load a Camera view? - wpf

I have a WPF 3D scene where I can pan, rotate and zoom using the TrackballDecorator from the 3DTools library. I would like to save the camera settings (transformation) and be able to re-apply them when the application restarts the next time (so the view is restored).
I tried to save each individual value of the Camera:
private void SaveCameraSettings()
{
var d = Properties.Settings.Default;
d.CameraPositionX = camera.Position.X;
d.CameraPositionY = camera.Position.Y;
...
d.Save();
}
This doesn't work, I guess because those settings are not updated according to the transformations applied to the camera (I always get the initial values set in xaml).
I checked the the Transformation3D class but couldn't find any way to set its value...
The problem is what values do I need to get from the PerspectiveCamera in order to be able to restore it the way it was when I closed my application the last time. The camera is set to a default position (in Xaml), then a transformation is applied to this camera by the TrackBallDecorator. How can I save this transformation (what values to store)? And how can I re-apply them at a later time?

This is going to be a bit long, so bear with me...
1st, you need to modify the 3DTools library so you can apply a transformation to the TrackballDecorator as follow:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Input;
namespace _3DTools
{
public class TrackballDecorator : Viewport3DDecorator
{
#region Private Members
private Point m_PreviousPosition2D;
private Vector3D m_PreviousPosition3D = new Vector3D(0, 0, 1);
private Transform3DGroup m_Transform;
private ScaleTransform3D m_Scale = new ScaleTransform3D();
private AxisAngleRotation3D m_Rotation = new AxisAngleRotation3D();
private TranslateTransform3D m_Translate = new TranslateTransform3D();
private readonly Border m_EventSource;
#endregion
#region Constructor
public TrackballDecorator()
{
TranslateScale = 10;
ZoomScale = 1;
RotateScale = 1;
// the transform that will be applied to the viewport 3d's camera
m_Transform = new Transform3DGroup();
m_Transform.Children.Add(m_Scale);
m_Transform.Children.Add(new RotateTransform3D(m_Rotation));
m_Transform.Children.Add(m_Translate);
// used so that we always get events while activity occurs within
// the viewport3D
m_EventSource = new Border { Background = Brushes.Transparent };
PreViewportChildren.Add(m_EventSource);
}
#endregion
#region Properties
/// <summary>
/// A transform to move the camera or scene to the trackball's
/// current orientation and scale.
/// </summary>
public Transform3DGroup Transform
{
get { return m_Transform; }
set
{
m_Transform = value;
m_Scale = m_Transform.GetScaleTransform3D();
m_Translate = m_Transform.GetTranslateTransform3D();
m_Rotation = m_Transform.GetRotateTransform3D().Rotation as AxisAngleRotation3D;
ApplyTransform();
}
}
public double TranslateScale { get; set; }
public double RotateScale { get; set; }
public double ZoomScale { get; set; }
#endregion
#region Event Handling
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
m_PreviousPosition2D = e.GetPosition(this);
m_PreviousPosition3D = ProjectToTrackball(ActualWidth,
ActualHeight,
m_PreviousPosition2D);
if (Mouse.Captured == null)
{
Mouse.Capture(this, CaptureMode.Element);
}
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
if (IsMouseCaptured)
{
Mouse.Capture(this, CaptureMode.None);
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (IsMouseCaptured)
{
Point currentPosition = e.GetPosition(this);
// avoid any zero axis conditions
if (currentPosition == m_PreviousPosition2D) return;
// Prefer tracking to zooming if both buttons are pressed.
if (e.LeftButton == MouseButtonState.Pressed)
{
Track(currentPosition);
}
else if (e.RightButton == MouseButtonState.Pressed)
{
Zoom(currentPosition);
}
else if (e.MiddleButton == MouseButtonState.Pressed)
{
Translate(currentPosition);
}
m_PreviousPosition2D = currentPosition;
ApplyTransform();
}
}
private void ApplyTransform()
{
Viewport3D viewport3D = Viewport3D;
if (viewport3D != null)
{
if (viewport3D.Camera != null)
{
if (viewport3D.Camera.IsFrozen)
{
viewport3D.Camera = viewport3D.Camera.Clone();
}
if (viewport3D.Camera.Transform != m_Transform)
{
viewport3D.Camera.Transform = m_Transform;
}
}
}
}
#endregion Event Handling
private void Track(Point currentPosition)
{
var currentPosition3D = ProjectToTrackball(ActualWidth, ActualHeight, currentPosition);
var axis = Vector3D.CrossProduct(m_PreviousPosition3D, currentPosition3D);
var angle = Vector3D.AngleBetween(m_PreviousPosition3D, currentPosition3D);
// quaterion will throw if this happens - sometimes we can get 3D positions that
// are very similar, so we avoid the throw by doing this check and just ignoring
// the event
if (axis.Length == 0) return;
var delta = new Quaternion(axis, -angle);
// Get the current orientantion from the RotateTransform3D
var r = m_Rotation;
var q = new Quaternion(m_Rotation.Axis, m_Rotation.Angle);
// Compose the delta with the previous orientation
q *= delta;
// Write the new orientation back to the Rotation3D
m_Rotation.Axis = q.Axis;
m_Rotation.Angle = q.Angle;
m_PreviousPosition3D = currentPosition3D;
}
private static Vector3D ProjectToTrackball(double width, double height, Point point)
{
var x = point.X / (width / 2); // Scale so bounds map to [0,0] - [2,2]
var y = point.Y / (height / 2);
x = x - 1; // Translate 0,0 to the center
y = 1 - y; // Flip so +Y is up instead of down
var z2 = 1 - x * x - y * y; // z^2 = 1 - x^2 - y^2
var z = z2 > 0 ? Math.Sqrt(z2) : 0;
return new Vector3D(x, y, z);
}
private void Zoom(Point currentPosition)
{
var yDelta = currentPosition.Y - m_PreviousPosition2D.Y;
var scale = Math.Exp(yDelta / 100) / ZoomScale; // e^(yDelta/100) is fairly arbitrary.
m_Scale.ScaleX *= scale;
m_Scale.ScaleY *= scale;
m_Scale.ScaleZ *= scale;
}
private void Translate(Point currentPosition)
{
// Calculate the panning vector from screen(the vector component of the Quaternion
// the division of the X and Y components scales the vector to the mouse movement
var qV = new Quaternion(((m_PreviousPosition2D.X - currentPosition.X) / TranslateScale),
((currentPosition.Y - m_PreviousPosition2D.Y) / TranslateScale), 0, 0);
// Get the current orientantion from the RotateTransform3D
var q = new Quaternion(m_Rotation.Axis, m_Rotation.Angle);
var qC = q;
qC.Conjugate();
// Here we rotate our panning vector about the the rotaion axis of any current rotation transform
// and then sum the new translation with any exisiting translation
qV = q * qV * qC;
m_Translate.OffsetX += qV.X;
m_Translate.OffsetY += qV.Y;
m_Translate.OffsetZ += qV.Z;
}
}
}
The GetXXXTransform3D methods are extension methods defined as follow:
public static ScaleTransform3D GetScaleTransform3D(this Transform3DGroup transform3DGroup)
{
ScaleTransform3D scaleTransform3D = null;
if (transform3DGroup != null)
{
foreach (var transform in transform3DGroup.Children)
{
scaleTransform3D = transform as ScaleTransform3D;
if (scaleTransform3D != null) return scaleTransform3D;
}
}
return scaleTransform3D;
}
public static RotateTransform3D GetRotateTransform3D(this Transform3DGroup transform3DGroup)
{
RotateTransform3D rotateTransform3D = null;
if (transform3DGroup != null)
{
foreach (var transform in transform3DGroup.Children)
{
rotateTransform3D = transform as RotateTransform3D;
if (rotateTransform3D != null) return rotateTransform3D;
}
}
return rotateTransform3D;
}
public static TranslateTransform3D GetTranslateTransform3D(this Transform3DGroup transform3DGroup)
{
TranslateTransform3D translateTransform3D = null;
if (transform3DGroup != null)
{
foreach (var transform in transform3DGroup.Children)
{
translateTransform3D = transform as TranslateTransform3D;
if (translateTransform3D != null) return translateTransform3D;
}
}
return translateTransform3D;
}
2nd, you need to declare a Transform to your PerspectiveCamera as follow:
(the example is taken from Sasha Barber's Elements3D project which I used to test this)
<Tools:TrackballDecorator x:Name="tbViewPort">
<Viewport3D x:Name="vpFeeds">
<Viewport3D.Camera>
<PerspectiveCamera x:Name="camera" Position="-2,2,40" LookDirection="2,-2,-40" FieldOfView="90">
<PerspectiveCamera.Transform>
<Transform3DGroup />
</PerspectiveCamera.Transform>
</PerspectiveCamera>
</Viewport3D.Camera>
<ContainerUIElement3D x:Name="container" />
<ModelVisual3D x:Name="model">
<ModelVisual3D.Content>
<DirectionalLight Color="White" Direction="-1,-1,-1" />
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Tools:TrackballDecorator>
3rd, since we are going to store each part of the whole transformation in a separate value, you need to create the relevant properties in your settings file, i.e. CameraScaleX, CameraScaleY, CameraScaleZ, CameraTranslateX, CameraTranslateY, CameraTranslateZ, CameraRotateAxisX, CameraRotateAxisY, CameraRotateAxisZ and CameraRotateAngle. All are of type double and are stored in User scope.
4th and last step is to actually save and load these settings into the camera using the following code:
private void SaveCameraSettings()
{
var transform3DGroup = camera.Transform as Transform3DGroup;
if (transform3DGroup != null)
{
foreach (var transform in transform3DGroup.Children)
{
var scale = transform as ScaleTransform3D;
if (scale != null) SaveCameraSetting(scale);
var rotate = transform as RotateTransform3D;
if (rotate != null) SaveCameraSetting(rotate);
var translate = transform as TranslateTransform3D;
if (translate != null) SaveCameraSetting(translate);
}
Settings.Default.Save();
}
}
private static void SaveCameraSetting(ScaleTransform3D transform)
{
Properties.Settings.Default.CameraScaleX = transform.ScaleX;
Properties.Settings.Default.CameraScaleY = transform.ScaleY;
Properties.Settings.Default.CameraScaleZ = transform.ScaleZ;
}
private static void SaveCameraSetting(RotateTransform3D transform)
{
var axisAngleRotation3D = transform.Rotation as AxisAngleRotation3D;
if (axisAngleRotation3D != null)
{
Properties.Settings.Default.CameraRotateAxisX = axisAngleRotation3D.Axis.X;
Properties.Settings.Default.CameraRotateAxisY = axisAngleRotation3D.Axis.Y;
Properties.Settings.Default.CameraRotateAxisZ = axisAngleRotation3D.Axis.Z;
Properties.Settings.Default.CameraRotateAngle = axisAngleRotation3D.Angle;
}
}
private static void SaveCameraSetting(TranslateTransform3D transform)
{
Properties.Settings.Default.CameraTranslateX = transform.OffsetX;
Properties.Settings.Default.CameraTranslateY = transform.OffsetY;
Properties.Settings.Default.CameraTranslateZ = transform.OffsetZ;
}
private void LoadCameraPosition()
{
var d = Settings.Default;
var transform3DGroup = new Transform3DGroup();
var scaleTransform3D = new ScaleTransform3D(d.CameraScaleX, d.CameraScaleY, d.CameraScaleZ);
var translateTransform3D = new TranslateTransform3D(d.CameraTranslateX, d.CameraTranslateY, d.CameraTranslateZ);
var axisAngleRotation3D = new AxisAngleRotation3D(new Vector3D(d.CameraRotateAxisX, d.CameraRotateAxisY, d.CameraRotateAxisZ),
d.CameraRotateAngle);
var rotateTransform3D = new RotateTransform3D(axisAngleRotation3D);
transform3DGroup.Children.Add(scaleTransform3D);
transform3DGroup.Children.Add(translateTransform3D);
transform3DGroup.Children.Add(rotateTransform3D);
tbViewPort.Transform = transform3DGroup;
}
Hopefully, I didn't forget anything. If you need more help or don't understand something, please don't hesitate to ask ;-)

You would need both the cameras view matrix data and the projection matrix data. The view matrix will contain the data about the position, rotation, scale and translation of the camera and the projection matrix will contain things like the field of view, near plane, far plane and other data.
Sorry I cant help with exporting/importing that data since I've not used WPF, but there might be rawdata properties exposed if it uses anything to do with as3's built in Matrix classes, this is usualy a as3 Vector. Object the the matrices 16 values exposed as row ordered floating point values.

I believe what you need is Position, LookDirection, UpDirection, FieldOfView, NearPlaneDistance, FarPlaneDistance. All the above properties define the camera.

Related

WPF - Helix Toolkit Auto Rotation

I have been researching this over the last week - 2 weeks and I have been unable to find a solution. I am loading a 3D Model from an STL file and attempting to rotate the 3D model automatically around 1 axis. The idea would be something like a slow-moving animation that displays a 360-degree view around the Y-axis of the model.
XAML:
<Grid>
<StackPanel x:Name="myViewPort">
<helix:HelixViewport3D x:Name="viewPort3d" ZoomExtentsWhenLoaded="true" RotateAroundMouseDownPoint="true" CameraRotationMode="Turntable" Height="1000" ShowViewCube="false">
<helix:DefaultLights/>
<ModelVisual3D x:Name="visualModel"/>
</helix:HelixViewport3D>
</StackPanel>
</Grid>
C#:
public void load3dModel()
{
StLReader stlReader = new StLReader();
Model3DGroup MyModel = stlReader.Read(MODEL_PATH);
/* Auto Rotate Here */
System.Windows.Media.Media3D.Material mat = MaterialHelper.CreateMaterial(new SolidColorBrush(Color.FromRgb(255, 255, 255)));
foreach (System.Windows.Media.Media3D.GeometryModel3D geometryModel in MyModel.Children)
{
geometryModel.Material = mat;
geometryModel.BackMaterial = mat;
}
visualModel.Content = MyModel;
}
The tricky part about this is I need to be able to rotate the model using CODE ONLY. The models generated can be one of the hundreds, and it will depend on the application for what the model will be... So the code needs to be able to handle rotating around the same axis, I can guarantee when the 3D model is exported to the STL file it will be flat along the X-axis.
--- UPDATE ---
Attempted Rotation via Storyboard:
public void load3dModel()
{
StLReader stlReader = new StLReader();
Model3DGroup MyModel = stlReader.Read(MODEL_PATH);
System.Windows.Media.Media3D.Material mat = MaterialHelper.CreateMaterial(new SolidColorBrush(Color.FromRgb(255, 255, 255)));
foreach (System.Windows.Media.Media3D.GeometryModel3D geometryModel in MyModel.Children)
{
geometryModel.Material = mat;
geometryModel.BackMaterial = mat;
}
visualModel.Content = MyModel;
/* Auto Rotate Here */
GeometryModel3D geoModel = new GeometryModel3D()
{
Transform = new RotateTransform3D()
{
Rotation = new AxisAngleRotation3D()
{
Axis = new Vector3D(0, 1, 0),
Angle = 0
}
}
};
MyModel.Children.Add(geoModel);
var Rotation3DAnimation = new Rotation3DAnimation();
var FromAxis = new AxisAngleRotation3D()
{
Axis = new Vector3D(0, 1, 0),
Angle = 0
};
var ToAxis = new AxisAngleRotation3D()
{
Axis = new Vector3D(0, 1, 0),
Angle = 359
};
Rotation3DAnimation.From = FromAxis;
Rotation3DAnimation.To = ToAxis;
Rotation3DAnimation.Duration = Duration.Forever; //ADDED DURATION, Still did not work!
var rotateStoryboard = new Storyboard
{
Duration = new Timespan(0, 0, 12),
RepeatBehavior = RepeatBehavior.Forever,
};
Storyboard.SetTarget(Rotation3DAnimation, geoModel.Transform);
Storyboard.SetTargetProperty(Rotation3DAnimation, new PropertyPath("Rotation"));
rotateStoryboard.Children.Add(Rotation3DAnimation);
rotateStoryboard.Begin();
}
This did not work... Nothing changed?
Thanks!
I am not sure if I understood correctly what you are trying to accomplish so let me know if I misunderstood:
In the code you showed, you are loading several GeometryModel3D so are you trying to make them all rotate or just one ?
One way you could make it rotate is via the Transform property of the GeometryModel3D.
You will have to set up a DispatcherTimer and update the angle of the rotation on every Tick:
I made an example based on what you provided where I make one 3D model rotate:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Load3dModel();
this.timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) };
this.timer.Tick += Timer_Tick;
this.timer.Start();
}
/// <summary>
/// Change the rotation
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Timer_Tick(object sender, EventArgs e)
{
if (this.angle >= 360)
{
this.angle = 0;
}
else
{
//Nothing to do
}
this.angle = this.angle + 0.25;
//You can adapt the code if you have many children
GeometryModel3D geometryModel3D = (GeometryModel3D)((Model3DGroup)visualModel.Content).Children.First();
if (geometryModel3D.Transform is RotateTransform3D rotateTransform3 && rotateTransform3.Rotation is AxisAngleRotation3D rotation)
{
rotation.Angle = this.angle;
}
else
{
///Initialize the Transform (I didn't do it in my example but you could do this initialization in <see Load3dModel/>)
geometryModel3D.Transform = new RotateTransform3D()
{
Rotation = new AxisAngleRotation3D()
{
Axis = new Vector3D(0, 1, 0),
Angle = this.angle,
}
};
}
}
private DispatcherTimer timer;
public void Load3dModel()
{
StLReader stlReader = new StLReader();
/*
Model3DGroup MyModel = stlReader.Read(OrLoadFromPath));
*/
Model3DGroup myModel = new Model3DGroup();
// Create a mesh builder and add a box to it
var meshBuilder = new MeshBuilder(false, false);
meshBuilder.AddBox(new Point3D(0, 0, 1), 1, 2, 0.5);
meshBuilder.AddBox(new Rect3D(0, 0, 1.2, 0.5, 1, 0.4));
// Create a mesh from the builder (and freeze it)
var mesh = meshBuilder.ToMesh(true);
// Add 3 models to the group (using the same mesh, that's why we had to freeze it)
myModel.Children.Add(new GeometryModel3D { Geometry = mesh});
myModel.Children.Add(new GeometryModel3D { Geometry = mesh, Transform = new TranslateTransform3D(-2, 0, 0)});
myModel.Children.Add(new GeometryModel3D { Geometry = mesh, Transform = new TranslateTransform3D(2, 0, 0)});
System.Windows.Media.Media3D.Material mat = MaterialHelper.CreateMaterial(new SolidColorBrush(Color.FromRgb(255, 255, 255)));
foreach (System.Windows.Media.Media3D.GeometryModel3D geometryModel in myModel.Children)
{
geometryModel.Material = mat;
geometryModel.BackMaterial = mat;
}
visualModel.Content = myModel;
}
private double angle = 0;
}
The rotation was pretty smooth on my end with those parameters but you will have to test/adapt it on your application.

Silverlight Pixel Perfect Collsion Detection

I am working on a project for my Uni, and I am currently stuck on a Pixel Perfect Collision Detection from this website http://www.andybeaulieu.com/Home/tabid/67/EntryID/160/Default.aspx. I have downloded example project that is using this collsion detection and it is working fine even with my own pictures. I have done the same thing in my project and it is not working. Here is the link to my app: https://www.cubby.com/pl/LostInTheMath.zip/_dd23e2c827604c068a3fe63ff42d22b2 could anyone tell me whats wrong with it? Thank you.
Here is the main code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Media.Imaging;
namespace LostInTheMath
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Page_Loaded);
}
void Page_Loaded(object sender, RoutedEventArgs e)
{
CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
}
private void UserCntrl_MouseMove(object sender, MouseEventArgs e)
{
Point pt = e.GetPosition(cnvHitTest);
Monkey.SetValue(Canvas.LeftProperty, pt.X);
Monkey.SetValue(Canvas.TopProperty, pt.Y);
}
void CompositionTarget_Rendering(object sender, EventArgs e)
{
Image MonkeyShell = Monkey.FindName("imgMonkey") as Image;
Image Mazer = Maze.FindName("imgMaze") as Image;
if (CheckCollision(Monkey, MonkeyShell, Maze, Mazer))
{
Monkey.Width = 1000;
txtStatus.Text = "Collision with XAML Element!";
return;
}
txtStatus.Text = string.Empty;
}
private bool CheckCollision(FrameworkElement control1, FrameworkElement controlElem1, FrameworkElement control2, FrameworkElement controlElem2)
{
// first see if sprite rectangles collide
Rect rect1 = UserControlBounds(control1);
Rect rect2 = UserControlBounds(control2);
rect1.Intersect(rect2);
if (rect1 == Rect.Empty)
{
// no collision - GET OUT!
return false;
}
else
{
bool bCollision = false;
Point ptCheck = new Point();
// NOTE that creating the writeablebitmap is a bit intense
// so we will do this once and store results in Tag property
// in a real game, you might abstract this into a Sprite class.
if (controlElem1 is Image)
controlElem1.Tag = GetWriteableBitmap(control1);
if (controlElem2 is Image)
controlElem2.Tag = GetWriteableBitmap(control2);
// now we do a more accurate pixel hit test
for (int x = Convert.ToInt32(rect1.X); x < Convert.ToInt32(rect1.X + rect1.Width); x++)
{
for (int y = Convert.ToInt32(rect1.Y); y < Convert.ToInt32(rect1.Y + rect1.Height); y++)
{
ptCheck.X = x;
ptCheck.Y = y;
if (CheckCollisionPoint(ptCheck, control1, controlElem1))
if (CheckCollisionPoint(ptCheck, control2, controlElem2))
{
bCollision = true;
break;
}
}
if (bCollision) break;
}
return bCollision;
}
}
public bool CheckCollisionPoint(Point pt, FrameworkElement control, FrameworkElement controlElem)
{
if (controlElem is Image)
{
// NOTE that we saved the WB in the Tag object for performance.
// in a real app, you might abstract this in your sprite class.
WriteableBitmap wb = controlElem.Tag as WriteableBitmap;
int width = wb.PixelWidth;
int height = wb.PixelHeight;
double offSetX = Convert.ToDouble(control.GetValue(Canvas.LeftProperty));
double offSetY = Convert.ToDouble(control.GetValue(Canvas.TopProperty));
pt.X = pt.X - offSetX;
pt.Y = pt.Y - offSetY;
int offset = Convert.ToInt32((width * pt.Y) + pt.X);
return (wb.Pixels[offset] != 0);
}
else
{
List<UIElement> hits = System.Windows.Media.VisualTreeHelper.FindElementsInHostCoordinates(pt, controlElem) as List<UIElement>;
return (hits.Contains(controlElem));
}
}
private WriteableBitmap GetWriteableBitmap(FrameworkElement control)
{
WriteableBitmap wb = new WriteableBitmap((int)control.Width, (int)control.Height); ;
wb.Render(control, new TranslateTransform());
wb.Invalidate();
return wb;
}
public Rect UserControlBounds(FrameworkElement control)
{
Point ptTopLeft = new Point(Convert.ToDouble(control.GetValue(Canvas.LeftProperty)), Convert.ToDouble(control.GetValue(Canvas.TopProperty)));
Point ptBottomRight = new Point(Convert.ToDouble(control.GetValue(Canvas.LeftProperty)) + control.Width, Convert.ToDouble(control.GetValue(Canvas.TopProperty)) + control.Height);
return new Rect(ptTopLeft, ptBottomRight);
}
}
}

Is a dashed border around a selected text/image element possible in Silverlight?

I have an image editor I'm developing in silverlight which has multiple text and image elements on one canvas, that are draggable etc. I need feedback for the user to highlight the selected element when it is clicked on by the user and highlight a different element instead if another is clicked. I think I should do this with a dashed border around the element, but I don't know if it's possible.
Below is my code relating to the elements -
Project.cs
namespace ImageEditor.Client.BLL
{
public class Project : INotifyPropertyChanged
{
private int numberOfElements;
#region Properties
private ObservableCollection<FrameworkElement> elements;
public ObservableCollection<FrameworkElement> Elements
{
get { return elements; }
set
{
elements = value;
NotifyPropertyChanged("Elements");
}
}
private FrameworkElement selectedElement;
public FrameworkElement SelectedElement
{
get { return selectedElement; }
set
{
selectedElement = value;
NotifyPropertyChanged("SelectedElement");
}
}
private TextBlock selectedTextElement;
public TextBlock SelectedTextElement
{
get { return selectedTextElement; }
set
{
selectedTextElement = value;
NotifyPropertyChanged("SelectedTextElement");
}
}
private Image selectedImageElement;
public Image SelectedImageElement
{
get { return selectedImageElement; }
set
{
selectedImageElement = value;
NotifyPropertyChanged("SelectedImageElement");
}
}
#endregion
#region Methods
private void AddTextElement(object param)
{
TextBlock textBlock = new TextBlock();
textBlock.Text = "New Text";
textBlock.Foreground = new SolidColorBrush(Colors.Gray);
textBlock.FontSize = 25;
textBlock.FontFamily = new FontFamily("Arial");
textBlock.Cursor = Cursors.Hand;
textBlock.Tag = null;
AddDraggingBehavior(textBlock);
textBlock.MouseLeftButtonUp += element_MouseLeftButtonUp;
this.Elements.Add(textBlock);
numberOfElements++;
this.SelectedElement = textBlock;
this.selectedTextElement = textBlock;
}
private BitmapImage GetImageFromLocalMachine(out bool? success, out string fileName)
{
OpenFileDialog dialog = new OpenFileDialog()
{
Filter = "Image Files (*.bmp;*.jpg;*.gif;*.png;)|*.bmp;*.jpg;*.gif;*.png;",
Multiselect = false
};
success = dialog.ShowDialog();
if (success == true)
{
fileName = dialog.File.Name;
FileStream stream = dialog.File.OpenRead();
byte[] data;
BitmapImage imageSource = new BitmapImage();
using (FileStream fileStream = stream)
{
imageSource.SetSource(fileStream);
data = new byte[fileStream.Length];
fileStream.Read(data, 0, data.Length);
fileStream.Flush();
fileStream.Close();
}
return imageSource;
}
else
{
fileName = string.Empty;
return new BitmapImage();
}
}
private void AddImageElement(object param)
{
bool? gotImage;
string fileName;
BitmapImage imageSource = GetImageFromLocalMachine(out gotImage, out fileName);
if (gotImage == true)
{
Image image = new Image();
image.Name = fileName;
image.Source = imageSource;
image.Height = imageSource.PixelHeight;
image.Width = imageSource.PixelWidth;
image.MaxHeight = imageSource.PixelHeight;
image.MaxWidth = imageSource.PixelWidth;
image.Cursor = Cursors.Hand;
image.Tag = null;
AddDraggingBehavior(image);
image.MouseLeftButtonUp += element_MouseLeftButtonUp;
this.Elements.Add(image);
numberOfElements++;
this.SelectedElement = image;
this.SelectedImageElement = image;
}
}
private void OrderElements()
{
var elList = (from element in this.Elements
orderby element.GetValue(Canvas.ZIndexProperty)
select element).ToList<FrameworkElement>();
for (int i = 0; i < elList.Count; i++)
{
FrameworkElement fe = elList[i];
fe.SetValue(Canvas.ZIndexProperty, i);
}
this.Elements = new ObservableCollection<FrameworkElement>(elList);
}
public void element_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
this.SelectedElement = sender as FrameworkElement;
if (sender is TextBlock)
{
this.SelectedTextElement = sender as TextBlock;
FadeOut(this.SelectedTextElement);
}
else if (sender is Image)
{
this.SelectedImageElement = sender as Image;
FadeOut(this.SelectedImageElement);
}
}
#endregion
More than needed there but you get a good idea of how it all works from that. How might I go about it? I'm still pretty new to silverlight
Edit:
This is my start attempt at a DashBorder Method, wherein I'm trying to make a rectangle the same dimensions as the selected element which will go around the element
public static void DashBorder(FrameworkElement element)
{
Rectangle rect = new Rectangle();
rect.Stroke = new SolidColorBrush(Colors.Black);
rect.Width=element.Width;
rect.Height=element.Height;
rect.StrokeDashArray = new DoubleCollection() { 2, 2 };
}
It appears to do nothing and isn't what I want to do anyway. Is there no way to make a dash border on a FrameworkElement directly?
I don't know how, but google does.
You can use the StrokeDashArray to achieve the desired effect,
example:
<Rectangle Canvas.Left="10" Canvas.Top="10" Width="100" Height="100"
Stroke="Black" StrokeDashArray="10, 2"/>
The first number in StrokeDashArray is the length of the dash, the
second number is the length of the gap. You can repeat the dash gap
pairs to generate different patterns.
Edit:
To do this in code create a rectangle and set it's StrokeDashArray property like this (code untested):
Rectangle rect = new Rectangle();
rect.StrokeThickness = 1;
double[] dashArray = new double[2];
dashArray[0] = 2;
dashArray[1] = 4;
rect.StrokeDashArray = dashArray;

Allow a user to create a line series on a wpf chart by clicking on the chart

I have a WPF chart currently displaying an Area series. I need to allow a user the ability to click on the chart and add a new point for a Line series. The problem I'm having is I can't seem to find a way to convert the point from a MouseButtonEventArgs to a LineDataPoint.
private void chtPowerFlowMap_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point p = e.GetPosition(chtPowerFlowMap);
points.Add(p); //Issue here is this will return a point in screen coordinates and not in chart coordinates
ls.ItemsSource = points; //ls is an existing lineseries and points is a List<Point> object
chtPowerFlowMap.Series.Add(ls);
}
Thanks in advance.
Here is a complete solution. You can click anywhere on the chart and there will be a new point at that palce.
MainWindows.xaml
<chart:Chart x:Name="chart" MouseLeftButtonDown="Chart_MouseLeftButtonDown">
<chart:LineSeries ItemsSource="{Binding}" x:Name="lineSeries"
DependentValuePath="Value"
IndependentValuePath="Date"/>
</chart:Chart>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private ObservableCollection<Item> items;
public MainWindow()
{
InitializeComponent();
Random rd = new Random();
items = new ObservableCollection<Item>(
Enumerable.Range(0, 10)
.Select(i => new Item
{
Date = DateTime.Now.AddMonths(i - 10),
Value = rd.Next(10,50)
}));
this.DataContext = items;
}
public class Item
{
public DateTime Date { get; set; }
public double Value { get; set; }
}
private void Chart_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var p = Mouse.GetPosition(this.lineSeries);
//ranges in the real values
var left = items.Min(i => i.Date);
var right = items.Max(i => i.Date);
var top = items.Max(i => i.Value);
var bottom = items.Min(i => i.Value);
var hRange = right - left;
var vRange = top - bottom;
//ranges in the pixels
var width = this.lineSeries.ActualWidth;
var height = this.lineSeries.ActualHeight;
//from the pixels to the real value
var currentX = left + TimeSpan.FromTicks((long)(hRange.Ticks * p.X / width));
var currentY = top - vRange * p.Y / height;
this.items.Add(new Item { Date = currentX, Value = currentY });
}
}

WPF FlowDocument Scale to Fit Page

I have a FlowDocument that varies in height due to an ItemsControl in a BlockUIContainer. In some cases, the ItemsControl extends beyond the page height. Is there a way to scale the FlowDocument to fit a page (8.5" X 11") right before printing if needed?
As of right now, the FlowDocument is named 'doc' and the method for printing I am using is:
private void Print_Click(object sender, RoutedEventArgs e)
{
PrintDialog pd = new PrintDialog();
doc.PageHeight = pd.PrintableAreaHeight;
doc.PageWidth = pd.PrintableAreaWidth;
doc.ColumnGap = 0;
doc.ColumnWidth = pd.PrintableAreaWidth;
IDocumentPaginatorSource dps = doc;
pd.PrintDocument(dps.DocumentPaginator, "Sheet");
}
I know it's a bit late, but here is the solution I came up with.
First, we create a wrapper that will generate the document pages for us. Each page will have a scale transformation applied to it before returning it.
public class FittedDocumentPaginator : DocumentPaginator
{
public DocumentPaginator Base { get; private set; }
public double Scale { get; private set; }
private readonly ScaleTransform _sTransform;
public FittedDocumentPaginator( DocumentPaginator baseDp, double scale )
{
if ( baseDp == null )
throw new ArgumentNullException( "baseDp" );
Base = baseDp;
Scale = scale;
_sTransform = new ScaleTransform( scale, scale );
}
public override DocumentPage GetPage( int pageNumber )
{
var page = Base.GetPage( pageNumber );
( (ContainerVisual)page.Visual ).Transform = _sTransform;
return page;
}
public override bool IsPageCountValid
{
get { return Base.IsPageCountValid; }
}
public override int PageCount
{
get { return Base.PageCount; }
}
public override Size PageSize
{
get { return Base.PageSize; }
set { Base.PageSize = value; }
}
public override IDocumentPaginatorSource Source
{
get { return Base.Source; }
}
}
Using it is fairly simple:
private void PrintDocument( PrintDialog pd, FlowDocument document, double scale, string title )
{
DocumentPaginator dp = ( (IDocumentPaginatorSource)document ).DocumentPaginator;
FittedDocumentPaginator fdp = new FittedDocumentPaginator( dp, scale );
pd.PrintDocument( fdp, title );
}
If you're interested, here is how we determined the scale. In our case, the document was extended past the page width, but it can easily be modified to accommodate the page height.
private void Print( FlowDocument document, string title, double documentWidth )
{
var pd = new PrintDialog();
if ( pd.ShowDialog() == true )
{
// Set the printing margins.
Thickness pageMargins = document.PagePadding;
document.PagePadding = new Thickness( 15.0 );
// In our case, the document's width is NaN so we have to navigate the visual tree to get the ActualWidth, which is represented by 'documentWidth'.
double scale = documentWidth / pd.PrintableAreaWidth;
if ( scale < 1.0 )
{
// The report fits on the page just fine. Don't scale.
scale = 1.0;
}
double invScale = 1 / scale;
document.PageHeight = pd.PrintableAreaHeight * scale;
document.PageWidth = pd.PrintableAreaWidth * scale;
DocumentPaginator dp = ( (IDocumentPaginatorSource)document ).DocumentPaginator;
FittedDocumentPaginator fdp = new FittedDocumentPaginator( dp, invScale );
pd.PrintDocument( fdp, title );
// Restore the original values so the GUI isn't altered.
document.PageHeight = Double.NaN;
document.PageWidth = Double.NaN;
document.PagePadding = pageMargins;
}
}

Resources