I have a Shape witch draws a Line dependent on a ObservableCollection of Nodes (Angles and Factors) around the center.
The DefiningGeometry override looks like this:
PolyLineSegment curve = new PolyLineSegment(this.Nodes.Select(NodeToPoint), true);
PolyLineSegment bounds = new PolyLineSegment(
new[] { new Point(0, 0), new Point(0, GeometrySize), new Point(GeometrySize, GeometrySize), new Point(GeometrySize, 0) }, false);
PathFigureCollection figures = new PathFigureCollection(new[]
{
new PathFigure(NodeToPoint(this.Nodes[this.Nodes.Count - 1]), new[] { curve }, false),
new PathFigure(new Point(0, 0), new[] { bounds }, false)
});
return new PathGeometry(figures, FillRule.Nonzero, null);
If a Node Factor or the Collection changes i invoke InvalidateVisual.
And here is the Problem if i have some more of this Shapes on a Window which cross each other processor load pops up to 25% (On a QuadCore ofc) if i change Factors frequently.
What is the correct approach to draw a frequently updated LineSegment collection?
Is Shape the right Component to do this. ? Maybe my approach is totally wrong but i am stuck here.
EDIT:
I Updated the Code to the following:
protected override Geometry DefiningGeometry
{
get { return this.curveGeometry ?? EmptyBounds; }
}
And the The PropertyChangedHandler to the following
private void NodePropertyChanged(object sender, PropertyChangedEventArgs e)
{
Node node = sender as Node;
if (node != null && this.indexMapping.ContainsKey(node) && this.indexMapping[node] != -1)
{
this.UpdatePoint(node);
}
}
private void UpdatePoint(Node node)
{
if (!this.Dispatcher.CheckAccess())
this.Dispatcher.BeginInvoke(new Action<Node>(UpdatePoint), node);
else
{
this.curve.Points[this.indexMapping[node]] = NodeToPoint(node);
this.InvalidateVisual();
}
}
as stated in the comments the code does not work if i don't call InvalidateVisual. The Problem remains if i add 5 curves with an overall of 1000 nodes the processor load starts to increase if the lines Cross each other and i change a single value.
I will take a look at some processor sampling and report back.
It should not be necessary to create a new geometry each time the data changes. The following simplified example (without MVVM) shows that the UI is updated by only changing a Point in a PolyLineSegment in a PathGeometry:
<Grid Background="Transparent" MouseDown="Grid_MouseDown">
<Path Stroke="Black" StrokeThickness="2">
<Path.Data>
<PathGeometry x:Name="geometry"/>
</Path.Data>
</Path>
</Grid>
Code behind:
public partial class MainWindow : Window
{
private PolyLineSegment segment;
public MainWindow()
{
InitializeComponent();
segment = new PolyLineSegment();
segment.IsStroked = true;
segment.Points.Add(new Point(100, 100));
segment.Points.Add(new Point(200, 200));
var figure = new PathFigure { StartPoint = new Point(0, 0) };
figure.Segments.Add(segment);
geometry.Figures.Add(figure);
}
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
segment.Points[0] = e.GetPosition((IInputElement)sender);
}
}
Related
I have a mesh system for a MMO and it's uses A* to find paths. Occasionally it fails because I have nodes that are badly placed. To fix this, I made a mesh visualiser. It works OKish - I can see that some nodes are badly placed. But I can't see which nodes.
Here is my code to show the nodes:
foreach (var node in FormMap.Nodes)
{
var x1 = (node.Point.X * sideX);
var y1 = (node.Point.Y * sideY);
var x = x1 - nodeWidth / 2;
var y = y1 - nodeWidth / 2;
var brs = Brushes.Black;
//if (node.Visited)
// brs = Brushes.Red;
if (node == FormMap.StartNode)
brs = Brushes.DarkOrange;
if (node == FormMap.EndNode)
brs = Brushes.Green;
g.FillEllipse(brs, (float)x, (float)y, nodeWidth, nodeWidth);
I know I can redo this and make thousands of small buttons and add events for them but that seems overkill.
Is there any way I can add tooltips to the nodes I am painting on the panel?
Yes, you can show a tooltip for your nodes that you have drawn on the drawing surface. To do so, you need to do the followings:
Implement hit-testing for your node, so you can get the node under the mouse position.
Create a timer and In mouse move event handler of the drawing surface, do hit-testing to find the hot item. If the hot node is not same as the current hot node, you stop the timer, otherwise, if there's a new hot item you start the timer.
In the timer tick event handler, check if there's a hot item, show the tooltip and stop the time.
In the mouse leave event of the drawing surface, stop the timer.
And here is the result, which shows tooltip for some points in a drawing:
The above algorithm, is being used in internal logic of ToolStrip control to show tooltip for the tool strip items (which are not control). So without wasting a lot of windows handle, and using a single parent control and a single tooltip, you can show tooltip for as many nodes as you want.
Code Example - Show Tooltip for some points in a drawing
Here is the drawing surface:
using System.ComponentModel;
using System.Drawing.Drawing2D;
public class DrawingSurface : Control
{
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public List<Node> Nodes { get; }
public DrawingSurface()
{
Nodes = new List<Node>();
ResizeRedraw = true;
DoubleBuffered = true;
toolTip = new ToolTip();
mouseHoverTimer = new System.Windows.Forms.Timer();
mouseHoverTimer.Enabled = false;
mouseHoverTimer.Interval = SystemInformation.MouseHoverTime;
mouseHoverTimer.Tick += mouseHoverTimer_Tick;
}
private void mouseHoverTimer_Tick(object sender, EventArgs e)
{
mouseHoverTimer.Enabled = false;
if (hotNode != null)
{
var p = hotNode.Location;
p.Offset(16, 16);
toolTip.Show(hotNode.Name, this, p, 2000);
}
}
private System.Windows.Forms.Timer mouseHoverTimer;
private ToolTip toolTip;
Node hotNode;
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
var node = Nodes.Where(x => x.HitTest(e.Location)).FirstOrDefault();
if (node != hotNode)
{
mouseHoverTimer.Enabled = false;
toolTip.Hide(this);
}
hotNode = node;
if (node != null)
mouseHoverTimer.Enabled = true;
Invalidate();
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
hotNode = null;
mouseHoverTimer.Enabled = false;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
if (Nodes.Count >= 2)
e.Graphics.DrawLines(Pens.Black,
Nodes.Select(x => x.Location).ToArray());
foreach (var node in Nodes)
node.Draw(e.Graphics, node == hotNode);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (mouseHoverTimer != null)
{
mouseHoverTimer.Enabled = false;
mouseHoverTimer.Dispose();
}
if (toolTip != null)
{
toolTip.Dispose();
}
}
base.Dispose(disposing);
}
}
Here is the node class:
using System.Drawing.Drawing2D;
public class Node
{
int NodeWidth = 16;
Color NodeColor = Color.Blue;
Color HotColor = Color.Red;
public string Name { get; set; }
public Point Location { get; set; }
private GraphicsPath GetShape()
{
GraphicsPath shape = new GraphicsPath();
shape.AddEllipse(Location.X - NodeWidth / 2, Location.Y - NodeWidth / 2,
NodeWidth, NodeWidth);
return shape;
}
public void Draw(Graphics g, bool isHot = false)
{
using (var brush = new SolidBrush(isHot ? HotColor : NodeColor))
using (var shape = GetShape())
{
g.FillPath(brush, shape);
}
}
public bool HitTest(Point p)
{
using (var shape = GetShape())
return shape.IsVisible(p);
}
}
And here is the example form, which has a drawing surface control on it:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
drawingSurface1.Nodes.Add(new Node() {
Name = "Node1", Location = new Point(100, 100) });
drawingSurface1.Nodes.Add(new Node() {
Name = "Node2", Location = new Point(150, 70) });
drawingSurface1.Nodes.Add(new Node() {
Name = "Node3", Location = new Point(170, 140) });
drawingSurface1.Nodes.Add(new Node() {
Name = "Node4", Location = new Point(200, 50) });
drawingSurface1.Nodes.Add(new Node() {
Name = "Node5", Location = new Point(90, 160) });
drawingSurface1.Invalidate();
}
i am creating a custom renderer for Xamarin.Forms Frame control to enable extended functionality like gradient background.
for now i got it to work somehow on iOS but i am working on the android version too.
First i tried to override the SetupLayer method as it is in the default frame renderer see here, and add a sublayer there
The way i got it to work was to override Draw method and add a sublayer to the frame view and set the frame layer background to UIColor.Clear.
The issue that i got is that controls i put inside of frame and also the gradient is somehow faded like there is some layer blending stuff going on. something like .5 opacity.
Any advice how to get the Layer behind (Gradient) fully opaque?
Am i doing it wrong ?
Thanks in advance.
Update : I had removed unnecessary code from sample for not creating confusion, the issue i am facing is understand how layers blending work in iOS, as the top-layers blend with added gradient layer and look more faded than normal frame.
//not working
private void SetupLayer()
{
***
var gl = new CAGradientLayer
{
StartPoint = new CGPoint(0, 0),
EndPoint = new CGPoint(1, 1),
Frame = rect,
Colors = new CGColor[]
{
_gradinetControl.StartColor.ToCGColor(),
_gradinetControl.EndColor.ToCGColor()
},
CornerRadius = _gradinetControl.CornerRadius
};
Layer.BackgroundColor = UIColor.Clear.CGColor;
Layer.InsertSublayer(gl,0);
***
}
*
//working but strange fade blending
public override void Draw(CGRect rect)
{
var gl = new CAGradientLayer
{
StartPoint = new CGPoint(0, 0),
EndPoint = new CGPoint(1, 1),
Frame = rect,
Colors = new CGColor[]
{
_gradinetControl.StartColor.ToCGColor(),
_gradinetControl.EndColor.ToCGColor()
},
CornerRadius = _gradinetControl.CornerRadius
};
NativeView.Layer.BackgroundColor = UIColor.Clear.CGColor;
NativeView.Layer.InsertSublayer(gl,0);
base.Draw(rect);
}
I think exists a plugin for this. You can take a look there:
XFGloss
in XAML
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xfg="clr-namespace:XFGloss;assembly=XFGloss"
x:Class="XFGlossSample.Views.AboutPage"
Title="XFGloss Sample App" Padding="10">
<xfg:ContentPageGloss.BackgroundGradient>
<xfg:Gradient Rotation="150">
<xfg:GradientStep StepColor="White" StepPercentage="0" />
<xfg:GradientStep StepColor="White" StepPercentage=".5" />
<xfg:GradientStep StepColor="#ccd9ff" StepPercentage="1" />
</xfg:Gradient>
</xfg:ContentPageGloss.BackgroundGradient>
...
</ContentPage>
In code
namespace XFGlossSample.Views
{
public class AboutPage : ContentPage
{
public AboutPage()
{
Title = "XFGloss Sample App";
Padding = 10;
// Manually construct a multi-color gradient at an angle of our choosing
var bkgrndGradient = new Gradient()
{
Rotation = 150,
Steps = new GradientStepCollection()
{
new GradientStep(Color.White, 0),
new GradientStep(Color.White, .5),
new GradientStep(Color.FromHex("#ccd9ff"), 1)
}
};
ContentPageGloss.SetBackgroundGradient(this, bkgrndGradient);
Content = { ... }
}
}
}
Image with the issue
Finally i found a solution on a Xamarin forum post. here
It seems iOS render colors differently on layers. the workaround was to remake the cgcolor based on the xamarin forms color.
public class ExtendedFrameRenderer : FrameRenderer
{
public override void Draw(CGRect rect)
{
var gl = new CAGradientLayer
{
StartPoint = new CGPoint(0, 0),
EndPoint = new CGPoint(1, 1),
Frame = rect,
Colors = new CGColor[]
{
//old
//_gradinetControl.StartColor.ToCGColor(),
// _gradinetControl.EndColor.ToCGColor()
//fix
ToCGColor(__gradinetControl.StartColor),
ToCGColor(__gradinetControl.EndColor)
},
CornerRadius = _gradinetControl.CornerRadius
};
NativeView.Layer.BackgroundColor = UIColor.Clear.CGColor;
NativeView.Layer.InsertSublayer(gl,0);
base.Draw(rect);
}
public static CGColor ToCGColor(Color color)
{
return new CGColor(CGColorSpace.CreateSrgb(), new nfloat[] { (float)color.R, (float)color.G, (float)color.B, (float)color.A });
}
}
The following custom control
public class DummyControl : FrameworkElement
{
private Visual visual;
protected override Visual GetVisualChild(int index)
{
return visual;
}
protected override int VisualChildrenCount { get; } = 1;
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
var pt = hitTestParameters.HitPoint;
return new PointHitTestResult(visual, pt);
}
public DummyControl()
{
var dv = new DrawingVisual();
using (var ctx = dv.RenderOpen())
{
var penTransparent = new Pen(Brushes.Transparent, 0);
ctx.DrawRectangle(Brushes.Green, penTransparent, new Rect(0, 0, 1000, 1000));
ctx.DrawLine(new Pen(Brushes.Red, 3), new Point(0, 500), new Point(1000, 500));
ctx.DrawLine(new Pen(Brushes.Red, 3), new Point(500, 0), new Point(500, 1000));
}
var m = new Matrix();
m.Scale(0.5, 0.5);
RenderTransform = new MatrixTransform(m);
//Does work; but only the left top quater enters hit test
//var hv = new HostVisual();
//var vt = new VisualTarget(hv);
//vt.RootVisual = dv;
//visual = hv;
//Never enters hit test
visual = dv;
}
}
The xaml
<Window x:Class="MyNamespace.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyNamespace"
mc:Ignorable="d">
<Border Width="500" Height="500">
<local:DummyControl />
</Border>
</Window>
Display a green area with two red coordinate lines through the center. But its hit testing behavior is not understandable for me.
I put a breakpoint in the method HitTestCore but it never hits.
If I un-comment the code to use HostVisual and VisualTarget instead, it hits but only when the mouse is in the left top quater (indiciaed by the red lines given above)
How could the above being explained and how can I could make it work as expected (enters hit test on full range)?
(Originally, I just wanted to handle mouse events on the custom control. Some existing solutions pointed me to overriding the HitTestCore method. So if you could provide any idea that can let me handle mouse events, I don't have to make HitTestCore method working.)
Update
Clemen's answer is good if I decided to use DrawingVisual. However, when I use HostVisual and VisualTarget it is Not working without overriding HitTestCore, and even I do this, still only the top left quater will receive mouse events.
The original question also includes explainations. Also, the use of HostVisual allows me to run the render (time consuming in my real case) in another thread.
(Let me hightlight the code using HostVisual above)
//Does work; but only the left top quater enters hit test
//var hv = new HostVisual();
//var vt = new VisualTarget(hv);
//vt.RootVisual = dv;
//visual = hv;
Any idea?
UPDATE #2
Clemen's new answer is still not working for my purpose. Yes, all the visual area receives hit test. However, what I wanted is to have the full viewport to receive hit test. Which, in his case, is the blank area as he scaled the full visual to the visual area.
In order to establish a visual tree (and thus make hit testing work by default), you also have to call AddVisualChild. From MSDN:
The AddVisualChild method sets up the parent-child relationship
between two visual objects. This method must be used when you need
greater low-level control over the underlying storage implementation
of visual child objects. VisualCollection can be used as a default
implementation for storing child objects.
Besides that, your control should re-render whenever its size changes:
public class DummyControl : FrameworkElement
{
private readonly DrawingVisual visual = new DrawingVisual();
public DummyControl()
{
AddVisualChild(visual);
}
protected override int VisualChildrenCount
{
get { return 1; }
}
protected override Visual GetVisualChild(int index)
{
return visual;
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
using (var dc = visual.RenderOpen())
{
var width = sizeInfo.NewSize.Width;
var height = sizeInfo.NewSize.Height;
var linePen = new Pen(Brushes.Red, 3);
dc.DrawRectangle(Brushes.Green, null, new Rect(0, 0, width, height));
dc.DrawLine(linePen, new Point(0, height / 2), new Point(width, height / 2));
dc.DrawLine(linePen, new Point(width / 2, 0), new Point(width / 2, height));
}
base.OnRenderSizeChanged(sizeInfo);
}
}
When your control uses a HostVisual and a VisualTarget it would still have to re-render itself when its size changes, and also call AddVisualChild to establish a visual tree.
public class DummyControl : FrameworkElement
{
private readonly DrawingVisual drawingVisual = new DrawingVisual();
private readonly HostVisual hostVisual = new HostVisual();
public DummyControl()
{
var visualTarget = new VisualTarget(hostVisual);
visualTarget.RootVisual = drawingVisual;
AddVisualChild(hostVisual);
}
protected override int VisualChildrenCount
{
get { return 1; }
}
protected override Visual GetVisualChild(int index)
{
return hostVisual;
}
protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParams)
{
return new PointHitTestResult(hostVisual, hitTestParams.HitPoint);
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
using (var dc = drawingVisual.RenderOpen())
{
var width = sizeInfo.NewSize.Width;
var height = sizeInfo.NewSize.Height;
var linePen = new Pen(Brushes.Red, 3);
dc.DrawRectangle(Brushes.Green, null, new Rect(0, 0, width, height));
dc.DrawLine(linePen, new Point(0, height / 2), new Point(width, height / 2));
dc.DrawLine(linePen, new Point(width / 2, 0), new Point(width / 2, height));
}
base.OnRenderSizeChanged(sizeInfo);
}
}
You could now set a RenderTransform and still get correct hit testing:
<Border>
<local:DummyControl MouseDown="DummyControl_MouseDown">
<local:DummyControl.RenderTransform>
<ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
</local:DummyControl.RenderTransform>
</local:DummyControl>
</Border>
This will work for you.
public class DummyControl : FrameworkElement
{
protected override void OnRender(DrawingContext ctx)
{
Pen penTransparent = new Pen(Brushes.Transparent, 0);
ctx.DrawGeometry(Brushes.Green, null, rectGeo);
ctx.DrawGeometry(Brushes.Red, new Pen(Brushes.Red, 3), line1Geo);
ctx.DrawGeometry(Brushes.Red, new Pen(Brushes.Red, 3), line2Geo);
base.OnRender(ctx);
}
RectangleGeometry rectGeo;
LineGeometry line1Geo, line2Geo;
public DummyControl()
{
rectGeo = new RectangleGeometry(new Rect(0, 0, 1000, 1000));
line1Geo = new LineGeometry(new Point(0, 500), new Point(1000, 500));
line2Geo = new LineGeometry(new Point(500, 0), new Point(500, 1000));
this.MouseDown += DummyControl_MouseDown;
}
void DummyControl_MouseDown(object sender, MouseButtonEventArgs e)
{
}
}
I'm in need of a way to make TextBox appear like a parallelogram but i can't figure out how to do so. I currently have this code:
private void IOBox_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Point cursor = PointToClient(Cursor.Position);
Point[] points = { cursor, new Point(cursor.X + 50, cursor.Y), new Point(cursor.X + 30, cursor.Y - 20),
new Point(cursor.X - 20, cursor.Y - 20) };
Pen pen = new Pen(SystemColors.MenuHighlight, 2);
g.DrawLines(pen, points);
}
But apparently it's not working. Either i misplaced/misused it or i'm not doing something right.
This is the method that i use to add it.
int IOCounter = 0;
private void inputOutput_Click(object sender, EventArgs e)
{
IOBox box = new IOBox();
box.Name = "IOBox" + IOCounter;
IOCounter++;
box.Location = PointToClient(Cursor.Position);
this.Controls.Add(box);
}
Any idea how i can fix it? IOBox is a UserControl made by me which contains a TextBox. Is that rightful to do?
If its possible, you should make your application using WPF. WPF is designed to do exactly what you are trying to do.
However, it can be done in WinForms, though not easily. You will need to make a new class that inherits the TextBox WinForm control. Here is an example that makes a TextBox look like a circle:
public class MyTextBox : TextBox
{
public MyTextBox() : base()
{
SetStyle(ControlStyles.UserPaint, true);
Multiline = true;
Width = 130;
Height = 119;
}
public override sealed bool Multiline
{
get { return base.Multiline; }
set { base.Multiline = value; }
}
protected override void OnPaintBackground(PaintEventArgs e)
{
var buttonPath = new System.Drawing.Drawing2D.GraphicsPath();
var newRectangle = ClientRectangle;
newRectangle.Inflate(-10, -10);
e.Graphics.DrawEllipse(System.Drawing.Pens.Black, newRectangle);
newRectangle.Inflate(1, 1);
buttonPath.AddEllipse(newRectangle);
Region = new System.Drawing.Region(buttonPath);
base.OnPaintBackground(e);
}
}
Keep in mind that you will still have to do other things, such as clipping the text, etc. But this should get you started.
Using DrawingContext.DrawingGeometry I'm drawing two triangles with common edge. I want this triangles to be filled, but not stroked with pen, because pen has thickness, and resulting triangles would be half thickness bigger than expected. Using code attached below I'm getting strange result (see picture) - there is a small gap between triangles. What am I doing wrong? Is there some better way, than drawing extra line on common edge?
XAML:
<Window x:Class="LearnDrawing.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LearnDrawing" xmlns:wpfApplication1="clr-namespace:WpfApplication1"
Title="Window1"
Height="500"
Width="500">
<Grid>
<wpfApplication1:DrawIt Width="400" Height="400" />
</Grid>
</Window>
Code:
using System.Windows;
using System.Windows.Media;
namespace WpfApplication1
{
class DrawIt : FrameworkElement
{
VisualCollection visuals;
public DrawIt()
{
visuals = new VisualCollection(this);
this.Loaded += new RoutedEventHandler(DrawIt_Loaded);
}
void DrawIt_Loaded(object sender, RoutedEventArgs e)
{
var visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
var t1 = CreateTriangleGeometry(new Point(0, 0), new Point(200, 0), new Point(0, 200));
var t2 = CreateTriangleGeometry(new Point(200, 0), new Point(200, 200), new Point(0, 200));
dc.DrawGeometry(Brushes.Black, null, t1);
dc.DrawGeometry(Brushes.Black, null, t2);
}
visuals.Add(visual);
}
static PathGeometry CreateTriangleGeometry(Point aPt1, Point aPt2, Point aPt3)
{
var figure = new PathFigure();
figure.StartPoint = aPt1;
figure.Segments.Add(new PolyLineSegment(new []{aPt2, aPt3}, true));
var pg = new PathGeometry();
pg.Figures.Add(figure);
figure.IsClosed = true;
figure.IsFilled = true;
return pg;
}
protected override Visual GetVisualChild(int index)
{
return visuals[index];
}
protected override int VisualChildrenCount
{
get
{
return visuals.Count;
}
}
}
}
Result:
You may set the EdgeMode of your visuals to EdgeMode.Aliased.
public DrawIt()
{
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
...
}
See also the Visual.VisualEdgeMode property.