I have a Canvas that I am using in WPF to draw many colored rectangles to but the program is running really slow when they are being added. I have tried different options, such as adding them to an Array and adding them all at once and using a Image instead of a Canvas to dispay them, but they didn't seem to do much. I have the coding leading up the drawing in a thread, but because of C# rules, I have to have the drawing part in the main thread. I should also note that the problem isn't my computer (its running Intel Core i7 with 14GB DDR2 RAM).
This is the code that adds the rectangles. It is ran over 83,000 times.
private void AddBlock(double left, double top, double width, double height, Brush color)
{
if (this.Dispatcher.Thread != Thread.CurrentThread)
{
this.Dispatcher.Invoke(new Action<double, double, double, double, Brush>(this.AddBlock), left, top, width, height, color);
return;
}
Rectangle rect = new Rectangle() { Width = width, Height = height, Fill = color, SnapsToDevicePixels = true };
this.canvas.Children.Add(rect);
Canvas.SetLeft(rect, left);
Canvas.SetTop(rect, top);
}
NOTE: As I stated in a comment below, I would like something that allows it to run on a seperate thread (even if it involves working with P/Invoke) as there doesn't seem to a viable solution to just using C# and WPF.
Any suggestions?
Using OnRender method
I created a class inheriting Canvas, and override the OnRender method to get the DrawingContext and used it to draw. so in the code I dont add rects to the canvas but to the rect list in new class and call InvalidateVisual(); using Dispatcher once I am done with add.
class MyCanvas:Canvas
{
public class MyRect
{
public Rect Rect;
public Brush Brush;
}
public List<MyRect> rects = new List<MyRect>();
protected override void OnRender(System.Windows.Media.DrawingContext dc)
{
base.OnRender(dc);
for (int i = 0; i < rects.Count; i++)
{
MyRect mRect = rects[i];
dc.DrawRectangle(mRect.Brush, null, mRect.Rect);
}
}
}
xaml
<l:MyCanvas x:Name="canvas"/>
to add the rects
private void AddBlock(double left, double top, double width, double height, Brush color)
{
canvas.rects.Add(new MyCanvas.MyRect() { Brush = color, Rect = new Rect(left, top, width, height) });
}
refresh when ready, should be made on dispatcher
canvas.InvalidateVisual();
This seems to be the fastest way to draw in WPF, you may not need to go till GDI+ or pinvoke. During tests in my system original code took approx 500 ms to render 830 rects and geometry took approx 400 ms to render the same, where as this approach rendered 83,000 rects in less then 100 ms
Also I would advice you to add some caching to avoid rendering excessively
A solution using geometry
class level variables
GeometryGroup gGroup;
prepare using the following code
DrawingBrush dBrush= new DrawingBrush();
gGroup = new GeometryGroup();
GeometryDrawing gDrawing = new GeometryDrawing(Brushes.Red, null, gGroup);
dBrush.Drawing = gDrawing;
Canvas.Background = dBrush
then comes your code
private void AddBlock(double left, double top, double width, double height, Brush color)
{
if (this.Dispatcher.Thread != Thread.CurrentThread)
{
this.Dispatcher.Invoke(new Action<double, double, double, double, Brush>(this.AddBlock), left, top, width, height, color);
return;
}
//color need to figure out as it is added in GeometryDrawing
//currently Brushes.Red defined earlier
gGroup.Children.Add(new RectangleGeometry(new Rect(left, top, width, height)));
}
This sample may help you achieve the same. I'll also do some experiment soon to get your desired result in a much faster way.
Related
I have a pictureBox in my program. This pictureBox is kind a "unlimited" working area for my other controls(it's width and height growing all the time, when some of my controls located inside this pictureBox came close to pictureBox's borders). I implement scrollBars for pictureBox by myself programatically, It's just changing control's location inside the pictureBox. Now i also want to implement scrollable background. I want that backgrounds position changed as well as a controls when i scroll scrollBar. I create code for that (i simplify it for easy reading. Here is only logic for horizontal scrollBar. I call it when ScrollBar event works):
public Bitmap DrawImageUnscaled(int x, int y, int width, int height)
{
//this is the part of the image that will be cropped
Bitmap croppedPartBitmpap = new Bitmap(width, height);
Rectangle croppedPartRectangle = new Rectangle(x, y, width, height);
using (var g = Graphics.FromImage(croppedPartBitmpap))
{
//BigBitmap - original background
g.DrawImage(this.BigBitmap, 0, 0, croppedPartRectangle, GraphicsUnit.Pixel);
}
//this is the part of the image that will left
Bitmap leftPartBitmap = new Bitmap(this.PanelWidth - width, height);
Rectangle leftPartRectangle = new Rectangle(width, y,this.PanelWidth - width, height);
using (var g = Graphics.FromImage(leftPartBitmap))
{
g.DrawImage(this.BigBitmap, 0, 0, leftPartRectangle, GraphicsUnit.Pixel);
}
//this is the merged image
Bitmap mergedBitmap = new Bitmap(this.PanelWidth, this.PanelHeight);
using (var g = Graphics.FromImage(mergedBitmap))
{
g.DrawImage(leftPartBitmap, 0, 0);
g.DrawImage(croppedPartBitmpap, leftPartBitmap.Width, 0);
}
return mergedBitmap;
}
When i scroll this pictureBox area, program is cropping part of background that goes out of the boundaries and after this merged it at end with the part that been left on the screen. It works okay, but it take so much memory, it can take 2gb and more. Can you help me to avoid of using such giant memoryt? Maybe the whole logic wrong and i should try better solution for that?
I have a sizable form and the location in which a pixel is drawn moves, I have a panel which is the desired image size 400,200 - how can I change individual pixels on this panel? I also need the fastest change possible.
GDI+ does not have a method to set a single pixel, however you can set a 1x1 rectangle to achieve the same effect in your OnPaint event. It feels somewhat like a hack, however there is no standard way of doing this.
SolidBrush brush = new SolidBrush(Color.White);
private void panel1_Paint(object sender, PaintEventArgs e)
{
SetPixel(brush, e.Graphics, 10, 10, Color.Red);
}
public void SetPixel(SolidBrush brush, Graphics graphics, int x, int y, Color color)
{
brush.Color = color;
graphics.FillRectangle(brush, x, y, 1, 1);
}
You could also access the bitmap directly and use it's GetPixel and SetPixel methods, however they are generally very slow if your pixels need to be updated quickly.
I think I'm missing something trivial here. I derived simple control directly from Control. I'm overriding OnPaint and painting the rectangle (e.Graphics.DrawRectangle)and a text inside it (e.Graphics.DrawString). I did not override any other members.
It paints itself well when the control is resized to the smaller size, but when it gets resized to the larger size, new area is not repainted properly. As soon as I resize it to the smaller size again, even if by one pixel, everything repaints correctly.
OnPaint gets called properly (with appropriate PaintEventArgs.ClipRectangle set correctly to new area), but the new area is not painted (artifacts appear) anyway.
What am I missing?
EDIT:
Code:
protected override void OnPaint(PaintEventArgs e)
{
// Adjust control's height based on current width, to fit current text:
base.Height = _GetFittingHeight(e.Graphics, base.Width);
// Draw frame (if available):
if (FrameThickness != 0)
{
e.Graphics.DrawRectangle(new Pen(FrameColor, FrameThickness),
FrameThickness / 2, FrameThickness / 2, base.Width - FrameThickness, base.Height - FrameThickness);
}
// Draw string:
e.Graphics.DrawString(base.Text, base.Font, new SolidBrush(base.ForeColor), new RectangleF(0, 0, base.Width, base.Height));
}
private int _GetFittingHeight(Graphics graphics, int width)
{
return (int)Math.Ceiling(graphics.MeasureString(base.Text, base.Font, width).Height);
}
Try adding this in your constructor:
public MyControl() {
this.ResizeRedraw = true;
this.DoubleBuffered = true;
}
and in your paint event, clear the previous drawing:
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.Clear(SystemColors.Control);
// yada-yada-yada
}
While ResizeRedraw will work, it forces the entire control to repaint for every resize event, rather than only painting the area that was revealed by the resize. This may or may not be desirable.
The problem the OP was having is caused by the fact that the old rectangle does not get invalidated; only the revealed area gets repainted, and old graphics stay where they were. To correct this, detect whether the size of your rectangle has increased vertically or horizontally, and invalidate the appropriate edge of the rectangle.
How you would specifically go about this would depend on your implementation. You would need to have something that erases the old rectangle edge and you would have to call Invalidate passing an area containing the old rectangle edge. It may be somewhat complicated to get it to work properly, depending what you're doing, and using ResizeRedraw after all may be much simpler if the performance difference is negligible.
Just for example, here is something you can do for this problem when drawing a border.
// member variable; should set to initial size in constructor
// (side note: should try to remember to give your controls a default non-zero size)
Size mLastSize;
int borderSize = 1; // some border size
...
// then put something like this in the resize event of your control
var diff = Size - mLastSize;
var wider = diff.Width > 0;
var taller = diff.Height > 0;
if (wider)
Invalidate(new Rectangle(
mLastSize.Width - borderSize, // x; some distance into the old area (here border)
0, // y; whole height since wider
borderSize, // width; size of the area (here border)
Height // height; all of it since wider
));
if (taller)
Invalidate(new Rectangle(
0, // x; whole width since taller
mLastSize.Height - borderSize, // y; some distance into the old area
Width, // width; all of it since taller
borderSize // height; size of the area (here border)
));
mLastSize = Size;
I writing my own textbox control in C# for Winforms.
One thing i can't figure out: how to draw the text position sign in various sizes?
It is called the 'caret'. The winapi functions are not wrapped by winforms, you'll have to pinvoke them. Start reading here. You'll find code in my answer here.
Try this.
I created a method which is meant to be called from the paint handler of whichever control you're drawing in. For simplicity I just used the form itself in mine. You probably have a panel or some other control.
The method accepts the graphics object, the scale of the cursor and the upper/left position of where to start drawing. The scale is just the height but all the math is performed relative to the height. You can tweak those numbers any way you want.
private void Form1_Paint(object sender, PaintEventArgs e)
{
DrawCaret(e.Graphics, 30, new Point(20, 20));
DrawCaret(e.Graphics, 50, new Point(100, 100));
}
private static void DrawCaret(Graphics g, int scale, Point loc)
{
g.SmoothingMode = SmoothingMode.HighQuality;
int height = scale;
int width = scale/10;
int rectBase = scale/5;
g.FillRectangle(Brushes.Black, loc.X, loc.Y, width, height);
var path = new GraphicsPath();
path.AddPolygon(new[]
{
new Point(loc.X+width, loc.Y),
new Point(loc.X+width+rectBase/2, loc.Y+rectBase/2),
new Point(loc.X+width, loc.Y+rectBase),
});
g.FillPath(Brushes.Black, path);
}
This sample produces the following output:
is there a way to add a drop shadow to controls?
are there any controls out there with this feature?
You have to overwrite the CreateParamsproperty like this:
private const int CS_DROPSHADOW = 0x00020000;
protected override CreateParams CreateParams
{
get
{
// add the drop shadow flag for automatically drawing
// a drop shadow around the form
CreateParams cp = base.CreateParams;
cp.ClassStyle |= CS_DROPSHADOW;
return cp;
}
}
This question has been around for 6 years and needs an answer. I hope that anyone who needs to do this can extrapolate an answer for any control set from my solution. I had a panel and wanted to draw a drop shadow underneath every child control - in this instance one or more panels (but the solution should hold good for other control types with some minor code changes).
As the drop shadow for a control has to be drawn on the surface of that control's container we start by adding a function to the container's Paint() event.
Container.Paint += dropShadow;
dropShadow() looks like this:
private void dropShadow(object sender, PaintEventArgs e)
{
Panel panel = (Panel)sender;
Color[] shadow = new Color[3];
shadow[0] = Color.FromArgb(181, 181, 181);
shadow[1] = Color.FromArgb(195, 195, 195);
shadow[2] = Color.FromArgb(211, 211, 211);
Pen pen = new Pen(shadow[0]);
using (pen)
{
foreach (Panel p in panel.Controls.OfType<Panel>())
{
Point pt = p.Location;
pt.Y += p.Height;
for (var sp = 0; sp < 3; sp++)
{
pen.Color = shadow[sp];
e.Graphics.DrawLine(pen, pt.X, pt.Y, pt.X + p.Width - 1, pt.Y);
pt.Y++;
}
}
}
}
Clearly you can pick a different control type from the container's collection and you can vary the colour and depth of the shadow with some minor tweaks.
The top answer does in fact generate a shadow, but I personally wasn't satisfied with it for a few reasons:
It only works for rectangles (granted, WinForms controls are all rectangles, but we might want to use this in other cases)
More importantly: It's not smooth. It doesn't look as natural as other shadows in other programs look.
Finally, it's slightly annoying to configure.
So, because of all these things, I ended up writing my own for my project and I thought I'd share it here:
public partial class Form1 : Form
{
List<Control> shadowControls = new List<Control>();
Bitmap shadowBmp = null;
public Form1()
{
InitializeComponent();
shadowControls.Add(panel1);
this.Refresh();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
if (shadowBmp == null || shadowBmp.Size != this.Size)
{
shadowBmp?.Dispose();
shadowBmp = new Bitmap(this.Width, this.Height, PixelFormat.Format32bppArgb);
}
foreach (Control control in shadowControls)
{
using (GraphicsPath gp = new GraphicsPath())
{
gp.AddRectangle(new Rectangle(control.Location.X, control.Location.Y, control.Size.Width, control.Size.Height));
DrawShadowSmooth(gp, 100, 60, shadowBmp);
}
e.Graphics.DrawImage(shadowBmp, new Point(0, 0));
}
}
private static void DrawShadowSmooth(GraphicsPath gp, int intensity, int radius, Bitmap dest)
{
using (Graphics g = Graphics.FromImage(dest))
{
g.Clear(Color.Transparent);
g.CompositingMode = CompositingMode.SourceCopy;
double alpha = 0;
double astep = 0;
double astepstep = (double)intensity / radius / (radius / 2D);
for (int thickness = radius; thickness > 0; thickness--)
{
using (Pen p = new Pen(Color.FromArgb((int)alpha, 0, 0, 0), thickness))
{
p.LineJoin = LineJoin.Round;
g.DrawPath(p, gp);
}
alpha += astep;
astep += astepstep;
}
}
}
}
In this implementation, all Controls added to the shadowControls will be painted with a smooth shadow. You should be able to implement this for non-rectangular shapes because the main function to generate the shadows takes a GraphicsPath. Please note that it's important you draw the shadow to another bitmap before drawing it to the form because the main function requires a compositing mode of SourceCopy to work, which means if you don't draw it to another surface first anything behind the shadow will be completely replaced and the transparency aspect is useless. I'm on a roll of answering 10-year-old questions, but hopefully, this helps someone!
There is in WPF if you can stretch to using that instead, I don't believe there is an alternative in Windows Forms due to the limited capabilities of GDI+.
Here's a controversial opinion, you do it without code.
Set your main panel Border Style to Fixed Single.
Create 3 panels below it, each 1 pixel larger in every direction.
Each of the 3 panels is of a lighter shade of gray.
Not perfect but cheap and easy.
panel with pseudo-shadow