DrawingContext.DrawGeometry draws adjacent geometries with gap - c#

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.

Related

WPF Application - Not able to change rectangle properties on MouseDown

I have a simple WPF application which is a window application. There is a Canvas on this window. What I want to do is when I move the mouse on the Canvas It should draw a rectangle on the canvas and next when I press the left mouse button the color of the rectangle should get changed. I am perfectly able to draw a rectangle on mouse move event and also receiving MouseDown event on Rectangle but when I am trying to change the color of this rectangle It is not working. The code is very simple
Here is my xaml file
<Window x:Class="WpfApp1.MainWindow"
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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Canvas Background="#11FFFFFF" IsHitTestVisible="True" x:Name="overlay" Opacity="1">
</Canvas>
</Grid>
</Window>
and here is my xaml.cs file
namespace WpfApp1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
overlay.MouseMove += OnOverlayMouseMove;
}
private void OnOverlayMouseMove(object sender, MouseEventArgs args)
{
overlay.Children.Clear();
Point ps = args.GetPosition(overlay);
Rectangle rect = new Rectangle
{
Fill = Brushes.LightBlue,
Stroke = Brushes.LightGray,
StrokeThickness = 2,
Width = 100,
Height = 50
};
rect.Opacity = 0.5;
rect.MouseLeftButtonDown += OnRectLeftMouseButtonDown;
rect.Name = "Blue";
Canvas.SetLeft(rect, ps.X - 50);
Canvas.SetTop(rect, ps.Y - 25);
overlay.Children.Add(rect);
}
private void OnRectLeftMouseButtonDown(object sender, MouseEventArgs args)
{
Rectangle rect = sender as Rectangle;
if (rect.Name.Equals("Blue"))
{
rect.Fill = Brushes.Black;
rect.Name = "Black";
}
else
{
rect.Fill = Brushes.LightBlue;
rect.Name = "Blue";
}
args.Handled = true;
}
}
}
The color is not changing because you are creating a new rectangle every time OnOverlayMouseMove is called and setting Fill to LightBlue
You can do something like the following,
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
overlay.MouseMove += OnOverlayMouseMove;
}
private void OnOverlayMouseMove(object sender, MouseEventArgs args)
{
overlay.Children.Clear();
Point ps = args.GetPosition(overlay);
Rectangle rect = new Rectangle
{
Fill = brush,
Stroke = Brushes.LightGray,
StrokeThickness = 2,
Width = 100,
Height = 50
};
rect.Opacity = 0.5;
rect.MouseLeftButtonDown += OnRectLeftMouseButtonDown;
rect.Name = "Blue";
Canvas.SetLeft(rect, ps.X - 50);
Canvas.SetTop(rect, ps.Y - 25);
overlay.Children.Add(rect);
}
private SolidColorBrush brush = Brushes.LightBlue;
private void OnRectLeftMouseButtonDown(object sender, MouseEventArgs args)
{
Rectangle rect = sender as Rectangle;
if (brush == Brushes.LightBlue)
{
brush = Brushes.Black;
}
else
{
brush = Brushes.LightBlue;
}
args.Handled = true;
}
}
I think you need something like this:
public partial class MainWindow : Window
{
private Color normal = Color.FromRgb(255, 0, 0);
private Color active = Color.FromRgb(0, 0, 0);
private SolidColorBrush rectangleBrush;
public MainWindow()
{
InitializeComponent();
rectangleBrush = new SolidColorBrush(normal);
overlay.MouseMove += OnOverlayMouseMove;
}
private void OnOverlayMouseMove(object sender, MouseEventArgs args)
{
overlay.Children.Clear();
Point ps = args.GetPosition(overlay);
Rectangle rect = new Rectangle
{
Fill = rectangleBrush,
Stroke = Brushes.LightGray,
StrokeThickness = 2,
Width = 100,
Height = 50
};
rect.Opacity = 0.5;
rect.MouseLeftButtonDown += OnRectLeftMouseButtonDown;
Canvas.SetLeft(rect, ps.X - 50);
Canvas.SetTop(rect, ps.Y - 25);
overlay.Children.Add(rect);
}
private void OnRectLeftMouseButtonDown(object sender, MouseEventArgs args)
{
Rectangle rect = sender as Rectangle;
if ((rect.Fill as SolidColorBrush).Color == normal) {
rectangleBrush.Color = active;
} else {
rectangleBrush.Color = normal;
}
args.Handled = true;
}
}
If you want use colors from Color struct just replace normal and active vars with "Colors.LightBlue" and "Colors.Black"

WPF hit test on custom control does not work

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)
{
}
}

Drawing a PolyLine with DataBinding

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);
}
}

