How to model a scrolling line graph in WPF? - wpf

I'm trying to re-learn WPF and I've started a little project that has a line graph, kind of like the CPU performance one you would see in Taskmanager. The graph is basically a canvas that has a punch of lines added to it's children and is based on an Avalon sample I found here http://msdn.microsoft.com/en-us/library/aa480159.aspx
Currently, when I get to the far right edge of the graph I clear my data set and reset my x-coordinate back to zero and the graph starts redrawing from the left hand side after a clear.
y = value;
x = x + 1;
if (x == samples)
{
CpuGraphAnimation2d.Children.RemoveRange(0, samples);
x = 0;
}
What I want to do is have the graph 'scroll' to the left as new values arrive, and effectively have all the old values shuffle to the left and drop the left most value.
What would be the most efficient way to achieve this effect in WPF?

You could try creating a StreamGeometry as follows, removing the first element of yourarray before doing the following:
StreamGeometry sg = new StreamGeometry();
using (StreamGeometryContext context = sg.Open())
{
context.BeginFigure(new Point(0, yourarray[0]), false, false);
for (int x = 0; x < len; x++)
{
context.LineTo(new Point(x, yourarray[x], true, true);
}
sg.Freeze();
System.Windows.Shapes.Path path = new System.Windows.Shapes.Path();
path.Data = sg;
path.Stroke = Brushes.Red;
}
which is a fast way of regenerating your graph each pass. the path created at the end of the code would need to be added to a control or panel in your window.

Related

Implementing stroke drawing similar to InkCanvas

My problem effectively boils down to accurate mouse movement detection.
I need to create my own implementation of an InkCanvas and have succeeded for the most part, except for drawing strokes accurately.
void OnMouseMove(object sneder, MouseEventArgs e)
{
var position = e.GetPosition(this);
if (!Rect.Contains(position))
return;
var ratio = new Point(Width / PixelDisplay.Size.X, Height / PixelDisplay.Size.Y);
var intPosition = new IntVector(Math2.FloorToInt(position.X / ratio.X), Math2.FloorToInt(position.Y / ratio.Y));
DrawBrush.Draw(intPosition, PixelDisplay);
UpdateStroke(intPosition); // calls CaptureMouse
}
This works. The Bitmap (PixelDisplay) is updated and all is well. However, any kind of quick mouse movement causes large skips in the drawing. I've narrowed down the problem to e.GetPosition(this), which blocks the event long enough to be inaccurate.
There's this question which is long beyond revival, and its answers are unclear or simply don't have a noticeable difference.
After some more testing, the stated solution and similar ideas fail specifically because of e.GetPosition.
I know InkCanvas uses similar methods after looking through the source; detect the device, if it's a mouse, get its position and capture. I see no reason for the same process to not work identically here.
I ended up being able to partially solve this.
var position = e.GetPosition(this);
if (!Rect.Contains(position))
return;
if (DrawBrush == null)
return;
var ratio = new Point(Width / PixelDisplay.Size.X, Height / PixelDisplay.Size.Y);
var intPosition = new IntVector(Math2.FloorToInt(position.X / ratio.X), Math2.FloorToInt(position.Y / ratio.Y));
// Calculate pixel coordinates based on the control height
var lastPoint = CurrentStroke?.Points.LastOrDefault(new IntVector(-1, -1));
// Uses System.Linq to grab the last stroke, if it exists
PixelDisplay.Lock();
// My special locking mechanism, effectively wraps Bitmap.Lock
if (lastPoint != new IntVector(-1, -1)) // Determine if we're in the middle of a stroke
{
var alphaAdd = 1d / new IntVector(intPosition.X - lastPoint.Value.X, intPosition.Y - lastPoint.Value.Y).Magnitude;
// For some interpolation, calculate 1 / distance (magnitude) of the two points.
// Magnitude formula: Math.Sqrt(Math.Pow(X, 2) + Math.Pow(Y, 2));
var alpha = 0d;
var xDiff = intPosition.X - lastPoint.Value.X;
var yDiff = intPosition.Y - lastPoint.Value.Y;
while (alpha < 1d)
{
alpha += alphaAdd;
var adjusted = new IntVector(
Math2.FloorToInt((position.X + (xDiff * alpha)) / ratio.X),
Math2.FloorToInt((position.Y + (yDiff * alpha)) / ratio.Y));
// Inch our way towards the current intPosition
DrawBrush.Draw(adjusted, PixelDisplay); // Draw to the bitmap
UpdateStroke(intPosition);
}
}
DrawBrush.Draw(intPosition, PixelDisplay); // Draw the original point
UpdateStroke(intPosition);
PixelDisplay.Unlock();
This implementation interpolates between the last point and the current one to fill in any gaps. It's not perfect when using a very small brush size for example, but is a solution nonetheless.
Some remarks
IntVector is a lazily implemented Vector2 by me, just using integers instead.
Math2 is a helper class. FloorToInt is short for (int)MathF.Round(...))

