I'm drawing a graph in a WPF application, but lines drawn using drawingContext.DrawLine(...) are drawn to sub-pixel boundaries.
I'm able to get them to look nice by creating Line objects, but I don't want to create tens of thousands of those every time the visual is invalidated.
How can I force them to fit to pixels?
You may draw the lines into a derived DrawingVisual that has the protected VisualEdgeMode property set to EdgeMode.Aliased:
public class MyDrawingVisual : DrawingVisual
{
public MyDrawingVisual()
{
VisualEdgeMode = EdgeMode.Aliased;
}
}
public class DrawingComponent : FrameworkElement
{
private DrawingVisual visual = new MyDrawingVisual();
public DrawingComponent()
{
AddVisualChild(visual);
using (DrawingContext dc = visual.RenderOpen())
{
dc.DrawLine(new Pen(Brushes.Black, 1d), new Point(100, 100), new Point(100, 200));
dc.DrawLine(new Pen(Brushes.Black, 1d), new Point(105.5, 100), new Point(105.5, 200));
dc.DrawLine(new Pen(Brushes.Black, 1d), new Point(112, 100), new Point(112, 200));
}
}
protected override int VisualChildrenCount
{
get { return 1; }
}
protected override Visual GetVisualChild(int index)
{
return visual;
}
}
Strange enough, but calling RenderOptions.SetEdgeMode(visual, EdgeMode.Aliased) on a non-derived DrawingVisual doesn't do the job.
That's great.
Another option (more complicated in that case) is using RenderOptions.SetEdgeMode on a DrawingGroup:
https://stackoverflow.com/a/16984921/2463642
Related
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 have a drawing visual which I have drawings, how do I add this to my canvas and display?
DrawingVisual drawingVisual = new DrawingVisual();
// Retrieve the DrawingContext in order to create new drawing content.
DrawingContext drawingContext = drawingVisual.RenderOpen();
// Create a rectangle and draw it in the DrawingContext.
Rect rect = new Rect(new System.Windows.Point(0, 0), new System.Windows.Size(100, 100));
drawingContext.DrawRectangle(System.Windows.Media.Brushes.Aqua, (System.Windows.Media.Pen)null, rect);
// Persist the drawing content.
drawingContext.Close();
How do I add this to a canvas? Suppose I have a Canvas as
Canvas canvas = null;
canvas.Children.Add(drawingVisual); //Doesnt work as UIElement expected.
How do I add my drawingVisual to canvas?
TIA.
You have to implement a host element class, which would have to override the VisualChildrenCount property and the GetVisualChild() method of a derived UIElement or FrameworkElement to return your DrawingVisual.
The most basic implementation could look like this:
public class VisualHost : UIElement
{
public Visual Visual { get; set; }
protected override int VisualChildrenCount
{
get { return Visual != null ? 1 : 0; }
}
protected override Visual GetVisualChild(int index)
{
return Visual;
}
}
Now you would add a Visual to your Canvas like this:
canvas.Children.Add(new VisualHost { Visual = drawingVisual });
I have a custom drawn control (overrides FrameworkElement) with several DrawingVisual objects. I'm keeping a list of visuals and overriding GetVisualChild and VisualChildrenCount. Performance is important, so most of them use BitmapCache.
One of visuals is going to be updated with new data every 50 ms. It draws a path which a machine takes in real world, so every 50 ms. there's a new line to draw, keeping old lines still there.
What would be the best way to draw this with good performance, so without redrawing existing machine path, but just adding another line? It seems once you draw something in the visual using RenderOpen you can't modify it. I tried visual.Drawing.Append() but it seems it doesn't draw anything.
Is there a way to add new data to DrawingVisual? If not, what to use instead?
Perhaps create a RenderTargetBitmap that you draw to instead, then when the OnRender call is made do a Context.DrawImage
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace WpfApplication13
{
public class PanelTest : FrameworkElement
{
public RenderTargetBitmap _renderTargetBitmap = null;
public System.Windows.Threading.DispatcherTimer _Timer = null;
public int _iYLoc = 0;
private Pen _pen = null;
protected override void OnRender(DrawingContext drawingContext)
{
if (!DesignerProperties.GetIsInDesignMode(this))
{
drawingContext.DrawImage(_renderTargetBitmap, new Rect(0, 0, 250, 250));
}
base.OnRender(drawingContext);
}
public PanelTest() :base()
{
_renderTargetBitmap = new RenderTargetBitmap(250, 250, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
_pen = new Pen(Brushes.Red, 1);
_Timer = new System.Windows.Threading.DispatcherTimer();
_Timer.Interval = new TimeSpan(0, 0, 0, 0, 50);
_Timer.Tick += _Timer_Tick;
if (!DesignerProperties.GetIsInDesignMode(this))
{
_Timer.Start();
}
}
private void _Timer_Tick(object sender, EventArgs e)
{
DrawingVisual vis = new DrawingVisual();
DrawingContext con = vis.RenderOpen();
con.DrawLine(_pen, new Point(0, _iYLoc), new Point(250, _iYLoc));
_iYLoc++;
con.Close();
_renderTargetBitmap.Render(vis);
}
}
}
I need to graph a polygonal on a panel (size 400, 400)
I try this, but it doesn't work.
PointF[] points = new PointF[totalpaso + 1];
for (int d = 0; d <= totalpaso; d++)
{
s = (float)(hola1[d] + 200);
w = (float)(hola2[d] + 200);
j = new PointF(s, w);
points[d] = j;
}
grafico.DrawPolygon(lapiz, points);
I think you should take a look at this post:
https://msdn.microsoft.com/en-us/library/07e699tw(v=vs.110).aspx
But with only that method you won't get far. It'll be drawn in a OnPaint Method:
C# Forms - Using Paint methods?
You will probably want to make your own control based on Panel.
A really basic example is this:
public sealed class MyPanel: Panel
{
public MyPanel()
{
this.ResizeRedraw = true;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (var brush = new SolidBrush(this.ForeColor))
{
e.Graphics.FillEllipse(brush, 0, 0, this.Width, this.Height);
}
}
}
To test it, add the class to a Windows Forms project, compile, and then drop it onto a form and set it to "Dock" in the designer.
Then set the BackColor to, say, red and the ForeColor to blue, then run the program.
You can then add your required drawing code to the OnPaint() override.
I have the following custom Control:
public class Line : Control
{
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
using (var p = new Pen(Color.Black, 3))
{
var point1 = new Point(234, 118);
var point2 = new Point(293, 228);
e.Graphics.DrawLine(p, point1, point2);
}
}
}
And a Form where I add as new control a new instance of the Line class:
Controls.Add(new Line());
The problem is that the method OnPaint isn't called and no line is drawn. Why? How can i fix it?
Your are not giving it a Size, try creating a Constructor and setting a default size there, you also seem to be using the parent controls coordinates, I would use the location of the Usercontrol to set your start position and only be concerned with the Width and Height of the control needed to contain your line.
public Line()
{
Size = new Size(500, 500);
}