Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 7 years ago.
Improve this question
I am coding my own Paint in C# using the System.Drawing namespace and basically everything is going well so far except for one thing, the eraser tool.
Right now I the eraser works just by drawing a line with the same color as the background so it appears like it's erasing something.
However, I want to make a new Eraser tool, perhaps improved. This new Eraser would be able to DELETE an element with a single click if the click is within it's bounds.
I know that if one thing has already been drawn it's there and nothing can be done but I was thinking about creating a string array and I'm going to add new elements to the array. For example when I add a line and a rectangle the first two elements of array would be:
line startPoint endPoint
rectangle height width x y
Something like that. And when the Erase tool is used, just compare the coordinates.
Is there an easier approach to this?
Thanks a lot!
Yes, there is. What you're planning to do is essentially retained mode rendering in a way. You keep a list (or other data structure) of objects that are on the canvas and you can reorder or change that list in any way, e.g. by removing or adding objects. After that you just re-create the drawing, that is, clear your drawing area and then draw each object in your list in order. This is necessary because once you have drawn something you only have pixels, and if your line and rectangle intersect, you may have trouble separating line pixels from rectangle pixels.
With GDI+ this is the only approach, since you don't get much more than a raw drawing surface. However, other things exist which already provide that rendering model for you, e.g. WPF.
However, a string[] is a horrible way of solving this. Usually you would have some kind of interface, e.g.
public interface IShape {
public void Draw(Graphics g);
public bool IsHit(PointF p);
}
which your shapes implement. A line would keep its stroke colour and start/end coordinates as state which it would then use to draw itself in the Draw method. Furthermore, when you want to click on a shape with the eraser, you'd have the IsHit method to determine whether a shape was hit. That way each shape is responsible for its own hit-testing. E.g a line could implement a little fuzziness so that you can click a little next to the line instead of having to hit a single pixel exactly.
That's the general idea, anyway. You could expand this as necessary to other ideas. Note that by using this approach your core code doesn't have to know anything about the shapes that are possible (comparing coordinates can be a bit cumbersome if you have to maintain an ever-growing switch statement of different shapes). Of course, for drawing those shapes you still need a bit more code because lines may need a different interaction than rectangles, ellipses or text objects.
I created a small sample that outlines above approach here. The interesting parts are as follows:
interface IShape
{
Pen Pen { get; set; }
Brush Fill { get; set; }
void Draw(Graphics g);
bool IsHit(PointF p);
}
class Line : IShape
{
public Brush Fill { get; set; }
public Pen Pen { get; set; }
public PointF Start { get; set; }
public PointF End { get; set; }
public void Draw(Graphics g)
{
g.DrawLine(Pen, Start, End);
}
public bool IsHit(PointF p)
{
// Find distance to the end points
var d1 = Math.Sqrt((Start.X - p.X) * (Start.X - p.X) + (Start.Y - p.Y) * (Start.Y - p.Y));
var d2 = Math.Sqrt((End.X - p.X) * (End.X - p.X) + (End.Y - p.Y) * (End.Y - p.Y));
// https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line
var dx = End.X - Start.X;
var dy = End.Y - Start.Y;
var length = Math.Sqrt(dx * dx + dy * dy);
var distance = Math.Abs(dy * p.X - dx * p.Y + End.X * Start.Y - End.Y * Start.X) / Math.Sqrt(dy * dy + dx * dx);
// Make sure the click was really near the line because the distance above also works beyond the end points
return distance < 3 && (d1 < length + 3 && d2 < length + 3);
}
}
public partial class Form1 : Form
{
private ObservableCollection<IShape> shapes = new ObservableCollection<IShape>();
private static Random random = new Random();
public Form1()
{
InitializeComponent();
pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
shapes.CollectionChanged += Shapes_CollectionChanged;
}
private void Shapes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
Redraw();
}
public void Redraw()
{
using (var g = Graphics.FromImage(pictureBox1.Image))
{
foreach (var shape in shapes)
{
shape.Draw(g);
}
}
pictureBox1.Invalidate();
}
private void button1_Click(object sender, EventArgs e)
{
shapes.Add(new Line
{
Pen = Pens.Red,
Start = new PointF(random.Next(pictureBox1.Width), random.Next(pictureBox1.Height)),
End = new PointF(random.Next(pictureBox1.Width), random.Next(pictureBox1.Height))
});
}
private void pictureBox1_SizeChanged(object sender, EventArgs e)
{
pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Redraw();
}
private IShape FindShape(PointF p)
{
// Reverse search order because we draw from bottom to top, but we need to hit-test from top to bottom.
foreach (var shape in shapes.Reverse())
{
if (shape.IsHit(p))
return shape;
}
return null;
}
private void button1_MouseClick(object sender, MouseEventArgs e)
{
var shape = FindShape(e.Location);
if (shape != null)
{
shape.Pen = Pens.Blue;
Redraw();
}
}
}
Clicking the button creates a random line and redraws. Redrawing is as simple as
public void Redraw()
{
using (var g = Graphics.FromImage(pictureBox1.Image))
{
foreach (var shape in shapes)
{
shape.Draw(g);
}
}
pictureBox1.Invalidate();
}
Clicking the picture will try finding a shape at the click point, and if it finds one, it colours it blue (along with a redraw). Finding the item works as follows:
private IShape FindShape(PointF p)
{
// Reverse search order because we draw from bottom to top, but we need to hit-test from top to bottom.
foreach (var shape in shapes.Reverse())
{
if (shape.IsHit(p))
return shape;
}
return null;
}
So as you can see, the fundamental parts of actually drawing things and maybe selecting them again, are fairly easy that way. Of course, the different drawing tools are another matter, although that has solutions, too.
Related
In the app I'm trying to develop a key part is getting the position of where the user has touched. First I thought of using a tap gesture recognizer but after a quick google search I learned that was useless (See here for an example).
Then I believe I discovered SkiaSharp and after learning how to use it, at least somewhat, I'm still not sure how I get the proper coordinates of a touch. Here are sections of the code in my project that are relevant to the problem.
Canvas Touch Function
private void canvasView_Touch(object sender, SKTouchEventArgs e)
{
// Only carry on with this function if the image is already on screen.
if(m_isImageDisplayed)
{
// Use switch to get what type of action occurred.
switch (e.ActionType)
{
case SKTouchAction.Pressed:
TouchImage(e.Location);
// Update simply tries to draw a small square using double for loops.
m_editedBm = Update(sender);
// Refresh screen.
(sender as SKCanvasView).InvalidateSurface();
break;
default:
break;
}
}
}
Touch Image
private void TouchImage(SKPoint point)
{
// Is the point in range of the canvas?
if(point.X >= m_x && point.X <= (m_editedCanvasSize.Width + m_x) &&
point.Y >= m_y && point.Y <= (m_editedCanvasSize.Height + m_y))
{
// Save the point for later and set the boolean to true so the algorithm can begin.
m_clickPoint = point;
m_updateAlgorithm = true;
}
}
Here I'm just seeing or TRYING to see if the point clicked was in range of the image and I made a different SkSize variable to help. Ignore the boolean, not that important.
Update function (function that attempts to draw ON the point pressed so it's the most important)
public SKBitmap Update(object sender)
{
// Create the default test color to replace current pixel colors in the bitmap.
SKColor color = new SKColor(255, 255, 255);
// Create a new surface with the current bitmap.
using (var surface = new SKCanvas(m_editedBm))
{
/* According to this: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/finger-paint ,
the points I have to start are in Xamarin forms coordinates, but I need to translate them to SkiaSharp coordinates which are in
pixels. */
Point pt = new Point((double)m_touchPoint.X, (double)m_touchPoint.Y);
SKPoint newPoint = ConvertToPixel(pt);
// Loop over the touch point start, then go to a certain value (like x + 100) just to get a "block" that's been altered for pixels.
for (int x = (int)newPoint.X; x < (int)newPoint.X + 200.0f; ++x)
{
for (int y = (int)newPoint.Y; y < (int)newPoint.Y + 200.0f; ++y)
{
// According to the x and y, change the color.
m_editedBm.SetPixel(x, y, color);
}
}
return m_editedBm;
}
}
Here I'm THINKING that it'll start, you know, at the coordinate I pressed (and these coordinates have been confirmed to be within the range of the image thanks to the function "TouchImage". And when it does get the correct coordinates (or at least it SHOULD of done that) the square will be drawn one "line" at a time. I have a game programming background so this kind of sounds simple but I can't believe I didn't get this right the first time.
Also I have another function, it MIGHT prove worthwhile because the original image is rotated and then put on screen. Why? Well by default the image, after taking the picture, and then displayed, is rotated to the left. I had no idea why but I corrected it with the following function:
// Just rotate the image because for some reason it's titled 90 degrees to the left.
public static SKBitmap Rotate()
{
using (var bitmap = m_bm)
{
// The new ones width IS the old ones height.
var rotated = new SKBitmap(bitmap.Height, bitmap.Width);
using (var surface = new SKCanvas(rotated))
{
surface.Translate(rotated.Width, 0.0f);
surface.RotateDegrees(90);
surface.DrawBitmap(bitmap, 0, 0);
}
return rotated;
}
}
I'll keep reading and looking up stuff on what I'm doing wrong, but if any help is given I'm grateful.
I would like to write an application that will measure fragments of a specimen examined under a microscope. I thought that the best way would be to capture the image and draw on selected parts of the specimen then count the value of the drawn line in pixels (and later to convert this value into the appropriate unit).
Is there anything that helps solve such issue already implemented or any tool/package or something that allows such calculations?
I will also willingly learn about solutions in other programming languages if they allow to solve this problem in a easier way or just in some way.
This is a very basic example of measuring a segmented line drawn onto an image in winforms.
It uses a PictureBox to display the image, a Label to display the current result and for good measure I added two Buttons the clear all points and to undo/remove the last one.
I collect to pixel positions in a List<Point> :
List<Point> points = new List<Point>();
The two edit buttons are rather simple:
private void btn_Clear_Click(object sender, EventArgs e)
{
points.Clear();
pictureBox1.Invalidate();
show_Length();
}
private void btn_Undo_Click(object sender, EventArgs e)
{
if (points.Any())points.Remove(points.Last());
pictureBox1.Invalidate();
show_Length();
}
Note how I trigger the Paint event by invalidating the image whenever the points collection changes..
The rest of the code is also simple; I call a function to calculate and display the sum of all segment lengths. Note that I need at least two points before I can do that or display the first line..
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
points.Add(e.Location);
pictureBox1.Invalidate();
show_Length();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (points.Count > 1) e.Graphics.DrawLines(Pens.Red, points.ToArray());
}
void show_Length()
{
lbl_len.Text = (pointsF.Count) + " point(s), no segments. " ;
if (!(points.Count > 1)) return;
double len = 0;
for (int i = 1; i < points.Count; i++)
{
len += Math.Sqrt((points[i-1].X - points[i].X) * (points[i-1].X - points[i].X)
+ (points[i-1].Y - points[i].Y) * (points[i-1].Y - points[i].Y));
}
lbl_len.Text = (points.Count-1) + " segments, " + (int) len + " pixels";
}
A few notes:
The image is displayed without any zooming. PictureBox has a SizeMode property to make zoomed display simple. In such a case I recommend to store not the direct pixel locations of the mouse but 'unzoomed' values and to use a 'rezoomed' list of values for the display. This way you can zoom in and out and still have the points stick to the right spots.
For this you ought to use a List<PointF> to keep precision.
When zooming e.g. by enlarging the PictureBox, maybe after nesting it in a Panel, make sure to either keep the aspect ratio equal to that of the Image or to do a full calculation to include the extra space left or top; in SizeMode.Normal the image will always sit flush TopLeft but in other modes it will not always do so.
For the calculation of actual i.e. physical distances simply divide by the actual dpi value.
Let's see what we have in action:
Update:
To get a chance to create cloers fits and better precision we obviously need to zoom in on the image.
Here are the necessary changes:
We add a list of 'floating points':
List<PointF> pointsF = new List<PointF>();
And use it to store the un-zoomed mouse positions in the mouse down:
pointsF.Add( scaled( e.Location, false));
We replace all other occurances of points with pointsF.
The Paint event always calculates the scaled points to the current zoom level:
if (pointsF.Count > 1)
{
points = pointsF.Select(x => Point.Round(scaled(x, true))).ToList();
e.Graphics.DrawLines(Pens.Red, points.ToArray());
}
And the function to do the scaling looks like this:
PointF scaled(PointF p, bool scaled)
{
float z = scaled ? 1f * zoom : 1f / zoom;
return new PointF(p.X * z, p.Y * z);
}
It uses a class level variable float zoom = 1f; which gets set along with the picturebox's Clientsize in the Scroll event of a trackbar:
private void trackBar1_Scroll(object sender, EventArgs e)
{
List<float> zooms = new List<float>()
{ 0.1f, 0.2f, 0.5f, 0.75f, 1f, 2, 3, 4, 6, 8, 10};
zoom = zooms[trackBar1.Value];
int w = (int)(pictureBox2.Image.Width * zoom);
int h = (int)(pictureBox2.Image.Height * zoom);
pictureBox2.ClientSize = new Size(w, h);
lbl_zoom.Text = "zoom: " + (zoom*100).ToString("0.0");
}
The picturebox is nested inside a Panel with AutoScroll on. Now we can zoom and scroll while adding segments:
This question is asked before but since it doesn't work and my lack of reputation point(I tried to comment at question but I couldn't) I had to ask this question again.
This is the link of the quustion asked before;
How to zoom at a point in picturebox
I used the code which is shown in the link but when I run it the point or shape disappear.
here is my code;
public partial class Form1 : Form
{
private Matrix transform = new Matrix();
private double m_dZoomscale = 1.0;
public static double s_dScrollValue = .1;
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Transform = transform;
Pen mypen = new Pen(Color.Red,5);
Rectangle rect = new Rectangle(10, 10, 30, 30);
e.Graphics.DrawRectangle(mypen, rect);
}
protected override void OnMouseWheel(MouseEventArgs mea)
{
pictureBox1.Focus();
if (pictureBox1.Focused == true && mea.Delta != 0)
{
ZoomScroll(mea.Location, mea.Delta > 0);
}
}
private void ZoomScroll(Point location, bool zoomIn)
{
transform.Translate(-location.X, -location.Y);
if (zoomIn)
transform.Scale((float)s_dScrollValue, (float)s_dScrollValue);
else
transform.Scale((float)-s_dScrollValue, (float)-s_dScrollValue);
transform.Translate(location.X, location.Y);
pictureBox1.Invalidate();
}
The answer you are referencing cannot possibly work. I have no idea why it was accepted, nor up-voted. Except that at some time in the past, I apparently up-voted it as well. I don't know what I was thinking.
Anyway, that code has some problems:
It uses the mouse coordinates passed in directly, rather than converting them to the coordinate system for the PictureBox control. The coordinates passed to the OnMouseWheel() method are relative to the Form itself, so only if the PictureBox top-left coincides with the Form's upper-left corner would that work.
More problematically, the code is completely misusing the Matrix.Scale() method, passing a value that seems intended to be a delta for the scale, when in fact the Scale() method accepts a factor for the scale. This has two implications:
Passing a negative value is wrong, because negative values flip the coordinate system, rather than reducing the scale, and
Passing an increment value is wrong, because the value passed will be multiplied with the current scaling to get the new scaling.
Also problematic is that the code applies the matrix transformations in the wrong order, because the default order is "prepend", not "append" (I find the latter more natural to work with, but I assume there's some reason known to those who specialize in matrix math that explains why the default is the former).
There is also the relatively minor issue that, even ignoring the above, allowing the user to adjust the scale factor arbitrarily will eventually lead to an out-of-range value. It would be better for the code to limit the scale to something reasonable.
Here is a version of your code, modified so that it addresses all of these issues:
private Matrix transform = new Matrix();
private float m_dZoomscale = 1.0f;
public const float s_dScrollValue = 0.1f;
public Form1()
{
InitializeComponent();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Transform = transform;
Pen mypen = new Pen(Color.Red, 5);
Rectangle rect = new Rectangle(10, 10, 30, 30);
e.Graphics.DrawRectangle(mypen, rect);
}
protected override void OnMouseWheel(MouseEventArgs mea)
{
pictureBox1.Focus();
if (pictureBox1.Focused == true && mea.Delta != 0)
{
// Map the Form-centric mouse location to the PictureBox client coordinate system
Point pictureBoxPoint = pictureBox1.PointToClient(this.PointToScreen(mea.Location));
ZoomScroll(pictureBoxPoint, mea.Delta > 0);
}
}
private void ZoomScroll(Point location, bool zoomIn)
{
// Figure out what the new scale will be. Ensure the scale factor remains between
// 1% and 1000%
float newScale = Math.Min(Math.Max(m_dZoomscale + (zoomIn ? s_dScrollValue : -s_dScrollValue), 0.1f), 10);
if (newScale != m_dZoomscale)
{
float adjust = newScale / m_dZoomscale;
m_dZoomscale = newScale;
// Translate mouse point to origin
transform.Translate(-location.X, -location.Y, MatrixOrder.Append);
// Scale view
transform.Scale(adjust, adjust, MatrixOrder.Append);
// Translate origin back to original mouse point.
transform.Translate(location.X, location.Y, MatrixOrder.Append);
pictureBox1.Invalidate();
}
}
With this code, you will find that no matter where you place the mouse before adjusting the mouse wheel, the rendered image will scale while keeping the point under the mouse fixed in place.
Note:
I took a look at some of the similar questions on Stack Overflow, and there are a few that might also be useful to you. Some of the answers overcomplicate things, in my opinion, but all should work. See:
Zoom To Point Not Working As Expected
Zoom in on a fixed point using matrices
Zooming graphics without scrolling
I have draggable images on my Windows Form. The PNG's themselves have transparent backgrounds yet it only matches the panel's background color when I load it. There are other labels with different colors I'd like to be able to see as I drag it instead. Anyone know what to use to make this happen?
http://postimg.org/image/d8p4s53pf/
EDIT:
Trying to make the PictureBox truely transparant. The drag stuff is done, but the background is just the background of the panel, and doesn't show controls it passes over.
I used this but it's a bit glitchy.
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle = 0x00000020; //WS_EX_TRANSPARENT
return cp;
}
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{
//do nothing
}
protected override void OnMove(EventArgs e)
{
RecreateHandle();
}
Dragable Controls as such are not hard in WinForms. But once they need transparency the options are rather restricted: Each must be completely contained in its Parent; this implies that they all must be nested. No problem to create a set of transparent layers like you have in a graphics program.
But moving even one PictureBox-Pawn across a board full of others simply will not work with WinForms Controls.
But your aim is simple enough to do without any moving controls.
Instead you can simply draw the chess pieces onto the control surface in the Paint event. I have implemented a rudimentary solution and here is the Paint event from it:
List<ChessPiece> pieces = new List<ChessPiece>();
int mpIndex = -1; // index of the moving piece
Rectangle mmr; // rectangle where moving piece is drawn
// board display variables
int pieceSize = 60;
int tileSize = 80;
int borderWidth = 50;
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
foreach(ChessPiece p in pieces)
{
if (!p.onBoard) continue; // skip the piece?
e.Graphics.DrawImage(imageList1.Images[p.pieceIndex],
borderWidth + p.col * tileSize,
borderWidth + p.row * tileSize);
}
// if we have a moving piece..
if (mpIndex >= 0 && !pieces[mpIndex].onBoard)
e.Graphics.DrawImage(imageList1.Images[pieces[mpIndex].pieceIndex],
mmr.Location);
}
As you can see it really isn't very long. It makes use of a few obvious variables and a few less obvious ones. The pieces list and the ImageList must be prepared up front. The rectangle mmr is set in the MouseMove:
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
mmr = new Rectangle(e.X - pieceSize / 2,
e.Y - pieceSize / 2, pieceSize, pieceSize);
pictureBox1.Invalidate(); // trigger the painting
}
}
I won't post the MouseUp and -Down events as they contain mostly code that doesn't actually pertain to moving transparent images but to managing the list of pieces..
The main idea is to draw all images from a list of pieces, unless a piece is not on the board. The moving piece is drawn separately, its coordinates are not from the board positions but from the mouse position.
If in the MouseUp event we find that the the move is illegal, we can simply set it back onBoard without changing the position and it will be drawn at its old place.
In the MouseDown we determine the piece clicked on, set its onBoard=false and set the moving index mpIndex.
The class for the pieces is not long or complicated either;
public class ChessPiece
{
public bool onBoard { get; set; } // is the piece standing on the board?
public int row { get; set; } // row
public int col { get; set; } // column
public char piecetype { get; set; } // a little simplistic..not used..
public char color { get; set;} // .. could be an enumeration!
public int pieceIndex { get; set; } // points into imageList with 12 images
public ChessPiece(char color, char type, int r, int c, int ind)
{ onBoard = true; piecetype = type; this.color = color;
row = r; col = c; pieceIndex = ind; }
}
All in all the full code is around 180 lines, uncommented but including 60 lines of setting up the pieces alone and without any chess logic..
Here are the first 3 lines of the code to create the pieces:
void makePieces()
{
ChessPiece
p = new ChessPiece('W', 'R', 7, 0, 8); pieces.Add(p);
p = new ChessPiece('W', 'N', 7, 1, 10); pieces.Add(p);
p = new ChessPiece('W', 'B', 7, 2, 9); pieces.Add(p);
..
So, moving a transparent Png graphic over other Png graphics and a board image is rather simple..
Here is a screenshot that shows a black bishop over a white knight:
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 8 years ago.
Improve this question
I am trying to make a Program where you can draw on either a SVG or a JPG/PNG. BUT, this drawing is limited to only the INSIDES of the outline. For a better visualisation, look at this Picture - http://www.clker.com/cliparts/c/9/8/9/1237099922676360396kelan_Human_figure.svg
So, right here we see a human shape, outlined with a black line.
What I need is basically that you can draw inside of the humans body and its head, but everything outside of the shape should not be able to be colored.
I have tried using SharpVector (A Nuget-Package) as well as the standart-Canvas so far but I have not been successive so far. Also I cannot provide any Codesamples since they help nothing anyway, at least I think so!
Edit: My original answer was:
Yes this can be done with SVG graphics. You need to extract the path
from the file and calculate Points and graphic items that scale to
your drawing canvas.
Now, this was naive. Looking closer at the SVG format I have to say that even with simple images this is not going to be easy, in fact it is a rather tall order..
The rest is correct:
..Then you can convert them to a Region and use it to clip the
Graphics object you will use to do your drawing.
See an example about clipping regions here on
MSDN
As for a JPEG image this is not so simple. I'm not sure how I'd do it;
at the moment I'm thinking about floodfilling the outside and XORing
the result with the canvas but that sounds terribly expensive..
Now, where does this leave you? Well I guess you need to get that outline path I thought the SVG data would supply.
Here is a solution that you may find too tedious or perfectly ok: You have to trace the outline and use the trace results to create the clipping region. The tracing is a manual task, but for the example file it can be done with reasonable precision in about a minute.
Here are a few routines to set up a minimal tracing program. You need a pictureBox to display the image as jpg, png or whatever; svg is not directly supported in .NET. Set the PB to zoom and anchor it on all sides. Add a few buttons and you can start..
// We collect the trace data in a list:
List<Point> points = new List<Point>();
// each mouse click adds a point
private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{ points.Add(e.Location); pictureBox1.Invalidate(); }
// show all all trace points
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{ if (points.Count < 2) return;
e.Graphics.DrawCurve(Pens.OrangeRed, points.ToArray());
foreach (Point p in points)
e.Graphics.DrawRectangle(Pens.Red, p.X - 1, p.Y - 1, 3, 3);
}
// always good to have an undo button
private void cb_Undo_Click(object sender, EventArgs e)
{ if (points.Count > 0) { points.RemoveAt(points.Count-1); pictureBox1.Invalidate();} }
// start a new trace by initializing the list
private void cb_NewTrace_Click(object sender, EventArgs e)
{ points = new List<Point>(); pictureBox1.Invalidate(); }
Now that we can collect the trace points we'll need a way to save and load them. Many ways to do that, esp. Serialization comes to mind. But these simple functions are hard to beat in terms of shortness:
void savePoints(List<Point> points, string filename)
{ StringBuilder SB = new StringBuilder();
foreach (Point P in points) SB.AppendLine(P.X.ToString() + ";" + P.Y.ToString());
File.WriteAllText(filename, SB.ToString());
}
List<Point> loadPoints(string filename)
{ List<Point> points = new List<Point>();
var lines = File.ReadAllLines(filename);
foreach (string l in lines)
{
var p = l.Split(';');
points.Add(new Point(Convert.ToInt16(p[0]), Convert.ToInt16(p[1])));
}
return points;
}
With Buttons to Load an Image, start a Trace, Undo a click, save and load the trace data all we need is one button to test the whole thing:
// non-persistent test routine that will draw randomly in the picturebox
private void cb_Test_Click(object sender, EventArgs e)
{
using (Graphics G = pictureBox1.CreateGraphics())
{
// set up the graphicsPath
GraphicsPath path = new GraphicsPath();
path.AddCurve(points.ToArray());
// create the clipping region
Region region = new Region(path);
G.SetClip(region, CombineMode.Replace);
// now we're good to go..
// note: all drawing is done on the full range of the box!
G.FillRectangle(Brushes.GhostWhite, pictureBox1.ClientRectangle);
Random R = new Random();
int xMax = pictureBox1.ClientSize.Width;
int yMax = pictureBox1.ClientSize.Height;
for (int i = 0; i < 1000; i++)
{
int x1 = R.Next(xMax); int y1 = R.Next(yMax);
int x2 = R.Next(xMax); int y2 = R.Next(yMax);
using (Pen pen = new Pen(Color.FromArgb(
255, x1 & 255, y1 & 255, x2 * y2 & 255), 10f))
G.DrawLine(pen, x1, y1, x2, y2);
}
}
}
Here is a preview of such a trace, consisting of only 94 points and a test fill of it: