How to draw a lot of clickable rectangles in WPF? - c#

I'm working on a basic mindmap program but I don't have a lot of experience with drawing with WPF. I want to be able to draw rectangles with text on them and i would like to be able to click on the rectangles to change the text for example.
As of now I have:
private void DrawSubject(int curve, double X, double Y, Brush clr)
{
Rectangle rect = new Rectangle();
rect.Width = 62;
rect.Height = 38;
rect.Fill = clr;
rect.Stroke = line;
rect.RadiusX = rect.RadiusY = curve;
Canvas.SetLeft(rect, X);
Canvas.SetTop(rect, Y);
mindmap.Children.Add(rect);
}
SolidColorBrush line = new SolidColorBrush(Color.FromArgb(255, 21, 26, 53));
minmap is the name of the canvas. I want to be able to draw a lot of these rectangles which present branches of the mindmap. However, when I drew 10,000 of these on random locations the process memory in the diagnostic tools went up by 100 MB, after it was done drawing all of them. I did this to sort of simulate a mindmap with 10,000 branches. So i was wondering if there might be a way to decrease the used memory for these rectangles?
Or is it better to use DrawingVisual and a grid.click event which checks if the clicked position matches the position of a rectangle by putting the coordinates of the rectangle in a List?

I would attempt the DrawingVisual method you described, if that proves costly in performance(I don't know how well DrawingVisual works) you could look into embedding OpenGL or DirectX into your application and rendering them via that.
But raytracing drawn visuals rather than making a Control for each is definitely the way to go for your scale.

Related

Use picturebox as a canvas and draw text

I want to use a PictureBox as a canvas and draw some text on it and save.
I wrote this piece of code but I'm not sure if im doing this the correct way:
Bitmap b = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Graphics g = Graphics.FromImage(b);
g.FillRectangle(new SolidBrush(Color.White), new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height)); // i used this code to make the background color white
g.DrawString("some text", new Font("Times New Roman", 20), new SolidBrush(Color.Red), new PointF(10, 10));
pictureBox1.Image = b;
This code works well but when I want to change the background color of the image I have to redraw the text.
Is there a way to change the background color without having to redraw the text?
Writing a Paint program is a lot of fun, but you need to plan ahead for all or most of the features you want.
So far you have these:
A background you can change
A way to modify an image by drawing text on it
The need to save it all to a file
Here are a few more things you will need:
Other tools than just text, like lines, rectangles etc..
A choice of colors and pens with widths
A way to undo one or more steps
Here are few thing that are nice to have:
A way to help with drawing and positioning with the mouse
Other type backgrounds like a canvas or pergament paper
The ability to draw with some level of tranparency
A redo feature (*)
Rotation and scaling (***)
Levels (*****)
Some things are harder (*) or a lot harder (***) than others, but all get hard when you decide to patch them on too late..
Do read this post (starting at 'actually') about PictureBoxes, which explain how it is the ideal choice for a Paint program.
Your original piece of code and your question have these problems:
You seem to think that repeating anything, like redrawing the text is wrong. It is not. Windows redraws huge numbers of things all the time..
You have mixed two of the tasks which really should be separate.
You have not parametrizied anything, most notably the drawing of the text should use several variables:
Font
Brush
Position
the text itself
The same will be true once you draw lines or rectangles..
So here are the hints how do get it right:
Use the BackgroundColor and/or the BackgroundImage of the Picturebox to dynamically change the background!
Collect all things to draw in a List<someDrawActionclass>
Combine all drawings by drawing it into he Picturebox's Image
Use the Paint event to draw supporting things like the temporary rectangle or line while moving the mouse. On MouseUp you add it to the list..
So, coming to the end, let's fix your code..:
You set the backgound with a function like this:
void setBackground(Color col, string paperFile)
{
if (paperFile == "") pictureBox1.BackColor = col;
else pictureBox1.BackgroundImage = Image.FromFile(paperFile);
}
you can call it like this: setBackground(Color.White, "");
To draw a piece of text into the Image of the PictureBox, first make sure you have one:
void newCanvas()
{
Bitmap bmp = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);
pictureBox1.Image = bmp;
}
Now you can write a function to write text. You really should not hard-code any of the settings, let alone the text! This is just a quick and very dirty example..:
void drawText()
{
using (Font font = new Font("Arial", 24f))
using (Graphics G = Graphics.FromImage(pictureBox1.Image))
{
// no anti-aliasing, please
G.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixel;
G.DrawString("Hello World", font, Brushes.Orange, 123f, 234f);
}
pictureBox1.Invalidate();
}
See here and here for a few remarks on how to create a drawAction class to store all the things your drawing is made up from..!
The last point is how to save all layers of the PictureBox:
void saveImage(string filename)
{
using (Bitmap bmp = new Bitmap(pictureBox1.ClientSize.Width,
pictureBox1.ClientSize.Height))
{
pictureBox1.DrawToBitmap(bmp, pictureBox1.ClientRectangle);
bmp.Save("yourFileName.png", ImageFormat.Png);
}
}

