Is there a way to add data to DrawingVisual - c#

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

Related

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

Issue with transparent images with a transparent background

I'm having a problem with displaying transparent images with a transparent background. The transparent background takes the color of the underlying control and that is fine ... bu the problem is that some details (lines) on the underlying background are being covered the the images as can be seen in the image below.
Here is the code I am using.... This is the code for the notes....
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Media;
using System.Drawing;
namespace Simpe_Piano_new
{
class MusicNote: PictureBox
{
public SoundPlayer sp = new SoundPlayer();
Timer tmr = new Timer();
public int pitch; //The no. of the music key (e.g. the sound freuency).
public int noteDuration; //Shape of note.
public string noteShape;
public MusicNote(int iPitch, int iNoteDuration)
: base()
{
pitch = iPitch;
noteDuration = iNoteDuration;
Size = new Size(40, 40);
}
public void ShowNote()
{ if (this.noteDuration == 1) noteShape = "Quaver.png";
if (this.noteDuration == 4) noteShape = "Crotchet.png";
if (this.noteDuration == 7) noteShape = "minim.png";
if (this.noteDuration == 10) noteShape = "DotMin.png";
if (this.noteDuration == 12) noteShape = "SemiBreve.png";
this.BackgroundImage = Image.FromFile(noteShape);
this.BackColor = Color.Transparent;
Location = new Point((pitch * 40) - 40, 100);
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
}
public void PlaySound()
{
sp.SoundLocation = this.pitch + ".wav";
sp.Play();
}
public void StopSound()
{
sp.SoundLocation = this.pitch + ".wav";
sp.Stop();
}
public void Play()
{
sp.SoundLocation = this.pitch + ".wav";
sp.Play();
//Time to play the duration
tmr.Interval = noteDuration;
tmr.Start();
tmr.Tick += new System.EventHandler(ClockTick);
}
void ClockTick(object sender, EventArgs e)
{
sp.Stop();
tmr.Stop();
}
}
}
This is the code for the underlying control..the music staff
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
namespace Simpe_Piano_new
{
public class MusicStaff: Panel
{
Pen myPen;
Graphics g;
public MusicStaff()
{
this.Size = new Size(1000, 150);
this.Location = new Point(0, 0);
this.BackColor = Color.Transparent;
this.Paint += new PaintEventHandler(DrawLines);
}
private void DrawLines(object sender, PaintEventArgs pea)
{
myPen = new Pen(Color.Black, 1);
g = this.CreateGraphics();
for (int i = 1; i < 6; i++)
{
g.DrawLine(myPen, 0, (this.Height / 6) * i, this.Width, (this.Height / 6) * i);
}
}
}
}
I have found that C# does not handle transparency really well...
Any help would be greatly appreciated..
add the top control "MusicNote" in the children of the underlying control "MusicStaff"
something like that after -Initializing all components-
// mStaff: the MusicStaff object
// mNote: the MusicNote object
mStaff.Children.Add(mNote);
in old scenario, the form is the parent of both of them, so they display the form background in any transparent area
after modifying the parent of the "MusicNote", it displays the "MusicStaff" background in the transparent area
I hope that help!
Two mistakes. PictureBox supports transparent images well, as long as you set its BackColor property to Color.Transparent. Which you did for the MusicStaff but not for the MusicNote. Layered transparency does not work, you don't need MusicStaff to be transparent, just the picture boxes.
This kind of transparency is simulated by asking the Parent to paint itself into the control to provide the background pixels. Which is your second mistake, you use CreateGraphics() in your DrawLines() method. Which draws directly to the screen, not the control surface. You must use pea.Graphics here.
Do note that the value-add you get from using PictureBox is a very low one. Controls are expensive and you'll easily burn up hundreds of them to display a sheet of music. You'll notice, it will become slow to paint itself. You avoid this by having MusicStaff just paint the notes itself, using Graphics.DrawImage() gets the job done. Transparency effects are now much simpler as well, just layers of paint. Which is the way WPF does it. The only inconvenience you'll have to deal with is that mouse hit testing isn't as simple anymore, you need to map the panel's MouseDown event's coordinates to a note. Just keep a List that keeps track where every note is displayed. You'll use that for painting as well as mouse hit testing.

Why when resizing the user control graph it's not really resized?

I have a user control chart in my Form1 designer and this is the code to resize it:
private void graphChart1_MouseEnter(object sender, EventArgs e)
{
graphChart1.Size = new Size(600, 600);
}
When I move the mouse to the control area it's not resizing it make it bigger but deleting some other controls.
This is an image before I move the mouse over the control:
And this is an image when I moved the mouse over the control:
This is the code of the user control where the chart is:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Web;
using System.Windows.Forms.DataVisualization.Charting;
namespace GatherLinks
{
public partial class GraphChart : UserControl
{
public GraphChart()
{
InitializeComponent();
}
private double f(int i)
{
var f1 = 59894 - (8128 * i) + (262 * i * i) - (1.6 * i * i * i);
return f1;
}
private void GraphChart_Load(object sender, EventArgs e)
{
chart1.Series.Clear();
var series1 = new System.Windows.Forms.DataVisualization.Charting.Series
{
Name = "Series1",
Color = System.Drawing.Color.Green,
IsVisibleInLegend = false,
IsXValueIndexed = true,
ChartType = SeriesChartType.Line
};
this.chart1.Series.Add(series1);
for (int i = 0; i < 100; i++)
{
series1.Points.AddXY(i, f(i));
}
chart1.Invalidate();
}
}
}
EDIT:
I did this in the user control class code:
public void ChangeChartSize(int width, int height)
{
chart1.Size = new Size(width, height);
chart1.Invalidate();
}
I had to add chart1.Invalidate(); to make it to take effect but then it sized the chart it self inside the user control. The user control was not changed.
So in the Form1 mouse enter I also changing the graphChart1 the control size:
private void graphChart1_MouseEnter(object sender, EventArgs e)
{
graphChart1.ChangeChartSize(600, 600);
graphChart1.Size = new Size(600, 600);
}
The problem is that now it's taking a lot of time almost 20 seconds or so until it take effect when I'm moving the mouse over the control. If I will remove the second line:
graphChart1.Size = new Size(600, 600);
it will work fast but then it will change the chart only inside the control but it won't change the control size.
Tried also with invalidate:
private void graphChart1_MouseEnter(object sender, EventArgs e)
{
graphChart1.ChangeChartSize(600, 600);
graphChart1.Size = new Size(600, 600);
graphChart1.Invalidate();
}
But still very slow. Maybe I need to change the control it self size also in the user control class code and not in Form1 ?
The problem is that you are resizing the GraphicChart (your user control) but not the Chart itself. You could add the method in your GraphChart class in order to do that. This is the method that will change the chart size:
public void ChangeChartSize(int width, int height)
{
chart1.Size = new Size(width, height);
}
And in your mouse enter event handler you could call something like this:
void graphicChart1_MouseEnter(object sender, EventArgs e)
{
graphChart1.ChangeChartSize(600, 600);
}
With graphChart1.Size = you are resizing your container but not the chart within it.
The easiest work-around is probably to make chart1 public in the control and do graphChart1.chart1.Size = instead.
In the user control class code I did:
public void ChangeChartSize(int width, int height)
{
this.Size = new Size(width, height);
chart1.Size = new Size(width, height);
chart1.Invalidate();
}
In Form1 I did:
private void graphChart1_MouseEnter(object sender, EventArgs e)
{
graphChart1.ChangeChartSize(600, 600);
}
Working smooth.

Forcing drawingContext lines to snap to pixel boundaries

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

Double buffering with C# has negative effect

