New Plot Type using OxyPlot - wpf

I am attempting to create a new plot type in OxyPlot. I essentally need a StairStepSeries, but with any negative values replaces by their Math.Abs value and when this occurs, the line style to reflect this has happened (by using color and or LineStyle). So, to highlight what I want
To do this, I have created the two classes (I have pasted actual code used below). This is conceptually easy when you know the tools you are working with, which I don't. My problem is directly related to my improper use of rectangle.DrawClippedLineSegments(). I can get a standard StairStepSeries plotting (copying the internal code) but when I attempt to use the rectangle.DrawClippedLineSegments() intuatively I realise I have not got a clue what this method does or how it is supposed to be used, but can't find any documentation. What is rectangle.DrawClippedLineSegments() doing and how should this method be used?
Thanks for your time.
Code:
namespace OxyPlot.Series
{
using System;
using System.Collections.Generic;
using OxyPlot.Series;
/// <summary>
/// Are we reversing positive of negative values?
/// </summary>
public enum ThresholdType { ReflectAbove, ReflectBelow };
/// <summary>
/// Class that renders absolute positive and absolute negative values
/// but changes the line style according to those values that changed sign.
/// The value at which the absolute vaue is taken can be manually set.
/// </summary>
public class AbsoluteStairStepSeries : StairStepSeries
{
/// <summary>
/// The default color used when a value is reversed accross the threshold.
/// </summary>
private OxyColor defaultColorThreshold;
#region Initialization.
/// <summary>
/// Default ctor.
/// </summary>
public AbsoluteStairStepSeries()
{
this.Threshold = 0.0;
this.ThresholdType = OxyPlot.Series.ThresholdType.ReflectAbove;
this.ColorThreshold = this.ActualColor;
this.LineStyleThreshold = OxyPlot.LineStyle.LongDash;
}
#endregion // Initialization.
/// <summary>
/// Sets the default values.
/// </summary>
/// <param name="model">The model.</param>
protected override void SetDefaultValues(PlotModel model)
{
base.SetDefaultValues(model);
if (this.ColorThreshold.IsAutomatic())
this.defaultColorThreshold = model.GetDefaultColor();
if (this.LineStyleThreshold == LineStyle.Automatic)
this.LineStyleThreshold = model.GetDefaultLineStyle();
}
/// <summary>
/// Renders the LineSeries on the specified rendering context.
/// </summary>
/// <param name="rc">The rendering context.</param>
/// <param name="model">The owner plot model.</param>
public override void Render(IRenderContext rc, PlotModel model)
{
if (this.ActualPoints.Count == 0)
return;
// Set defaults.
this.VerifyAxes();
OxyRect clippingRect = this.GetClippingRect();
double[] dashArray = this.ActualDashArray;
double[] verticalLineDashArray = this.VerticalLineStyle.GetDashArray();
LineStyle lineStyle = this.ActualLineStyle;
double verticalStrokeThickness = double.IsNaN(this.VerticalStrokeThickness) ?
this.StrokeThickness : this.VerticalStrokeThickness;
OxyColor actualColor = this.GetSelectableColor(this.ActualColor);
// Perform thresholding on clipping rectangle.
//double threshold = this.YAxis.Transform(this.Threshold);
//switch (ThresholdType)
//{
// // reflect any values below the threshold above the threshold.
// case ThresholdType.ReflectAbove:
// //if (clippingRect.Bottom < threshold)
// clippingRect.Bottom = threshold;
// break;
// case ThresholdType.ReflectBelow:
// break;
// default:
// break;
//}
// Perform the render action.
Action<IList<ScreenPoint>, IList<ScreenPoint>> renderPoints = (lpts, mpts) =>
{
// Clip the line segments with the clipping rectangle.
if (this.StrokeThickness > 0 && lineStyle != LineStyle.None)
{
if (!verticalStrokeThickness.Equals(this.StrokeThickness) ||
this.VerticalLineStyle != lineStyle)
{
// TODO: change to array
List<ScreenPoint> hlptsOk = new List<ScreenPoint>();
List<ScreenPoint> vlptsOk = new List<ScreenPoint>();
List<ScreenPoint> hlptsFlip = new List<ScreenPoint>();
List<ScreenPoint> vlptsFlip = new List<ScreenPoint>();
double threshold = this.YAxis.Transform(this.Threshold);
for (int i = 0; i + 2 < lpts.Count; i += 2)
{
switch (ThresholdType)
{
case ThresholdType.ReflectAbove:
clippingRect.Bottom = threshold;
if (lpts[i].Y < threshold)
hlptsFlip.Add(new ScreenPoint(lpts[i].X, threshold - lpts[i].Y));
else
hlptsOk.Add(lpts[i]);
if (lpts[i + 1].Y < threshold)
{
ScreenPoint tmp = new ScreenPoint(
lpts[i + 1].X, threshold - lpts[i + 1].Y);
hlptsFlip.Add(tmp);
vlptsFlip.Add(tmp);
}
else
{
hlptsOk.Add(lpts[i + 1]);
vlptsOk.Add(lpts[i + 1]);
}
if (lpts[i + 2].Y < threshold)
vlptsFlip.Add(new ScreenPoint(lpts[i + 2].X, threshold - lpts[i + 2].Y));
else
vlptsOk.Add(lpts[i + 2]);
break;
case ThresholdType.ReflectBelow:
break;
default:
break;
}
}
//for (int i = 0; i + 2 < lpts.Count; i += 2)
//{
// hlpts.Add(lpts[i]);
// hlpts.Add(lpts[i + 1]);
// vlpts.Add(lpts[i + 1]);
// vlpts.Add(lpts[i + 2]);
//}
rc.DrawClippedLineSegments(
clippingRect,
hlptsOk,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
hlptsFlip,
OxyColor.FromRgb(255, 0, 0),
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsOk,
actualColor,
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsFlip,
OxyColor.FromRgb(255, 0, 0),
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
}
else
{
rc.DrawClippedLine(
clippingRect,
lpts,
0,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
}
}
if (this.MarkerType != MarkerType.None)
{
rc.DrawMarkers(
clippingRect,
mpts,
this.MarkerType,
this.MarkerOutline,
new[] { this.MarkerSize },
this.MarkerFill,
this.MarkerStroke,
this.MarkerStrokeThickness);
}
};
// Transform all points to screen coordinates
// Render the line when invalid points occur.
var linePoints = new List<ScreenPoint>();
var markerPoints = new List<ScreenPoint>();
double previousY = double.NaN;
foreach (var point in this.ActualPoints)
{
if (!this.IsValidPoint(point))
{
renderPoints(linePoints, markerPoints);
linePoints.Clear();
markerPoints.Clear();
previousY = double.NaN;
continue;
}
var transformedPoint = this.Transform(point);
if (!double.IsNaN(previousY))
{
// Horizontal line from the previous point to the current x-coordinate
linePoints.Add(new ScreenPoint(transformedPoint.X, previousY));
}
linePoints.Add(transformedPoint);
markerPoints.Add(transformedPoint);
previousY = transformedPoint.Y;
}
renderPoints(linePoints, markerPoints);
if (this.LabelFormatString != null)
{
// Render point labels (not optimized for performance).
this.RenderPointLabels(rc, clippingRect);
}
}
#region Properties.
/// <summary>
/// The value, positive or negative at which any values are reversed
/// accross the threshold.
/// </summary>
public double Threshold { get; set; }
/// <summary>
/// Hold the thresholding type.
/// </summary>
public ThresholdType ThresholdType { get; set; }
/// <summary>
/// Gets or sets the color for the part of the
/// line that is above/below the threshold.
/// </summary>
public OxyColor ColorThreshold { get; set; }
/// <summary>
/// Gets the actual threshold color.
/// </summary>
/// <value>The actual color.</value>
public OxyColor ActualColorThreshold
{
get { return this.ColorThreshold.GetActualColor(this.defaultColorThreshold); }
}
/// <summary>
/// Gets or sets the line style for the part of the
/// line that is above/below the threshold.
/// </summary>
/// <value>The line style.</value>
public LineStyle LineStyleThreshold { get; set; }
/// <summary>
/// Gets the actual line style for the part of the
/// line that is above/below the threshold.
/// </summary>
/// <value>The line style.</value>
public LineStyle ActualLineStyleThreshold
{
get
{
return this.LineStyleThreshold != LineStyle.Automatic ?
this.LineStyleThreshold : LineStyle.Solid;
}
}
#endregion // Properties.
}
}
and the WPF class
namespace OxyPlot.Wpf
{
using System.Windows;
using System.Windows.Media;
using OxyPlot.Series;
/// <summary>
/// The WPF wrapper for OxyPlot.AbsoluteStairStepSeries.
/// </summary>
public class AbsoluteStairStepSeries : StairStepSeries
{
/// <summary>
/// Default ctor.
/// </summary>
public AbsoluteStairStepSeries()
{
this.InternalSeries = new OxyPlot.Series.AbsoluteStairStepSeries();
}
/// <summary>
/// Creates the internal series.
/// </summary>
/// <returns>
/// The internal series.
/// </returns>
public override OxyPlot.Series.Series CreateModel()
{
this.SynchronizeProperties(this.InternalSeries);
return this.InternalSeries;
}
/// <summary>
/// Synchronizes the properties.
/// </summary>
/// <param name="series">The series.</param>
protected override void SynchronizeProperties(OxyPlot.Series.Series series)
{
base.SynchronizeProperties(series);
var s = series as OxyPlot.Series.AbsoluteStairStepSeries;
s.Threshold = this.Threshold;
s.ColorThreshold = this.ColorThreshold.ToOxyColor();
}
/// <summary>
/// Identifies the <see cref="Threshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty ThresholdProperty = DependencyProperty.Register(
"Threshold", typeof(double), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(0.0, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="ThresholdType"/> dependency property.
/// </summary>
public static readonly DependencyProperty ThresholdTypeProperty = DependencyProperty.Register(
"ThresholdType", typeof(ThresholdType), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(ThresholdType.ReflectAbove, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="ColorThreshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty ColorThresholdProperty = DependencyProperty.Register(
"ColorThreshold", typeof(Color), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(Colors.Red, AppearanceChanged));
/// <summary>
/// Identifies the <see cref="LineStyleThreshold"/> dependency property.
/// </summary>
public static readonly DependencyProperty LineStyleThresholdProperty = DependencyProperty.Register(
"LineStyleThreshold", typeof(LineStyle), typeof(AbsoluteStairStepSeries),
new UIPropertyMetadata(LineStyle.LongDash, AppearanceChanged));
/// <summary>
/// Get or set the threshold value.
/// </summary>
public double Threshold
{
get { return (double)GetValue(ThresholdProperty); }
set { SetValue(ThresholdProperty, value); }
}
/// <summary>
/// Get or set the threshold type to be used.
/// </summary>
public ThresholdType ThresholdType
{
get { return (ThresholdType)GetValue(ThresholdTypeProperty); }
set { SetValue(ThresholdTypeProperty, value); }
}
/// <summary>
/// Get or set the threshold color.
/// </summary>
public Color ColorThreshold
{
get { return (Color)GetValue(ColorThresholdProperty); }
set { SetValue(ColorThresholdProperty, value); }
}
/// <summary>
/// Get or set the threshold line style.
/// </summary>
public LineStyle LineStyleThreshold
{
get { return (LineStyle)GetValue(LineStyleThresholdProperty); }
set { SetValue(LineStyleThresholdProperty, value); }
}
}
}

I had a chance to have a look into this, and whilst what I suggest may not be the ideal solution, it should give you some helpful nudges.
Firstly, DrawClippedLineSegments (You can view the source here) and its extension method counterparts (DrawClippedRectangleAsPolygon, DrawClippedEllipse, etc.) are used to draw various plot graphics onto the main plot/rendering area. The clipping rectangle supplied to this method represents the area in which the graph can be plotted, we don't want anything drawn outside of this area, as it wouldn't be within the axis limits, would look odd, and wouldn't be of particular benefit. In your case, you're passing it a list of data points, along with their calculated rendering locations; only data points within the clipping rectangle will be drawn on your plot.
You can see the start of the clipping calculation occuring on line 118 of that source file var clipping = new CohenSutherlandClipping(clippingRectangle); - this is not something I'm particularly familiar with, but a quick wikipedia search shows that it's an algorithm used specifically for working out line clipping, there is it least on other algorithm used elswhere in that source file. I don't believe you need to alter the clipping rectangle, unless the inversion of one of the data points would place it outside the currently drawn region.
As to actually helping arrive at a solution, there are a couple of things I noticed while exploring your code. The first thing I tried was to plot some data points (all positive), and found the entire graph was inverted, essentially because this statement:
if (lpts[i].Y < threshold) is always true for positive values. That's a result of the Y axis coordinate system starting at the top of the window, and increasing toward the bottom of the window. Since the threshold in my case was 0, when translated to a rendering position on the screen, every positive data point's Y position will be smaller than the axis Y value; essentially your logic as to which points are flipped or not, requires inverting. This should get you the behaviour you're after (ensuring the flipped points are calculated correctly.)
Alternative Approach
Rather than get too deep into the clipping rectangle / calculating the transformed data points approach, I went for a slightly lazier route, which could benefit from some tidyup, but may be usefulm depending on your requirements.
I decided to perform the threshold flipping/amendment just before the call to actually render the points is made.
I altered your AbsoluteStairStepSeries class with these changes (To the Render method) in a minimal way, retaining most of your existing structure:
public override void Render(IRenderContext rc, PlotModel model)
{
if (this.ActualPoints.Count == 0)
return;
// Set defaults.
this.VerifyAxes();
OxyRect clippingRect = this.GetClippingRect();
double[] dashArray = this.ActualDashArray;
double[] verticalLineDashArray = this.VerticalLineStyle.GetDashArray();
LineStyle lineStyle = this.ActualLineStyle;
double verticalStrokeThickness = double.IsNaN(this.VerticalStrokeThickness) ?
this.StrokeThickness : this.VerticalStrokeThickness;
OxyColor actualColor = this.GetSelectableColor(this.ActualColor);
// Perform the render action.
Action<IList<Tuple<bool, ScreenPoint>>, IList<Tuple<bool, ScreenPoint>>> renderPoints = (lpts, mpts) =>
{
// Clip the line segments with the clipping rectangle.
if (this.StrokeThickness > 0 && lineStyle != LineStyle.None)
{
if (!verticalStrokeThickness.Equals(this.StrokeThickness) ||
this.VerticalLineStyle != lineStyle)
{
// TODO: change to array
List<ScreenPoint> hlptsOk = new List<ScreenPoint>();
List<ScreenPoint> vlptsOk = new List<ScreenPoint>();
List<ScreenPoint> hlptsFlip = new List<ScreenPoint>();
List<ScreenPoint> vlptsFlip = new List<ScreenPoint>();
double threshold = this.YAxis.Transform(this.Threshold);
for (int i = 0; i + 2 < lpts.Count; i += 2)
{
hlptsOk.Add(lpts[i].Item2);
hlptsOk.Add(lpts[i + 1].Item2);
vlptsOk.Add(lpts[i + 1].Item2);
vlptsOk.Add(lpts[i + 2].Item2);
// Add flipped points so they may be overdrawn.
if (lpts[i].Item1 == true)
{
hlptsFlip.Add(lpts[i].Item2);
hlptsFlip.Add(lpts[i + 1].Item2);
}
}
rc.DrawClippedLineSegments(
clippingRect,
hlptsOk,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
hlptsFlip,
OxyColor.FromRgb(255, 0, 0),
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsOk,
actualColor,
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
rc.DrawClippedLineSegments(
clippingRect,
vlptsFlip,
OxyColor.FromRgb(255, 0, 0),
verticalStrokeThickness,
verticalLineDashArray,
this.LineJoin,
false);
}
else
{
rc.DrawClippedLine(
clippingRect,
lpts.Select(x => x.Item2).ToList(),
0,
actualColor,
this.StrokeThickness,
dashArray,
this.LineJoin,
false);
}
}
if (this.MarkerType != MarkerType.None)
{
rc.DrawMarkers(
clippingRect,
mpts.Select(x => x.Item2).ToList(),
this.MarkerType,
this.MarkerOutline,
new[] { this.MarkerSize },
this.MarkerFill,
this.MarkerStroke,
this.MarkerStrokeThickness);
}
};
// Transform all points to screen coordinates
// Render the line when invalid points occur.
var linePoints = new List<Tuple<bool, ScreenPoint>>();
var markerPoints = new List<Tuple<bool, ScreenPoint>>();
double previousY = double.NaN;
foreach (var point in this.ActualPoints)
{
var localPoint = point;
bool pointAltered = false;
// Amend/Reflect your points data here:
if (localPoint.Y < Threshold)
{
localPoint.Y = Math.Abs(point.Y);
pointAltered = true;
}
if (!this.IsValidPoint(localPoint))
{
renderPoints(linePoints, markerPoints);
linePoints.Clear();
markerPoints.Clear();
previousY = double.NaN;
continue;
}
var transformedPoint = this.Transform(localPoint);
if (!double.IsNaN(previousY))
{
// Horizontal line from the previous point to the current x-coordinate
linePoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, new ScreenPoint(transformedPoint.X, previousY)));
}
linePoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, transformedPoint));
markerPoints.Add(new Tuple<bool, ScreenPoint>(pointAltered, transformedPoint));
previousY = transformedPoint.Y;
}
renderPoints(linePoints, markerPoints);
if (this.LabelFormatString != null)
{
// Render point labels (not optimized for performance).
this.RenderPointLabels(rc, clippingRect);
}
}
I'm using a List<Tuple<bool, ScreenPoint>>, rather than List<ScreenPoint> to store a bool flag against each point, representing whether or not that point has been altered; you could use a small class to simplify the syntax.
Because you're interacting with the point data directly, you don't need to worry about screen position (reversed Y axis), so conceptually the calculation to take the absolute value is easier to read:
// Amend/Reflect your points data here:
if (localPoint.Y < Threshold)
{
localPoint.Y = Math.Abs(point.Y);
pointAltered = true;
}
I notice that your code has reflect above/reflect below, which is probably logic you would insert here if required, I've gone for Math.Abs which you mentioned was your initial requirement.
When it comes to actually rendering the line, I've left the original code which draws the StepSeries in place, so actually the entire series is drawn in green. I've only added a conditional statement to check for modified/reflected points, if any are found, the relevant plot points are added to your existing lists containing flipped points, which are then drawn in red.
The Tuples make things a little messy in the render method (the addition of Item1/Item2), and you could remove the double drawing of the modified points, but I think the results are what you're after (or can certainly point you in the right direction.
Example Behaviour:

Related

WPF keyboard simulation (sendkeys is for winform)

I want to send a string to textbox using a button click on wpf... Does anyone knows how to use inputmanager because the code below wont work? I use the code to test to send a delete or a letter a to a textbox...
public static class SendKeys
{
/// <summary>
/// Sends the specified key.
/// </summary>
/// <param name="key">The key.</param>
public static void Send(Key key)
{
if (Keyboard.PrimaryDevice != null)
{
if (Keyboard.PrimaryDevice.ActiveSource != null)
{
var e1 = new KeyEventArgs(Keyboard.PrimaryDevice, Keyboard.PrimaryDevice.ActiveSource, 0, Key.Down) {RoutedEvent = Keyboard.KeyDownEvent};
InputManager.Current.ProcessInput(e1);
}
}
}
}

OxyPlot: How to set ticks/decade to 1 on a logarithmic axis

I have a log-log plot with both axes in the range 0.1 to 1000. I want only 1 major tick per decade. So far I have found no way to control the tick spacing, except to set IntervalLength as in this code.
var logxAxis = new LogarithmicAxis
{
Position = AxisPosition.Bottom,
Title = "Resistivity of Approaching Bed (ohm-m)",
IntervalLength = 100,
MajorGridlineStyle = LineStyle.Solid,
MinorGridlineStyle = LineStyle.None,
MinorTickSize = 0.0,
Key = "logx"
};
The default IntervalLength is 60, which gave me 2 ticks/decade. Unfortunately, as I increase the window size of my application the number of major ticks increase. So setting IntervalLength is not an ideal solution. I have looked through the OxyPlot source and found nothing. Is there something I am missing, or perhaps I need to derive my own LogarithmicAxis class?
Edit: I decided to derive my own logarithmic axis and replace the function the generated the ticks.
public override void GetTickValues(out IList<double> majorLabelValues, out IList<double> majorTickValues,
out IList<double> minorTickValues)
{
majorLabelValues = new List<double> { 0.1, 1, 10, 100, 1000 };
majorTickValues = new List<double> { 0.1, 1, 10, 100, 1000 };
minorTickValues = new List<double>();
}
This at least lets me get my application out the door.
After some thought, I decided that hard coding the ticks was not the best idea. I decided to create a logarithmic axis where I could lock in the tick placement, but I could also unlock, and let the built in algorithms of OxyPlot work. You will that I reused some OxyPlot methods that are internal, so I just copied the code into my class and made them private. This problem was a lot easier to solve with access to the code. I am stiil open to hearing other solutions.
public class LockableLogarithmicAxis : LogarithmicAxis
{
#region Properties
public bool IsLocked { get; set; }
public double[] MajorTickPositions { get; set; }
public double[] MinorTickPositions { get; set; }
#endregion
#region Constructor
public LockableLogarithmicAxis()
{
IsLocked = true;
}
#endregion
#region Methods
public override void GetTickValues(out IList<double> majorLabelValues, out IList<double> majorTickValues,
out IList<double> minorTickValues)
{
if (!IsLocked)
{
base.GetTickValues(out majorLabelValues, out majorTickValues, out minorTickValues);
return;
}
if (MajorTickPositions != null && MajorTickPositions.Length > 0)
{
majorTickValues = MajorTickPositions.ToList();
}
else
{
majorTickValues = this.DecadeTicks();
}
if (MinorTickPositions != null && MinorTickPositions.Length > 0)
{
minorTickValues = MinorTickPositions.ToList();
}
else
{
minorTickValues = this.SubdividedDecadeTicks();
}
majorLabelValues = majorTickValues;
}
/// <summary>
/// Calculates ticks of the decades in the axis range with a specified step size.
/// </summary>
/// <param name="step">The step size.</param>
/// <returns>A new IList containing the decade ticks.</returns>
private IList<double> DecadeTicks(double step = 1)
{
return this.PowList(this.LogDecadeTicks(step));
}
/// <summary>
/// Calculates logarithmic ticks of the decades in the axis range with a specified step size.
/// </summary>
/// <param name="step">The step size.</param>
/// <returns>A new IList containing the logarithmic decade ticks.</returns>
private IList<double> LogDecadeTicks(double step = 1)
{
var ret = new List<double>();
if (step > 0)
{
var last = double.NaN;
for (var exponent = Math.Ceiling(this.LogActualMinimum); exponent <= this.LogActualMaximum; exponent += step)
{
if (exponent <= last)
{
break;
}
last = exponent;
if (exponent >= this.LogActualMinimum)
{
ret.Add(exponent);
}
}
}
return ret;
}
/// <summary>
/// Raises all elements of a List to the power of <c>this.Base</c>.
/// </summary>
/// <param name="logInput">The input values.</param>
/// <param name="clip">If true, discards all values that are not in the axis range.</param>
/// <returns>A new IList containing the resulting values.</returns>
private IList<double> PowList(IList<double> logInput, bool clip = false)
{
return
logInput.Where(item => !clip || !(item < this.LogActualMinimum))
.TakeWhile(item => !clip || !(item > this.LogActualMaximum))
.Select(item => Math.Pow(this.Base, item))
.ToList();
}
/// <summary>
/// Calculates ticks of all decades in the axis range and their subdivisions.
/// </summary>
/// <param name="clip">If true (default), the lowest and highest decade are clipped to the axis range.</param>
/// <returns>A new IList containing the decade ticks.</returns>
private IList<double> SubdividedDecadeTicks(bool clip = true)
{
var ret = new List<double>();
for (var exponent = (int)Math.Floor(this.LogActualMinimum); ; exponent++)
{
if (exponent > this.LogActualMaximum)
{
break;
}
var currentDecade = Math.Pow(this.Base, exponent);
for (var mantissa = 1; mantissa < this.Base; mantissa++)
{
var currentValue = currentDecade * mantissa;
if (clip && currentValue < this.ActualMinimum)
{
continue;
}
if (clip && currentValue > this.ActualMaximum)
{
break;
}
ret.Add(currentDecade * mantissa);
}
}
return ret;
}
#endregion
}

WPF Draw custom shapes in canvas

I have an array of lines and all the line are connected, forming a custom shape from it. Also i need to fill it with a color.
A line contains something like this:
"StartX":800.0,
"StartY":600.0,
"EndX":0.0,
"EndY":800.0,
"Radius":800.0
I saw that you can build a polygon from points but in my case i need some curved lines cause i have a radius for them.
<Polygon Points="50, 100 200, 100 200, 200 300, 30"
Stroke="Black" StrokeThickness="4"
Fill="Yellow" />
How can i draw something custom in a canvas?
I forgot to mention that i will build this shape programmatically, not from xaml.
Here is an example of lines array and the expected result:
"$values":[
{
"StartX":0.0,
"StartY":0.0,
"EndX":800.0,
"EndY":0.0,
"Radius":0.0
},
{
"StartX":800.0,
"StartY":0.0,
"EndX":800.0,
"EndY":400.0,
"Radius":0.0
},
{
"StartX":800.0,
"StartY":400.0,
"EndX":700.0,
"EndY":400.0,
"Radius":0.0
},
{
"StartX":700.0,
"StartY":400.0,
"EndX":372.727272727273,
"EndY":497.115756874933,
"Radius":600.0
},
{
"StartX":372.727272727273,
"StartY":497.115756874933,
"EndX":100.0,
"EndY":578.045554270711,
"Radius":500.0
},
{
"StartX":100.0,
"StartY":578.045554270711,
"EndX":0.0,
"EndY":578.045554270711,
"Radius":0.0
},
{
"StartX":0.0,
"StartY":578.045554270711,
"EndX":0.0,
"EndY":0.0,
"Radius":0.0
}
]
and the image is:
You should use Path control and adapt it to your needs.
Drawing wpf
Path drawing
Got from CodeProject Code project article:
public class RoundedCornersPolygon : Shape
{
private readonly Path _path;
#region Properties
private PointCollection _points;
/// <summary>
/// Gets or sets a collection that contains the points of the polygon.
/// </summary>
public PointCollection Points
{
get { return _points; }
set
{
_points = value;
RedrawShape();
}
}
private bool _isClosed;
/// <summary>
/// Gets or sets a value that specifies if the polygon will be closed or not.
/// </summary>
public bool IsClosed
{
get
{
return _isClosed;
}
set
{
_isClosed = value;
RedrawShape();
}
}
private bool _useRoundnessPercentage;
/// <summary>
/// Gets or sets a value that specifies if the ArcRoundness property value will be used as a percentage of the connecting segment or not.
/// </summary>
public bool UseRoundnessPercentage
{
get
{
return _useRoundnessPercentage;
}
set
{
_useRoundnessPercentage = value;
RedrawShape();
}
}
private double _arcRoundness;
/// <summary>
/// Gets or sets a value that specifies the arc roundness.
/// </summary>
public double ArcRoundness
{
get
{
return _arcRoundness;
}
set
{
_arcRoundness = value;
RedrawShape();
}
}
public Geometry Data
{
get
{
return _path.Data;
}
}
#endregion
public RoundedCornersPolygon()
{
var geometry = new PathGeometry();
geometry.Figures.Add(new PathFigure());
_path = new Path {Data = geometry};
Points = new PointCollection();
Points.Changed += Points_Changed;
}
private void Points_Changed(object sender, EventArgs e)
{
RedrawShape();
}
#region Implementation of Shape
protected override Geometry DefiningGeometry
{
get
{
return _path.Data;
}
}
#endregion
#region Private Methods
/// <summary>
/// Redraws the entire shape.
/// </summary>
private void RedrawShape()
{
var pathGeometry = _path.Data as PathGeometry;
if (pathGeometry == null) return;
var pathFigure = pathGeometry.Figures[0];
pathFigure.Segments.Clear();
for (int counter = 0; counter < Points.Count; counter++)
{
switch (counter)
{
case 0:
AddPointToPath(Points[counter], null, null);
break;
case 1:
AddPointToPath(Points[counter], Points[counter - 1], null);
break;
default:
AddPointToPath(Points[counter], Points[counter - 1], Points[counter - 2]);
break;
}
}
if (IsClosed)
CloseFigure(pathFigure);
}
/// <summary>
/// Adds a point to the shape
/// </summary>
/// <param name="currentPoint">The current point added</param>
/// <param name="prevPoint">Previous point</param>
/// <param name="prevPrevPoint">The point before the previous point</param>
private void AddPointToPath(Point currentPoint, Point? prevPoint, Point? prevPrevPoint)
{
if (Points.Count == 0)
return;
var pathGeometry = _path.Data as PathGeometry;
if(pathGeometry == null) return;
var pathFigure = pathGeometry.Figures[0];
//the first point of a polygon
if (prevPoint == null)
{
pathFigure.StartPoint = currentPoint;
}
//second point of the polygon, only a line will be drawn
else if (prevPrevPoint == null)
{
var lines = new LineSegment { Point = currentPoint };
pathFigure.Segments.Add(lines);
}
//third point and above
else
{
ConnectLinePoints(pathFigure, prevPrevPoint.Value, prevPoint.Value, currentPoint, ArcRoundness, UseRoundnessPercentage);
}
}
/// <summary>
/// Adds the segments necessary to close the shape
/// </summary>
/// <param name="pathFigure"></param>
private void CloseFigure(PathFigure pathFigure)
{
//No need to visually close the figure if we don't have at least 3 points.
if (Points.Count < 3)
return;
Point backPoint, nextPoint;
if (UseRoundnessPercentage)
{
backPoint = GetPointAtDistancePercent(Points[Points.Count - 1], Points[0], ArcRoundness, false);
nextPoint = GetPointAtDistancePercent(Points[0], Points[1], ArcRoundness, true);
}
else
{
backPoint = GetPointAtDistance(Points[Points.Count - 1], Points[0], ArcRoundness, false);
nextPoint = GetPointAtDistance(Points[0], Points[1], ArcRoundness, true);
}
ConnectLinePoints(pathFigure, Points[Points.Count - 2], Points[Points.Count - 1], backPoint, ArcRoundness, UseRoundnessPercentage);
var line2 = new QuadraticBezierSegment { Point1 = Points[0], Point2 = nextPoint };
pathFigure.Segments.Add(line2);
pathFigure.StartPoint = nextPoint;
}
/// <summary>
/// Method used to connect 2 segments with a common point, defined by 3 points and aplying an arc segment between them
/// </summary>
/// <param name="pathFigure"></param>
/// <param name="p1">First point, of the first segment</param>
/// <param name="p2">Second point, the common point</param>
/// <param name="p3">Third point, the second point of the second segment</param>
/// <param name="roundness">The roundness of the arc</param>
/// <param name="usePercentage">A value that indicates if the roundness of the arc will be used as a percentage or not</param>
private static void ConnectLinePoints(PathFigure pathFigure, Point p1, Point p2, Point p3, double roundness, bool usePercentage)
{
//The point on the first segment where the curve will start.
Point backPoint;
//The point on the second segment where the curve will end.
Point nextPoint;
if (usePercentage)
{
backPoint = GetPointAtDistancePercent(p1, p2, roundness, false);
nextPoint = GetPointAtDistancePercent(p2, p3, roundness, true);
}
else
{
backPoint = GetPointAtDistance(p1, p2, roundness, false);
nextPoint = GetPointAtDistance(p2, p3, roundness, true);
}
int lastSegmentIndex = pathFigure.Segments.Count - 1;
//Set the ending point of the first segment.
((LineSegment)(pathFigure.Segments[lastSegmentIndex])).Point = backPoint;
//Create and add the curve.
var curve = new QuadraticBezierSegment { Point1 = p2, Point2 = nextPoint };
pathFigure.Segments.Add(curve);
//Create and add the new segment.
var line = new LineSegment { Point = p3 };
pathFigure.Segments.Add(line);
}
/// <summary>
/// Gets a point on a segment, defined by two points, at a given distance.
/// </summary>
/// <param name="p1">First point of the segment</param>
/// <param name="p2">Second point of the segment</param>
/// <param name="distancePercent">Distance percent to the point</param>
/// <param name="firstPoint">A value that indicates if the distance is calculated by the first or the second point</param>
/// <returns></returns>
private static Point GetPointAtDistancePercent(Point p1, Point p2, double distancePercent, bool firstPoint)
{
double rap = firstPoint ? distancePercent / 100 : (100 - distancePercent) / 100;
return new Point(p1.X + (rap * (p2.X - p1.X)), p1.Y + (rap * (p2.Y - p1.Y)));
}
/// <summary>
/// Gets a point on a segment, defined by two points, at a given distance.
/// </summary>
/// <param name="p1">First point of the segment</param>
/// <param name="p2">Second point of the segment</param>
/// <param name="distance">Distance to the point</param>
/// <param name="firstPoint">A value that indicates if the distance is calculated by the first or the second point</param>
/// <returns>The point calculated.</returns>
private static Point GetPointAtDistance(Point p1, Point p2, double distance, bool firstPoint)
{
double segmentLength = Math.Sqrt(Math.Pow((p2.X - p1.X), 2) + Math.Pow((p2.Y - p1.Y), 2));
//The distance cannot be greater than half of the length of the segment
if (distance > (segmentLength / 2))
distance = segmentLength / 2;
double rap = firstPoint ? distance / segmentLength : (segmentLength - distance) / segmentLength;
return new Point(p1.X + (rap * (p2.X - p1.X)), p1.Y + (rap * (p2.Y - p1.Y)));
}
#endregion
}
Use this in your View like this:
... xmlns:CustomRoundedCornersPolygon="clr-namespace:CustomRoundedCornersPolygon" />
<CustomRoundedCornersPolygon:RoundedCornersPolygon Points="50, 100 200, 100 200, 200 300, 30"
StrokeThickness="1" ArcRoundness="25" UseRoundnessPercentage="False" Stroke="Black" IsClosed="True">
</CustomRoundedCornersPolygon:RoundedCornersPolygon>
I have just tried it and it works. Give it a try.
I was able to implement it using ArcSegment
For this i need a starting point and a PathSegmentCollection with all the ArcSegments.
An arc segment has the end point of the arc, the sweep direction (clockwise or counterClockwise), isLargeArc (if it represents the large arc or the small arc of the circle it describes) and a size (this is the radius on x and y axis because the arc segment is actually an ellipse arc)
So i have something like this:
foreach (var line in contour)
{
pathSegmentCollection.Add(new ArcSegment
{
Size = new Size(line.Radius, line.Radius),
Point = new Point(line.EndX, line.EndY),
IsLargeArc = false,
SweepDirection = line.LineType == LineType.Clockwise ? SweepDirection.Clockwise : SweepDirection.Counterclockwise
});
}

How determine fault StoryBoard

The initial problem is enough known - "Cannot animate '(0).(1)' on an immutable object instance".
There are many questions here in SO about it but all the solutions are more fixes or crutches. And most of questions are linked to concrete part of code.
Also there are few topics about this problem with possible causes:
https://wpftutorial.net/DebuggingAnimations.html
https://blogs.msdn.microsoft.com/mikehillberg/2006/09/25/tip-cannot-animate-on-an-immutable-object-instance/
I have huge corporate app where a have hundreds styles and storyboards. I can't disable them step by step and it's painstaking work to looking for problem part of code.
I look at these bug not from side of looking for in many xamls but from side of loging. I tried to research info in InvalidOperationException that is raised but there is no useful info like control place in xaml or smth else.
Also one idea is to create class inherited from Storyboard and to override methods.
But there is no methods to override.
Can someone propose how to log the internality of storyboard or other class that is responsible of animation?
At last I found sulution.
You should add classes: listener to animation, AttachedProperty and custom StoryBoard.
public static class TriggerTracing
{
static TriggerTracing()
{
// Initialise WPF Animation tracing and add a TriggerTraceListener
PresentationTraceSources.Refresh();
PresentationTraceSources.AnimationSource.Listeners.Clear();
PresentationTraceSources.AnimationSource.Listeners.Add(new TriggerTraceListener());
PresentationTraceSources.AnimationSource.Switch.Level = SourceLevels.All;
}
#region TriggerName attached property
/// <summary>
/// Gets the trigger name for the specified trigger. This will be used
/// to identify the trigger in the debug output.
/// </summary>
/// <param name="trigger">The trigger.</param>
/// <returns></returns>
public static string GetTriggerName(TriggerBase trigger)
{
return (string)trigger.GetValue(TriggerNameProperty);
}
/// <summary>
/// Sets the trigger name for the specified trigger. This will be used
/// to identify the trigger in the debug output.
/// </summary>
/// <param name="trigger">The trigger.</param>
/// <returns></returns>
public static void SetTriggerName(TriggerBase trigger, string value)
{
trigger.SetValue(TriggerNameProperty, value);
}
public static readonly DependencyProperty TriggerNameProperty =
DependencyProperty.RegisterAttached(
"TriggerName",
typeof(string),
typeof(TriggerTracing),
new UIPropertyMetadata(string.Empty));
#endregion
#region TraceEnabled attached property
/// <summary>
/// Gets a value indication whether trace is enabled for the specified trigger.
/// </summary>
/// <param name="trigger">The trigger.</param>
/// <returns></returns>
public static bool GetTraceEnabled(TriggerBase trigger)
{
return (bool)trigger.GetValue(TraceEnabledProperty);
}
/// <summary>
/// Sets a value specifying whether trace is enabled for the specified trigger
/// </summary>
/// <param name="trigger"></param>
/// <param name="value"></param>
public static void SetTraceEnabled(TriggerBase trigger, bool value)
{
trigger.SetValue(TraceEnabledProperty, value);
}
public static readonly DependencyProperty TraceEnabledProperty =
DependencyProperty.RegisterAttached(
"TraceEnabled",
typeof(bool),
typeof(TriggerTracing),
new UIPropertyMetadata(false, OnTraceEnabledChanged));
private static void OnTraceEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var triggerBase = d as EventTrigger;
if (triggerBase == null)
return;
if (!(e.NewValue is bool))
return;
if ((bool)e.NewValue)
{
// insert dummy story-boards which can later be traced using WPF animation tracing
var storyboard = new TriggerTraceStoryboard(triggerBase, TriggerTraceStoryboardType.Enter);
triggerBase.Actions.Insert(0, new BeginStoryboard() { Storyboard = storyboard });
//storyboard = new TriggerTraceStoryboard(triggerBase, TriggerTraceStoryboardType.Exit);
//triggerBase.ExitActions.Insert(0, new BeginStoryboard() { Storyboard = storyboard });
}
else
{
// remove the dummy storyboards
//foreach (TriggerActionCollection actionCollection in new[] { triggerBase.EnterActions, triggerBase.ExitActions })
foreach (TriggerActionCollection actionCollection in new[] { triggerBase.Actions })
{
foreach (TriggerAction triggerAction in actionCollection)
{
BeginStoryboard bsb = triggerAction as BeginStoryboard;
if (bsb != null && bsb.Storyboard != null && bsb.Storyboard is TriggerTraceStoryboard)
{
actionCollection.Remove(bsb);
break;
}
}
}
}
}
#endregion
private enum TriggerTraceStoryboardType
{
Enter, Exit
}
/// <summary>
/// A dummy storyboard for tracing purposes
/// </summary>
private class TriggerTraceStoryboard : Storyboard
{
public TriggerTraceStoryboardType StoryboardType { get; private set; }
public TriggerBase TriggerBase { get; private set; }
public TriggerTraceStoryboard(TriggerBase triggerBase, TriggerTraceStoryboardType storyboardType)
{
TriggerBase = triggerBase;
StoryboardType = storyboardType;
}
}
/// <summary>
/// A custom tracelistener.
/// </summary>
private class TriggerTraceListener : TraceListener
{
public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
{
base.TraceEvent(eventCache, source, eventType, id, format, args);
if (format.StartsWith("Storyboard has begun;"))
{
TriggerTraceStoryboard storyboard = args[1] as TriggerTraceStoryboard;
if (storyboard != null)
{
// add a breakpoint here to see when your trigger has been
// entered or exited
// the element being acted upon
object targetElement = args[5];
// the namescope of the element being acted upon
INameScope namescope = (INameScope)args[7];
TriggerBase triggerBase = storyboard.TriggerBase;
string triggerName = GetTriggerName(storyboard.TriggerBase);
var str = "";
var element = targetElement as DependencyObject;
while (element != null)
{
str += element.ToString() + Environment.NewLine;
element = VisualTreeHelper.GetParent(element);
}
LoggingInfrastructure.DefaultLogger.Log(...);
}
}
}
public override void Write(string message)
{
}
public override void WriteLine(string message)
{
}
}
Then you could add property to xaml where you need:
<EventTrigger Ui:TriggerTracing.TriggerName="CopyTextBlockStyle PreviewMouseLeftButtonDown"
Ui:TriggerTracing.TraceEnabled="True" RoutedEvent="PreviewMouseLeftButtonDown">

Is OnRenderSizeChanged not called properly?

I have a custom canvas that's supposed to fill itself with the maximum amount of Tile objects.
If it's resized to be bigger, it fills the empty space with additional Tiles.
If It's resized to be smaller, it removed the Tiles that are no longer visible.
This only works if I resize very slowly, otherwise it all starts to mess up.
Correct Image http://imageshack.us/a/img585/105/50186779.png
Correct Image http://imageshack.us/a/img267/3179/76648216.png
I must be obviously doing something wrong, the code below is the canvas:
using System;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1.View
{
public class TileCanvas : Canvas
{
#region Fields
private Boolean _firstCreation;
private Double _heightDifference;
private Double _widthDifference;
#endregion // Fields
#region Properties
public static readonly DependencyProperty TileSizeProperty =
DependencyProperty.Register("TileSize", typeof (Int32), typeof (TileCanvas),
new PropertyMetadata(30));
public Int32 TileSize
{
get { return (Int32) GetValue(TileSizeProperty); }
set { SetValue(TileSizeProperty, value); }
}
public Int32 Columns { get; private set; }
public Int32 Rows { get; private set; }
#endregion // Properties
#region Constructors
public TileCanvas()
{
_firstCreation = true;
}
#endregion // Constructors
#region Methods
#region Public
#endregion // Methods - Public
#region Protected
/// <summary>
/// Gets called when the rendering size of this control changes.
/// </summary>
/// <param name="sizeInfo">Information on the size change.</param>
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
Console.WriteLine("{0}x{1}", sizeInfo.NewSize.Width, sizeInfo.NewSize.Height);
_widthDifference += sizeInfo.NewSize.Width - sizeInfo.PreviousSize.Width;
_heightDifference += sizeInfo.NewSize.Height - sizeInfo.PreviousSize.Height;
int columnDifference = 0;
int rowDifference = 0;
if (_widthDifference > TileSize || _widthDifference < TileSize*-1)
{
columnDifference = ((Int32) _widthDifference)/TileSize;
Columns += columnDifference;
_widthDifference = 0;
}
if (_heightDifference > TileSize || _heightDifference < TileSize*-1)
{
rowDifference = ((Int32) _heightDifference)/TileSize;
Rows += rowDifference;
_heightDifference = 0;
}
UpdateTileSet(columnDifference, rowDifference);
}
#endregion // Methods - Protected
#region Private
/// <summary>
/// Updates the number of tiles if the control changed in size.
/// </summary>
private void UpdateTileSet(Int32 columnChange, Int32 rowChange)
{
// Exit if there's no practical change
if (columnChange == 0 && rowChange == 0)
{
return;
}
// Delete all tiles that fall out on vertical resize
if (rowChange < 0)
{
for (var row = Rows; row > Rows + rowChange; row--)
{
for (var column = 0; column < Columns; column++)
{
Children.RemoveRange(GetListIndex(0, Rows), Columns);
}
}
}
// Delete all tiles that fall out on horizontal resize
if (columnChange < 0)
{
for (var column = Columns; column > Columns + columnChange; column--)
{
for (var row = 0; row < Rows; row++)
{
Children.RemoveAt(GetListIndex(column, row));
}
}
}
// Fill new rows with tiles on vertical resize
if (rowChange > 0)
{
for (var row = Rows - rowChange; row < Rows; row++)
{
for (var column = 0; column < Columns; column++)
{
var tile = new Tile(column, row);
Point position = GetCanvasPosition(column, row);
var index = GetListIndex(column, row);
SetLeft(tile, position.X);
SetTop(tile, position.Y);
Children.Insert(index, tile);
}
}
}
// The first population is a special case that can be handled
// by filling rows only.
if (_firstCreation)
{
_firstCreation = false;
return;
}
// Fill new columns with tiles on horizontal resize
if (columnChange > 0)
{
for (var column = Columns - columnChange; column < Columns; column++)
{
for (var row = 0; row < Rows; row++)
{
var tile = new Tile(column, row);
Point position = GetCanvasPosition(column, row);
var index = GetListIndex(column, row);
SetLeft(tile, position.X);
SetTop(tile, position.Y);
Children.Insert(index, tile);
}
}
}
}
/// <summary>
/// Returns the index a tile should occupy based on its column and row.
/// </summary>
/// <param name="column">The column in which this tile resides.</param>
/// <param name="row">The row in which this tile resides.</param>
/// <returns></returns>
private Int32 GetListIndex(Int32 column, Int32 row)
{
return row*Columns + column;
}
/// <summary>
/// Returns the coordinates of a specific position.
/// </summary>
/// <param name="column">The column the position is in.</param>
/// <param name="row">The row the position is in.</param>
/// <returns></returns>
private Point GetCanvasPosition(Int32 column, Int32 row)
{
var positionX = column*TileSize;
var positionY = row*TileSize;
return new Point(positionX, positionY);
}
#endregion // Methods - Private
#endregion // Methods
}
}
This is Tile.cs
using System;
using System.Windows.Controls;
namespace WpfApplication1.View
{
/// <summary>
/// Interaction logic for Tile.xaml
/// </summary>
public partial class Tile : Border
{
public Tile(Int32 column, Int32 row)
{
InitializeComponent();
Textfield.Text = String.Format("{0}x{1}", column, row);
}
}
}
and Tile.xaml
<Border x:Class="WpfApplication1.View.Tile"
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"
Width="40" Height="40">
<TextBlock x:Name="Textfield" FontSize="8" HorizontalAlignment="Center" VerticalAlignment="Top"/>
</Border>
Where's the issue?
Here's a good source of information about how to place elements at custom positions in a WPF control: http://codeblitz.wordpress.com/2009/03/20/wpf-auto-arrange-animated-panel/
I don't know if it applies to canvas, but placing your code inside the Measure/Arrange Override methods might yield better results.

Resources