WPF: Bitmap doesn't show

I want to create some kind of simulation. There will be numerous sprites floating around. Because I think that rendering every frame thousand times the same primitives which make up a sprite, will be slow, I want render them once into a bitmap and then show this sprite every frame.
But it doesn't seem to work, the screen stays white.
My WPF source is trivial:
<Window x:Class="WPFGraphicsTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="800" Width="1000">
<Canvas>
</Canvas>
</Window>
And this is my code:
public partial class MainWindow : Window
{
Ellipse e;
RenderTargetBitmap bmp2;
public MainWindow()
{
InitializeComponent();
e = new Ellipse();
e.Width = 40;
e.Height = 40;
e.Fill = new SolidColorBrush(Color.FromRgb(0, 0, 200));
((Canvas)this.Content).Children.Add(e);
((Canvas)this.Content).Measure(new Size(1000, 800));
((Canvas)this.Content).Arrange(new Rect(new Size(1000, 800)));
RenderTargetBitmap bmp2 = new RenderTargetBitmap(40, 40, 96, 96, PixelFormats.Pbgra32);
bmp2.Render(e);
((Canvas)this.Content).Children.Remove(e);
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
drawingContext.DrawImage(bmp2, new Rect(100,100, 40, 40));
}
}
Why doesn't this work?
You could put an Image object on the Canvas and then use the RenderTargetBitmap to update the image. For example
<Window x:Class="WPFGraphicsTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="800" Width="1000">
<Canvas>
<Image Name="frameImage" />
</Canvas>
</Window>
Then you can update the image like this, for the example, I am just rendering a new ellipse every 100 ms. Of course you should manage the Pens and Brushes better than what I show here, this is just an example to clarify the suggestion.
public partial class MainWindow : Window
{
DispatcherTimer _timer = new DispatcherTimer();
RenderTargetBitmap _renderSurface =
new RenderTargetBitmap(100, 100, 96, 96, PixelFormats.Pbgra32);
Random _rnd = new Random();
public MainWindow()
{
InitializeComponent();
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromMilliseconds(100);
_timer.Tick += new EventHandler(_timer_Tick);
_timer.Start();
}
void _timer_Tick(object sender, EventArgs e)
{
DrawingVisual visual = new DrawingVisual();
DrawingContext context = visual.RenderOpen();
int value = _rnd.Next(40);
context.DrawEllipse(
new SolidColorBrush(Colors.Red),
new Pen(new SolidColorBrush(Colors.Black), 1),
new Point(value, value), value, value);
context.Close();
_renderSurface.Render(visual);
frameImage.Source = _renderSurface;
}
}

aliasing problem

the code below draws two vertical lines on a canvas. these lines appear to be of different thickness on the screen although they are the same in code. i am tying to find a way to make them look as sharp as the border around the canvas. setting Path.SnapsToDevicePixels does not have any effect. The code is a contrived example, and in general the canvas that plots these lines can be nested deeper inside the visual tree.
thanks for any help
konstantin
<Window x:Class="wpfapp.MyWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Border BorderBrush="Black"
BorderThickness="1"
Margin="10">
<Canvas x:Name="Canvas"
SizeChanged="OnCanvasSizeChanged" />
</Border>
</Grid>
</Window>
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;
namespace wpfapp
{
public partial class MyWindow : Window
{
public MyWindow()
{
InitializeComponent();
}
private void OnCanvasSizeChanged(object sender, SizeChangedEventArgs e)
{
StreamGeometry g = new StreamGeometry();
double h = this.Canvas.ActualHeight;
using (StreamGeometryContext c = g.Open())
{
c.BeginFigure(new Point(7, 0), false, false);
c.LineTo(new Point(7, h), true, false);
c.BeginFigure(new Point(14, 0), false, false);
c.LineTo(new Point(14, h), true, false);
}
g.Freeze();
Path p = new Path();
p.Data = g;
p.SnapsToDevicePixels = true;
p.Stroke = new SolidColorBrush(Colors.Black);
p.StrokeThickness = 1;
this.Canvas.Children.Clear();
this.Canvas.Children.Add(p);
}
}
}
need to use GuidelineSet:
protected override void OnRender(DrawingContext c)
{
base.OnRender(c);
Pen pen = new Pen(Brushes.Black, 1);
double h = this.ActualHeight;
double d = pen.Thickness / 2;
foreach (double x in new double[] { 7, 14 })
{
GuidelineSet g = new GuidelineSet(new double[] { x + d },
new double[] { 0 + d, h + d });
c.PushGuidelineSet(g);
c.DrawLine(pen, new Point(x, 0), new Point(x, h));
c.Pop();
}
}

Categories