WPF DrawLine performance issue

I need to draw about 12.000 lines in my project. When i was using WinForms, thanks for e.Graphics.DrawLine function of pictureBox, it was fine. But when i migrate the project to the WPF -which is i'm kinda new at- i decided to use canvas, and draw line as a children on it. Turns out it is the most insufficient method. But i just cannot succeed the other methods. Here is the function i've been using to draw a line on a canvas :
public void DrawLine(int x, int y1, int y2, System.Drawing.Color color)
{
Line top = new Line();
top.Stroke = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B));
top.StrokeThickness = 5;
top.X1 = x;
top.Y1 = y1;
top.X2 = x;
top.Y2 = y2;
Canvas.SetTop(top, 0);
Canvas.SetLeft(top, 0);
scanCanvas.Children.Add(top);
}
When i tried to do a benchmark of my overall draw function by using stopwatch, it says there are just 300 ellapsed miliseconds, which seems fine. But what i see on the screen has a delay about 2-3 seconds. How can i draw lines on a canvas faster than this?
You should then rather use DrawingVisual Class.
DrawingVisual is a lightweight drawing class that is used to render shapes, images, or text. This class is considered lightweight because it does not provide layout, input, focus, or event handling, which improves its performance. For this reason, drawings are ideal for backgrounds and clip art.
Look at using StreamGeometry and freeze everything. Drawing in WPF is very, very slow.

Custom Cursor - Colour Issues

I'm having a problem where a the colours in a custom cursor are sometimes not displaying correctly.
I have a Windows Forms application and on one of the forms there is a PictureBox control. When an image is loaded into that PictureBox and the mouse is moved over that control I want to display a custom cursor. This cursor is a little unusual in that for each image that's loaded into the PictureBox there's another associated image, and as the user moves the mouse pointer over the PictureBox control I want the cursor to be a square and within that square I want it to display the corresponding pixels from the associated image. Within certain limits, the size of the cursor may be changed by the user.
In principle I've nearly got it to work. Certainly at any given point the cursor is showing the right part of the associated image and it updates properly as I move the pointer around.
The problem is that the colours being displayed in the cursor are not always correct. They're in the right ballpark: a shade of red will always display as a shade of red for instance, just not necessarily the right shade. Sometimes the cursor colours look slightly darker, other times slightly lighter. I've noticed that for any colours where all three of the R, G and B components are a combination of 0 or 255, the colours always look correct.
In addition, this only happens if the cursor is a 64x64 pixel square or smaller. If the user increases the size of the cursor beyond that point the problem disappears, though it will reappear if the cursor size is later reduced back to 64x64 or smaller.
This is the code which does the work. X and Y come from the MouseMove event. CursorSize is a form-wide int variable which holds the size of the cursor in pixels (always an even number) and imgAssoc is a form-wide Image variable into which the associated image was loaded. picMain is the PictureBox control.
private void LoadCursorFromBitmap(int X, int Y)
{
int halfSize = CursorSize / 2;
// The new cursor image is a [CursorSize] x [CursorSize] pixel square.
using (Bitmap bmp = new Bitmap(CursorSize, CursorSize))
{
using (Graphics g = Graphics.FromImage(bmp))
{
// Create a [CursorSize] x [CursorSize] pixel square, centered on the X and Y co-ordinates.
Rectangle square = new Rectangle(X - halfSize, Y - halfSize, CursorSize, CursorSize);
// Copy the square section of the image to the bitmap.
g.DrawImage(imgAssoc, 0, 0, square, GraphicsUnit.Pixel);
// Draw a line around the edge of the bitmap.
using (Pen pen = new Pen(Color.FromArgb(64, Color.Black), 1F))
{
g.DrawRectangle(pen, 0, 0, CursorSize - 1, CursorSize - 1);
}
}
picMain.Cursor = new Cursor(bmp.GetHicon());
}
}
Any ideas how to solve this?

gridding extraction in image

There is an image, and there are some grid lines which are vertical or horizontal in the image. I just want to extract the gridding from the image, but there are many unnecessary lines in the image, interfering my work. Do anyone have a more methods to extract the gridding via C#?
My idea is that for grid lines, there are some basic unit that compose the whole gridding, so if I know the size of the basic unit and I know the initial point of the gridding, maybe I can re-draw the gridding. Thus, I think it is the key to resolve the problem.
There are two direction you can take:
Either you take a look at the image and draw a grid just like the one there
Or you try to analyze the image to find out the parameters it takes to draw thw grid
I loaded the image into Photoshop and found that the gridlines start at (6,10) with a raster of 43 pixels and a line width of 1.5-2 pixels.
Using these values to draw those lines is trivial:
Image img = Image.FromFile(yourImage);
Bitmap bmp = new Bitmap(img.Width, img.Height);
using (Graphics G = Graphics.FromImage(bmp) )
using (Pen pen = new Pen(Color.DarkBlue, 2f) )
{
G.Clear(Color.Black);
for (int x = 6; x < img.Width; x += 43)
G.DrawLine(pen, x, 0, x, img.Height);
for (int y = 10; y < img.Height; y += 43)
G.DrawLine(pen, 0, y, img.Width, y);
}
bmp.Save(yourGrid);
bmp.Dispose();
img.Dispose();
The second option is not trivial. If the image is in fact typical, the good news is that the lines are straight and the raster regular. But pulling those values from the image automagically is still not so simple. One dificulty id the varying color of the grid.. I would scan a number of rows and test the pixels for being not black. Those would go into lists and then I would discard points that are only in a minority of those lists. This would then be repeated for a number of columns.
The effort to put into this will only pay if you need this analysis to be done for a considerable number of files with different grids. If the number is small a semi-automatic analysis may be easier and if it is only about one file go for the manual analysis!

Render Large Canvases in UserControl

I've been having trouble trying to implement this for a couple of days now. I've searched extensively on similar questions in regards to what I'm trying to do but I haven't come across a question that helps my issues directly.
Basically I'm rendering tiles onto a grid on my UserControl class. This is for my Tile Engine based world editor I'm developing. Here is a screenshot of an open world document and some tiles brushed on.
Initially, I was going to use a Bitmap in my control that would be the world's preview canvas. Using a brush tool for example, when you move your mouse and have the left button down, it sets the nearest tile beneath your cursor to the brush's tile, and paints it on the layer bitmap. The control's OnPaint method is overridden to where the layer bitmap is draw with respect to the paint event's clipping rectangle.
The issue with this method is that when dealing with large worlds, the bitmap will be extremely large. I need this application to be versatile with world sizes, and it's quite obvious there are performance issues when rendering large bitmaps onto the control each time it's invalidated.
Currently, I'm drawing the tiles onto the control directly in my control's overridden OnPaint event. This is great because it doesn't require a lot of memory. For example, a (1000, 1000) world at (20, 20) per tile (total canvas size is (20000, 20000)) runs at about 18mb of memory for the whole application. While not memory intensive, it's pretty processor intensive because every time the control is invalidated it iterates through every tile in the viewport. This produces a very annoying flicker.
What I want to accomplish is a way to meet in the middle as far as memory usage and performance. Essentially double buffer the world so that there isn't flickering when the control is redrawn (form resize, focus and blur, scrolling, etc). Take Photoshop for example - how does it render the open document when it overflows the container viewport?
For reference, here's my control's OnPaint override that is using the direct draw method mentioned above.
getRenderBounds returns a rectangle relative to PaintEventArgs.ClipRectangle that is used to render visible tiles, instead of looping through all the tiles in the world and checking if it's visible.
protected override void OnPaint(PaintEventArgs e)
{
WorldSettings settings = worldSettings();
Rectangle bounds = getRenderBounds(e.ClipRectangle),
drawLocation = new Rectangle(Point.Empty, settings.TileSize);
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
e.Graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.None;
e.Graphics.PixelOffsetMode =
System.Drawing.Drawing2D.PixelOffsetMode.None;
e.Graphics.CompositingQuality =
System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
for (int x = bounds.X; x < bounds.Width; x++)
{
for (int y = bounds.Y; y < bounds.Height; y++)
{
if (!inWorld(x, y))
continue;
Tile tile = getTile(x, y);
if (tile == null)
continue;
drawLocation.X = x * settings.TileSize.Width;
drawLocation.Y = y * settings.TileSize.Height;
e.Graphics.DrawImage(img,
drawLocation,
tileRectangle,
GraphicsUnit.Pixel);
}
}
}
Just comment if you need some more context from my code.
The trick is to not use a big bitmap for this at all. You only need a bitmap covering the visible area. Then you draw whatever is visible.
To achieve this you will need to maintain the data separately from the bitmap. This can be a simple array or an array/list with a simple class holding information for each block such as world position.
When your block is within the visible area then you draw it. You may or may not have to iterate through the whole array, but that isn't really a problem (you can also calculate the visible array on a separate thread). You can also make the function more intelligent by creating region indexes so you don't iterate all blocks.
To add a new block to the array, calculate it's canvas position to world coordinates, add it and then render the array again (or the area where the block is drawn).
This is how controls with scrollable areas are drawn by the system too.
Enable double-buffering will keep it clear and flicker-less.
In this case I would also use a panel with separate scroll bars and calculate the scroll-bars' relative position.

Categories