WPF, rendering a million polygons, nVidia Quardro K2200

I need to display more than a million polygons on a usercontrol to display CAD inquiry. Because I need to be able to zoom in to view the polygons, they can't be drawn onto a bitmap. So in the OnRender override, I use StreamGeometry, StreamGeometryContext to generate polygon geometry. I was surprised how little WPF takes advantages of the powerful nVidia GPU. On the nVidia Control Panel, Workstation, Manage GPU Utilization, I can monitor how many % the GPU has been tapped. During rendering, it routinely uses less than 30% during the OnRender call. After the end of the OnRender, no GPU has been use and the CPU is still clunking away data in the background trying to display. After the polygons are drawn, it becomes impossible to scroll (the control is in a ScrollViewer), change opacity, etc. These operations do not tap into any of the GPU resources either. My only solution so far is set the usercontrol's CacheMode = new BitmapCache(8).
How can I let WPF uses the GPU in this case?
for (int j = 0; j < Data.Count; j++)
{
StreamGeometry geometry = new StreamGeometry();
geometry.FillRule = FillRule.Nonzero;
using (StreamGeometryContext context = geometry.Open())
{
...
{
Point startPoint = new Point(aPolygon[0].X, aPolygon[0].Y);
List<Point> points = new List<Point>();
//draw polygons
bool isPolygonViewAble = false;
if (viewingRect.Contains(startPoint)) isPolygonViewAble = true;
for (int i = 1; i < aPolygon.Count; i++) //foreach (CADGlobal.aPoint pt in polygon)
{
Point apoint = new Point(aPolygon[i].X, aPolygon[i].Y);
if (viewingRect.Contains(apoint)) isPolygonViewAble = true;
points.Add(apoint);
}
if (isPolygonViewAble)
{
context.BeginFigure(startPoint, true, true);
context.PolyLineTo(points, true, false);
}
}
}
geometry.Freeze();
dc.DrawGeometry(null, pen, geometry); //place null with a brush to fill

Drawing multiple lines with DrawLines and DrawLine produces different result

I am trying to draw multiple lines on a winforms panel using it's graphics object in paint event. I am actually drawing a number of lines joining given points. So, first of all I did this,
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawLines(new Pen(new SolidBrush(Color.Crimson), 3), PointFs.ToArray());
float width = 10;
float height = 10;
var circleBrush = new SolidBrush(Color.Crimson);
foreach (var point in PointFs)
{
float rectangleX = point.X - width / 2;
float rectangleY = point.Y - height / 2;
var r = new RectangleF(rectangleX, rectangleY, width, height);
e.Graphics.FillEllipse(circleBrush, r);
}
}
Which produces a result like the image below,
As you can see lines are drawn with having a little bit of extension at sharp turns, which is not expected. So, I changed the drawlines code to,
var pen = new Pen(new SolidBrush(Color.Crimson), 3);
for (int i = 1; i < PointFs.Count; i++)
{
e.Graphics.DrawLine(pen, PointFs[i - 1], PointFs[i]);
}
And now the drawing works fine.
Can anyone tell the difference between the two approaches?
I have just had the same problem (stumbled upon this question during my research), but I have now found the solution.
The problem is caused by the LineJoin property on the Pen used. This DevX page explains the different LineJoin types (see Figure 1 for illustrations). It seems that Miter is the default type, and that causes the "overshoot" when you have sharp angles.
I solved my problem by setting the LineJoin property to Bevel:
var pen = new Pen(new SolidBrush(Color.Crimson), 3);
pen.LineJoin = Drawing2D.LineJoin.Bevel;
Now DrawLines no longer overshoot the points.

Snapping a SurfaceListBox

