I am working with a WPF control derived from the Canvas that I am drawing a selection of geometry to, through a process that works along these lines, but is far more complex so although this illustrates the process, it is closer to pseudo-code:
public class LineView: Canvas
{
private List<GeometryLine> lines;
public LineView()
{
lines = new List<GeometryLine>();
}
private PathFigure GetPathFigure(List<Point> line, bool close)
{
var size = line.Count;
var points = new PathSegmentCollection(size);
var first = line.First();
for (int i = 1; i < size; i++)
{
points.Add(new LineSegment(line[i], true));
}
var figure = new PathFigure(first, points, close);
return new PathGeometry(new List<PathFigure>(figure));
}
public DrawingGroup DrawLine(GeometryLine line)
{
var geometry = new GeometryGroup();
geometry.Children.Add(GetPathGeometry(line.Points, false));
var d = new GeometryDrawing();
d.Pen = GetPen();
d.Geometry = geometry;
DrawingGroup group = new DrawingGroup();
group.Append();
group.Children.Add(d);
return group;
}
public override void OnRender(DrawingContext drw)
{
if ( 0 < lines.Count )
{
foreach ( var line in lines )
{
drw.DrawDrawing(DrawLine(line));
}
}
}
public void SetLines(List<GeometryLine> newLines)
{
lines = newLines;
this.InvalidateVisual();
}
}
This all works fine to render geometry content on the canvas. However, when the data changes and SetLines is called with a new set of data, it doesn't always clear the canvas - sometimes it will draw the new set of lines over the old set, other times it will clear the canvas. I can't see any pattern as to when it draws over as opposed to when it clears the canvas.
If I call InvalidateVisual from the Render method it will empty the canvas reliably, but it also forces the context to render again. Past questions on this topic indicate that either this.Children.Clear() or InvalidateVisual are the recommended strategies, but neither of them prevents this overdrawing problem. When I look at the Children collection it is always empty.
What do I need to do in order to ensure it will clear the previous geometry and then draw the updated geometry every time it changes?
Related
I draw chart with DynamicDataDisplay.ChartPlotter and each time I redraw the chart memory increase. So when I have redraw 200 times my chart memory of my app increase to 200mo in memory.
I tried to GC.Collect() manually but nothing change.
private ChartPlotter _courbeChartPlotter;
public ChartPlotter CourbeChartPlotter
{
get { return _courbeChartPlotter; }
set
{
_courbeChartPlotter = value;
RaisePropertyChanged();
}
}
private static ListPointD3Cs _listeX = new ListPointD3Cs { new Point(0, 0) };
private static EnumerableDataSource<Point> dsX = new EnumerableDataSource<Point>(_listeX);
private int nbPoint = 0;
public void TakeMeasureTest()
{
nbPoint++;
_listeX.Add(new Point(nbPoint, GetRandomNumber(-1, 1)));
InitDotsCourbe(out dsX);
TraceCourbe(dsX);
}
public void InitDotsCourbe(out EnumerableDataSource<Point> dsX)
{
dsX = new EnumerableDataSource<Point>(_listeX);
dsX.SetXMapping(x => x.X);
dsX.SetYMapping(y => y.Y);
}
public ChartPlotter TraceCourbe(EnumerableDataSource<Point> dsX)
{
CourbeChartPlotter = new ChartPlotter();
ViewPortAxesRangeRestriction restr = new ViewPortAxesRangeRestriction
{
XRange = new DisplayRange(0, nbPoint + 1),
YRange = new DisplayRange(-2, 2)
};
CourbeChartPlotter.Viewport.Restrictions.Add(restr);
CourbeChartPlotter.AddLineGraph(dsX, Colors.Red, 2, "X");
return CourbeChartPlotter;
}
I want memory not increase each time I redraw the chart or at least when I GC.Collect() after all Measure retrieve a normal memory usage.
I'm assuming you're redrawing the chart by repeatedly calling TraceCourbe method, and reassigning CourbeChartPlotter property, then displaying that. In that case, you probably have a memory leak because something (for example another property, control or an event subscriber) is holding a reference to your ChartPlotter control even after it is reassigned and theoretically removed (your case may be similar to this one).
You can reassign the chart's data source instead, as is described here.
I have created a custom shape that draws an ObservableCollection'Point using a StreamGeometry. When the signal surpasses the width of the canvas it is drawn on, it wraps around to the begining. Two polyLines are used; one from the beginning of the canvas to the last added point and one from the first added point in the current collection till the end of the canvas:
protected override Geometry DefiningGeometry
{
get
{
if (this.Points == null || !this.Points.Any())
{
return Geometry.Empty;
}
var firstGeometryPoints = this.Points.Where(p => p.Point.X >= this.Points.First().Point.X);
var secondGeometryPoints = this.Points.Except(firstGeometryPoints);
var geometry = new StreamGeometry();
using (var context = geometry.Open())
{
context.BeginFigure(firstGeometryPoints.First().Point, true, false);
context.PolyLineTo(firstGeometryPoints.Skip(1).Select(x => x.Point).ToArray(), true, true);
if (secondGeometryPoints.Any())
{
context.BeginFigure(secondGeometryPoints.First().Point, true, false);
context.PolyLineTo(secondGeometryPoints.Skip(1).Select(x => x.Point).ToArray(), true, true);
}
geometry.Freeze();
}
return geometry;
}
}
Whenever the Points collection changes I use InvalidateVisual() to force a redraw of the shape. But since data points are added at 500 samples per second, the application starts to lag. Is there another way of creating this type of graph without redrawing the entire shape on every sample?
I am currently following this tutorial for adding a polygon to a map. I need to be able to add multiple polygons to my map, so I have slightly altered the code so that I can use addOverlays which takes in an array of IMKOverlay objects instead of one addOverlay which just takes in a single IMKOverlay object.
This doesn't work however... It only draws the first polygon on the map!
void addPolygonsToMap()
{
overlayList = new List<IMKOverlay>();
for (int i = 0; i < polygons.Count; i++)
{
CLLocationCoordinate2D[] coords = new CLLocationCoordinate2D[polygons[i].Count];
int index=0;
foreach (var position in polygons[i])
{
coords[index] = new CLLocationCoordinate2D(position.Latitude, position.Longitude);
index++;
}
var blockOverlay = MKPolygon.FromCoordinates(coords);
overlayList.Add(blockOverlay);
}
IMKOverlay[] imko = overlayList.ToArray();
nativeMap.AddOverlays(imko);
}
In this discussion, it would appear that I have to call a new instance of MKPolygonRenderer each time I need to add another polygon to my map, but I'm unsure how this example translates to my code. Here is my MKPolygonRenderer function:
MKOverlayRenderer GetOverlayRenderer(MKMapView mapView, IMKOverlay overlayWrapper)
{
if (polygonRenderer == null && !Equals(overlayWrapper, null)) {
var overlay = Runtime.GetNSObject(overlayWrapper.Handle) as IMKOverlay;
polygonRenderer = new MKPolygonRenderer(overlay as MKPolygon) {
FillColor = UIColor.Red,
StrokeColor = UIColor.Blue,
Alpha = 0.4f,
LineWidth = 9
};
}
return polygonRenderer;
}
Create a new renderer instance each time OverlayRenderer is called, there is no need to cache the renderer in a class level variable as the MKMapView will cache the renderers as needed.
Subclass MKMapViewDelegate:
class MyMapDelegate : MKMapViewDelegate
{
public override MKOverlayRenderer OverlayRenderer(MKMapView mapView, IMKOverlay overlay)
{
switch (overlay)
{
case MKPolygon polygon:
var prenderer = new MKPolygonRenderer(polygon)
{
FillColor = UIColor.Red,
StrokeColor = UIColor.Blue,
Alpha = 0.4f,
LineWidth = 9
};
return prenderer;
default:
throw new Exception($"Not supported: {overlay.GetType()}");
}
}
}
Instance and assign the delegate to your map:
mapDelegate = new MyMapDelegate();
map.Delegate = mapDelegate;
Note: Store the instance of your MyMapDelegate in a class level variable as you do not want to get GC'd
Update:
MKMapView has two steps involved to display an overlay on its map.
1. Calling `AddOverlay` and `AddOverlays`
First you add overlays to the map that conform to IMKOverlay. There are basic built-in types such as MKCircle, MKPolygon, etc... but you can also design your own overlays; i.e. overlays that define the location of severe weather (lightning, storm clouds, tornados, etc..). These MKOverlays describe the geo-location of the item but not how to draw it.
2. Responding to `OverlayRenderer` requests
When the display area of the map intersects with one of the overlays, the map need to draw it on the screen. The map's delegate (your MKMapViewDelegate subclass) is called to supply a MKOverlayRenderer that defines the drawing routines to paint the overlay on the map.
This drawing involves converting the geo-coordinates of the overlay to local display coordinates (helper methods are available) using Core Graphics routines (UIKit can be used with some limitations). There are basic built-in renderers for MKCircleRenderer, MKPolygonRenderer, etc.. that can be used or you can write your own MKOverlayRenderer subclass.
You could supply a custom way to renderer a MKCircle overlay, maybe a target-style red/white multi-ringed bullseye, instead of the way the default circle renderer draws it, or custom renderers that draw severe storm symbols within the bounds of a MKPolygon to match your custom severe storm overlays.
My Example code:
Since you are using MKPolygon to build your overlays, you can use the MKPolygonRenderer to display them. In my example, I provide a pattern matching switch (C# 6) that returns a semi-transparent Red/Blue MKPolygonRenderer for every MKPolygon that you added to the map (if you added a non-MKPolygon based overlay it will throw an exception).
I was also stuck in this issue and I have found the way to create the sub class of MKPolygon.
I have checked it with my example and it works like a charm. But not sure that Apple may reject my app or not.
public class CvPolyon : MKPolygon
{
public CustomObject BoundaryOption { get; }
public CvPolyon1(MKPolygon polygon, CustomObject boundaryOption)
:base(polygon.Handle)
{
BoundaryOption = boundaryOption;
}
}
We can add polygon on map like this.
var polygon = MKPolygon.FromCoordinates(coordinates);
var overlay = new CvPolyon(polygon, new CustomObject());
mapView.AddOverlay(overlay);
We can recognize our polygon in the class which extends MKMapViewDelegate like this.
public override MKOverlayRenderer OverlayRenderer(MKMapView mapView, IMKOverlay overlay)
{
if (overlay is CvPolyon polygon)
{
var polygonRenderer = new MKPolygonRenderer(polygon)
{
FillColor = polygon.BoundaryOption.AreaColor,
StrokeColor = polygon.BoundaryOption.LineColor,
Alpha = polygon.BoundaryOption.Alpha,
LineWidth = polygon.BoundaryOption.LineWidth
};
if (polygon.BoundaryOption.IsDashedLine)
polygonRenderer.LineDashPattern = new[] { new NSNumber(2), new NSNumber(5) };
return polygonRenderer;
}
return mapView.RendererForOverlay(overlay);
}
I want to display a kind of banner everytime the player enters a new location. This should be at the very left border of the screen. My problem is that my current solution behaves inconsistently across resolutions. On higher resolutions the banner is positioned further to the right than needed.
I also use a piece of code to properly position the text every time it is displayed.
public class AreaName : MonoBehaviour
{
private static AreaName instance;
void Start ()
{
if (instance)
Destroy(this);
else
instance = this;
BannerBase = transform.FindChild("BannerBase").GetComponent<Image>();
NameDisplay = BannerBase.transform.FindChild("BannerText").GetComponent<Text>();
}
private Image BannerBase;
private Text NameDisplay;
public static void Display (string areatitle)
{
int width = TextWidth(instance.NameDisplay, areatitle);
instance.BannerBase.rectTransform.position = new Vector3(-305 + width, instance.BannerBase.transform.position.y, 0);
instance.NameDisplay.text = areatitle;
print(width);
}
private static int TextWidth (Text obj, string text)
{
int totalLength = 0;
Font myFont = obj.font; //chatText is my Text component
CharacterInfo characterInfo = new CharacterInfo();
char[] arr = text.ToCharArray();
foreach (char c in arr)
{
myFont.GetCharacterInfo(c, out characterInfo, obj.fontSize);
totalLength += characterInfo.advance;
}
return totalLength;
}
}
Are there any simple errors that I am overseeing? Or am I just understanding the Unity UI system wrong?
You shouldn't need to code nothing to do that.
You have put your banner at the lower left corner of the screen, with a (0,0) pivot, that's all right, but, why is it positioned at x: -320?
If you put your banner at -320 for some reason, at native resolution it probably will work as expected, but if you have activated the Canvas Resizer, at other resolutions it will be moved since is not positioned at the corner at all.
I have a weird problem concerning the Region object.
I created a method which returns the Region defined by an array of Point3D (which is just a 3-float struct) which are converted to PointF.(Face is the face of a 3d cube)
public Region GetRegion(params Point3D[] facePoints)
{
float distance = Camera3DLib.Camera3D.GetDistance();
PointF[] planePoints = new PointF[links.Length];
for (int i = 0; i < planePoints.Length; i++)
{
planePoints[i] = facePoints[i].ToPointF(distance);
}
GraphicsPath pth = new GraphicsPath();
pth.AddPolygon(planePoints);
return new Region(pth);
}
The problem is displaying this region.
When I use FillRegion, the graphics fills everything except the region.
To get the region needed to fill, I use the above method a number of times (Solid3D contains an array of Face3D and an array of vertices, I hope the purpose of the methods that I use are self explanatory by their names)
private static Region GetVisibleRegion(Solid3D s)
{
Region visible = new Region();
for (int i = 0; i < s.GetFaces().Length; i++)
{
Face3D face = s.GetFaces()[i];
Point3D[] facePoints = s.GetFaceVertices(i);
Region region = face.GetRegion(facePoints);
visible.Union(region);
}
return visible;
}
The drawing itself is done in a method called from the Form_paint event:
gr.FillRegion(Brushes.Blue, GetVisibleRegion(arr[0].Key));
gr is the graphics and arr is a dictionary who's key is a Solid3D.
This causes everything except the faces to be added to the Region.
What would be the cause of this?
I thought maybe the object, being passed by pointers between methods, is somehow getting reset (to default values of members) but I'm not so sure that's it.
PS: The code I wrote doesn't throw exceptions, so (I think) it's a logic or usage problem. If there are any questions about parts of the implementation that which aren't posted here (it's a lot of code :P) please ask.
Thanks!