I have written the following simple program, which draws lines on the screen every 100 milliseconds (triggered by timer1). I noticed that the drawing flickers a bit (that is, the window is not always completely blue, but some gray shines through). So my idea was to use double-buffering. But when I did that, it made things even worse. Now the screen was almost always gray, and only occasionally did the blue color come through (demonstrated by timer2, switching the DoubleBuffered property every 2000 milliseconds).
What could be an explanation for this?
using System;
using System.Drawing;
using System.Windows.Forms;
namespace WindowsFormsApplication1 {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void Form1_Paint(object sender, PaintEventArgs e) {
Graphics g = CreateGraphics();
Pen pen = new Pen(Color.Blue, 1.0f);
Random rnd = new Random();
for (int i = 0; i < Height; i++)
g.DrawLine(pen, 0, i, Width, i);
}
// every 100 ms
private void timer1_Tick(object sender, EventArgs e) {
Invalidate();
}
// every 2000 ms
private void timer2_Tick(object sender, EventArgs e) {
DoubleBuffered = !DoubleBuffered;
this.Text = DoubleBuffered ? "yes" : "no";
}
}
}
I would just draw all of your items to your own buffer, then copy it all in at once. I've used this for graphics in many applications, and it has always worked very well for me:
public Form1()
{
InitializeComponent();
}
private void timer1_Tick(object sender, EventArgs e)
{
Invalidate();// every 100 ms
}
private void Form1_Load(object sender, EventArgs e)
{
DoubleBuffered = true;
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Bitmap buffer = new Bitmap(Width, Height);
Graphics g = Graphics.FromImage(buffer);
Pen pen = new Pen(Color.Blue, 1.0f);
//Random rnd = new Random();
for (int i = 0; i < Height; i++)
g.DrawLine(pen, 0, i, Width, i);
BackgroundImage = buffer;
}
EDIT: After further investigation, it looks like your problem is what you're setting your Graphics object to:
Graphics g = CreateGraphics();
needs to be:
Graphics g = e.Graphics();
So your problem can be solved by either creating a manual buffer like I did above, or simply changing you Graphics object. I've tested both and they both work.
No need to use multiple buffers or Bitmap objects or anything.
Why don't you use the Graphics object provided by the Paint event? Like this:
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Pen pen = new Pen(Color.Blue, 1.0f);
Random rnd = new Random();
for (int i = 0; i < Height; i++)
g.DrawLine(pen, 0, i, Width, i);
}
Try setting the double buffered property to true just once in the constructor while you're testing.
You need to make use of the back buffer. Try this:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace DoubleBufferTest
{
public partial class Form1 : Form
{
private BufferedGraphicsContext context;
private BufferedGraphics grafx;
public Form1()
{
InitializeComponent();
this.Resize += new EventHandler(this.OnResize);
DoubleBuffered = true;
// Retrieves the BufferedGraphicsContext for the
// current application domain.
context = BufferedGraphicsManager.Current;
UpdateBuffer();
}
private void timer1_Tick(object sender, EventArgs e)
{
this.Refresh();
}
private void OnResize(object sender, EventArgs e)
{
UpdateBuffer();
this.Refresh();
}
private void UpdateBuffer()
{
// Sets the maximum size for the primary graphics buffer
// of the buffered graphics context for the application
// domain. Any allocation requests for a buffer larger
// than this will create a temporary buffered graphics
// context to host the graphics buffer.
context.MaximumBuffer = new Size(this.Width + 1, this.Height + 1);
// Allocates a graphics buffer the size of this form
// using the pixel format of the Graphics created by
// the Form.CreateGraphics() method, which returns a
// Graphics object that matches the pixel format of the form.
grafx = context.Allocate(this.CreateGraphics(),
new Rectangle(0, 0, this.Width, this.Height));
// Draw the first frame to the buffer.
DrawToBuffer(grafx.Graphics);
}
protected override void OnPaint(PaintEventArgs e)
{
grafx.Render(e.Graphics);
}
private void DrawToBuffer(Graphics g)
{
//Graphics g = grafx.Graphics;
Pen pen = new Pen(Color.Blue, 1.0f);
//Random rnd = new Random();
for (int i = 0; i < Height; i++)
g.DrawLine(pen, 0, i, Width, i);
}
}
}
It's a slightly hacked around version of a double buffering example on MSDN.

Categories