I'm looking to create a scrolling surfacelistbox which automatically snaps into a position after a drag is finished so that the center item on the screen is centered itself in the viewport.
I've gotten the center item, but now as usual the way that WPF deals with sizes, screen positions, and offsets has me perplexed.
At the moment I've chosen to subscribe to the SurfaceScrollViewer's ManipulationCompleted event, as that seems to consistently fire after I've finished a scroll gesture (whereas the ScrollChanged event tends to fire early).
void ManipCompleted(object sender, ManipulationCompletedEventArgs e)
{
FocusTaker.Focus(); //reset focus to a dummy element
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < List.Items.Count; i++)
{
SurfaceListBoxItem item = List.ItemContainerGenerator.ContainerFromIndex(i) as SurfaceListBoxItem;
if (ViewportHelper.IsInViewport(item) && (List.Items[i] as string != "Dummy"))
{
FrameworkElement el = item as FrameworkElement;
visibleElements.Add(el);
}
}
int centerItemIdx = visibleElements.Count / 2;
FrameworkElement centerItem = visibleElements[centerItemIdx];
double center = ss.ViewportWidth / 2;
//ss is the SurfaceScrollViewer
Point itemPosition = centerItem.TransformToAncestor(ss).Transform(new Point(0, 0));
double desiredOffset = ss.HorizontalOffset + (center - itemPosition.X);
ss.ScrollToHorizontalOffset(desiredOffset);
centerItem.Focus(); //this also doesn't seem to work, but whatever.
}
The list snaps, but where it snaps seems to be somewhat chaotic. I have a line down the center of the screen, and sometimes it looks right down the middle of the item, but other times it's off to the side or even between items. Can't quite nail it down, but it seems that the first and fourth quartile of the list work well, but the second and third are progressively more off toward the center.
Just looking for some help on how to use positioning in WPF. All of the relativity and the difference between percentage-based coordinates and 'screen-unit' coordinates has me somewhat confused at this point.
After a lot of trial and error I ended up with this:
void ManipCompleted(object sender, ManipulationCompletedEventArgs e)
{
FocusTaker.Focus(); //reset focus
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < List.Items.Count; i++)
{
SurfaceListBoxItem item = List.ItemContainerGenerator.ContainerFromIndex(i) as SurfaceListBoxItem;
if (ViewportHelper.IsInViewport(item))
{
FrameworkElement el = item as FrameworkElement;
visibleElements.Add(el);
}
}
Window window = Window.GetWindow(this);
double center = ss.ViewportWidth / 2;
double closestCenterOffset = double.MaxValue;
FrameworkElement centerItem = visibleElements[0];
foreach (FrameworkElement el in visibleElements)
{
double centerOffset = Math.Abs(el.TransformToAncestor(window).Transform(new Point(0, 0)).X + (el.ActualWidth / 2) - center);
if (centerOffset < closestCenterOffset)
{
closestCenterOffset = centerOffset;
centerItem = el;
}
}
Point itemPosition = centerItem.TransformToAncestor(window).Transform(new Point(0, 0));
double desiredOffset = ss.HorizontalOffset - (center - itemPosition.X) + (centerItem.ActualWidth / 2);
ss.ScrollToHorizontalOffset(desiredOffset);
centerItem.Focus();
}
This block of code effectively determines which visible list element is overlapping the center line of the list and snaps that element to the exact center position. The snapping is a little abrupt, so I'll have to look into some kind of animation, but otherwise I'm fairly happy with it! I'll probably use something from here for animations: http://blogs.msdn.com/b/delay/archive/2009/08/04/scrolling-so-smooth-like-the-butter-on-a-muffin-how-to-animate-the-horizontal-verticaloffset-properties-of-a-scrollviewer.aspx
Edit: Well that didn't take long. I expanded the ScrollViewerOffsetMediator to include HorizontalOffset and then simply created the animation as suggested in the above post. Works like a charm. Hope this helps someone eventually.
Edit2: Here's the full code for SnapList:
SnapList.xaml
SnapList.xaml.cs
Note that I got pretty lazy as this project went on an hard-coded some of it. Some discretion will be needed to determine what you do and don't want from this code. Still, I think this should work pretty well as a starting point for anyone who wants this functionality.
The code has also changed from what I pasted above; I found that using Windows.GetWindow gave bad results when the list was housed in a control that could move. I made it so you can assign a control for your movement to be relative to (recommended that be the control just above your list in the hierarchy). I think a few other things changed as well; I've added a lot of customization options including being able to define a custom focal point for the list.

get data point when user click a line graph using DataVisualization.Charting.Chart

I am using DataVisualization.Charting.Chart (winform), I need to get the data point index when user clicks on a line graph in MouseDown event.
I know there is a HitTest function accepting x & y, but for a line graph, we only need to verify x, if we scan the y (0 to height of graph), it will work, but the performance is too bad.
One way to do this is to enable the cursor
chartArea1.CursorX.IsUserEnabled = true;
chartArea1.CursorX.IsUserSelectionEnabled = true;
// set selection color to transparent so that range selection is not drawn
chartArea1.CursorX.SelectionColor = System.Drawing.Color.Transparent;
and handle the CursorPositionChanged event.
private void chart1_CursorPositionChanged(object sender, CursorEventArgs e)
{
// find a point (this series only has Y values, so using position as index works
// for a series with actual X values, you'd need to Find the closest point
DataPoint pt = chart1.Series[0].Points[(int)Math.Max(e.ChartArea.CursorX.Position - 1, 0)];
// do what is need with the data point
pt.MarkerStyle = MarkerStyle.Square;
}
This obviously assumes a single Series in your ChartArea.
if you are use HitTestResult's ChartElementType.
HitTestResult result = chart.HitTest(e.X, e.Y);
if (result.ChartElementType == ChartElementType.DataPoint)
{
int index = result.PointIndex;
// todo something...
}

Resources