I am trying to move a drawn line by grabbing it with the mouse.
The line have already been drawn with Graphics.DrawLine(Pen P, Point A, Point B).
There is absolutely no problems with creating the Line and drawing it on the form.
I've tried:
Adding the line to a GraphicsPath - This does not even draw the line OnPaint.
Checking if MouseEventArgs e.Location is on the line with some basic algebra (calculations which I have thrown away as of now)
So to sum it up: I want to grab the line and drag it somewhere but I can't even check if e.Location even is on the Line, how do I do this?
EDIT: This is how the code looks when I'm using the GraphicsPath.
When I don't use the GraphicsPath I have:
if (s.thisShape == ShapeType.Line) {
g.DrawLine(pen, s.p1, s.p2);
} else { ... }`
in the drawingShapes method.
From the drawStuff : Usercontrol class:
private void drawStuff_MouseDown(object sender, MouseEventArgs e)
{
pointRegion = e.Location;
for (int i = 0; i < Shapes.Count; i++)
{
if (Shapes[i].Region.IsVisible(pointRegion))
{
isDragging = true;
count = i;
break;
}
}
}
private void drawStuff_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
Shapes[count].moveWithDiff(pointRegion, e.Location);
pointRegion = e.Location;
Refresh();
}
}
private void drawStuff_MouseUp(object sender, MouseEventArgs e)
{
isDragging = false;
Refresh();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
drawShapes(e.Graphics);
}
private void drawShapes(Graphics g)
{
temporaryPen = pennaLeft;
foreach (Shape s in Shapes)
{
g.FillRegion(temporaryPen, s.Region);
}
}
From the Shape : Usercontrol class:
public void moveWithDiff(Point pr, Point mp)
{
Point p = new Point();
if (this.thisShape == ShapeType.Line)
{
p.X = mp.X - pr.X;
p.Y = mp.Y - pr.Y;
this.p1.X += p.X;
this.p1.Y += p.Y;
this.p2.X += p.X;
this.p2.Y += p.Y;
}
RefreshPath();
}
private void RefreshPath()
{
gPath = new GraphicsPath();
switch (thisShape)
{
case ShapeType.Line:
gPath.AddLine(this.p1, this.p2);
break;
}
this.Region = new Region(gPath);
}
Now this doesn't even draw the line, however with said if statement in drawingShapes() It draws perfectly but I can not drag it somewhere else.
Let's start with the basics, getting a line on the screen. I created a custom class to handle some of the functions I want available to me for this process:
public class MyLine
{
public Pen pen { get; set; }
public Point Start { get; set; }
public Point End { get; set; }
public MyLine(Pen p, Point p1, Point p2)
{
pen = p;
Start = p1;
End = p2;
}
public float slope
{
get
{
return (((float)End.Y - (float)Start.Y) / ((float)End.X - (float)Start.X));
}
}
public float YIntercept
{
get
{
return Start.Y - slope*Start.X;
}
}
public bool IsPointOnLine(Point p, int cushion)
{
float temp = (slope * p.X + YIntercept);
if (temp >= (p.Y-cushion) && temp <=(p.Y+cushion))
{
return true;
}
else
{
return false;
}
}
}
This class provides a few helper functions that will make our life easier. We have properties that return the slope and the Y-intercept, so we can determine if a certain point is on the line. We then provide a helper function IsPointOnLine() that takes a point and a cushion. The cushion is used to simply allow for a user to click close enough to the line to get it to return true.
Next I am going to instantiate the line and draw it in the Form's paint event:
MyLine m;
private void Form1_Load(object sender, EventArgs e)
{
m= new MyLine(Pens.Black, new Point(20, 20), new Point(40, 40));
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawLine(m.pen, m.Start, m.End);
}
Now you should be able to run your application and see a line that goes from 20,20 to 40,40 on the screen.
Now I want to handle the mouse interaction with the line, so on MouseDown, we will see if the click point intersects the line and if it is set a flag and keep our deltas from the endpoints. On the MouseMove event, we will see if the line has been clicked but not released and reset the coordinates appropriately. In the MouseUp event, we simple reset our flag:
Point deltaStart;
Point deltaEnd;
bool dragging = false;
private void Form1_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left && m.IsPointOnLine(e.Location, 5))
{
dragging = true;
deltaStart = new Point(m.Start.X - e.Location.X, m.Start.Y - e.Location.Y);
deltaEnd = new Point(m.End.X - e.Location.X, m.End.Y - e.Location.Y);
}
}
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
if (dragging && deltaStart != null && deltaEnd != null )
{
m.Start = new Point(deltaStart.X + e.Location.X, deltaStart.Y + e.Location.Y);
m.End = new Point(deltaEnd.X + e.Location.X, deltaEnd.Y + e.Location.Y);
this.Refresh();
}
}
private void Form1_MouseUp(object sender, MouseEventArgs e)
{
dragging = false;
}
Now you should be able to click within 5 pixels of the line and have it move with your mouse.
Note, there are some spots in the code that need additional error handling, especially to handle division by 0 errors.
I suggest you create a rectangle that is the width of the line and the length of the line, then you can use
if(rectangle.Contains(Point p))
{
// do your move
}
You can also inflate the rectangle to make it easier to grab with:
rectangle.Inflate(1, 1);
Related
I have written a program that is supposed to connect 10 points generated by mouseclicks, with lines.
I am the following problems:
it draws all lines from the top left corner.
I can think of the solution being something along the lines of setting the first point as the first mouseclick, but i do not know how to do this.
code:
public partial class Form1 : Form
{
Point[] punten = new Point[10];
private int kliks = 0;
private int lijst = 0;
public Form1()
{
InitializeComponent();
}
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
kliks = kliks + 1;
lijst = kliks;
punten[lijst] = e.Location;
if (lijst < 9)
{
punten[lijst] = e.Location;
}
else
{
Pen pen = new Pen(Color.Blue);
Graphics papier = this.CreateGraphics();
papier.DrawLines(pen, punten);
}
}
}
}
The problem for you is using a fixed-size array. Each entry defaults to a point (0,0) and so the Drawlines() method draws the 10 elements, even the ones not set by mouseclicks. This results in the final few points begin at the origin (0,0). In addition, there is a buggy implementation of which clicks to keep and how many have happened in using kliks and lijst for the same thing.
This being C#, it is better to learn how to separate your data model that keeps track of your mouselicks and your display codes that draws the lines.
PointHistory.cs
Consider the following class that uses an array of Point to keep track of the mouseclicks. The class is initialized with the maximum number of points to keep (here 10 for example). There are methods for adding a point AddPoint(point) and to remove the oldest point with RemoveFirstPoint(). The number of points defined is viewed with the .Count property.
Finally, the GetPoints() method, returns a copy of the array with only the elements that have been defined. This method is used in the code that draws the lines.
using System;
using System.Linq;
public class PointHistory
{
private readonly Point[] points;
private readonly int maxPoints;
private int count;
public PointHistory(int pointCapacity)
{
count = 0;
maxPoints = pointCapacity;
points = new Point[pointCapacity];
}
public Point[] GetPoints() { return points.Take(count).ToArray(); }
public int Count { get => count; }
public int Capacity { get => maxPoints; }
public void AddPoint(Point point)
{
if (count < maxPoints)
{
points[count++] = point;
}
}
public void RemoveFirstPoint()
{
if (count > 0)
{
for (int i = 0; i < count - 1; i++)
{
points[i] = points[i + 1];
}
count--;
}
}
}
Form1.cs
Another issue is your drawing, which needs to happen in Paint event handler. Use your MouseClick event to modify the list of points, and the Paint event to do the drawing.
public partial class Form1 : Form
{
readonly PointHistory history;
public Form1()
{
InitializeComponent();
history = new PointHistory(10);
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.MouseClick += Form1_MouseClick;
this.Paint += Form1_Paint;
}
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
history.AddPoint(e.Location);
}
else if (e.Button == MouseButtons.Right)
{
history.RemoveFirstPoint();
history.AddPoint(e.Location);
}
this.Invalidate();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
if (history.Count >= 2)
{
e.Graphics.DrawLines(Pens.Blue, history.GetPoints());
}
foreach (var point in history.GetPoints())
{
e.Graphics.FillEllipse(Brushes.Blue, point.X - 2, point.Y - 2, 4, 4);
}
}
}
I have added some extra code to add little dots at the mouse clicks for a better visual effect.
Left button clicks try to add points to the history of mouseclicks, up to the limit set in the constructor for PointHistory. The right mousebutton cicks removes the oldest point first before adding the new point.
Based on the comments, I want to present a version below that does not define a separate class for the logic, and everything is done internally withing the Form1 class. Also no advanced libraries such as Linq are used
public partial class Form1 : Form
{
private readonly Point[] points;
private int count;
public Form1()
{
InitializeComponent();
points = new Point[10];
count = 0;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.Paint += Form1_Paint;
this.MouseClick += Form1_MouseClick;
}
public Point[] GetPoints()
{
Point[] results = new Point[count];
for (int i = 0; i < count; i++)
{
results[i] = points[i];
}
return results;
// Alternative to the loop above
//
//Array.Copy(points, results, count);
//return results;
}
public void AddPoint(Point point)
{
if (count < points.Length)
{
points[count++] = point;
}
}
public void RemoveFirstPoint()
{
if (count > 0)
{
for (int i = 0; i < count - 1; i++)
{
points[i] = points[i + 1];
}
count--;
}
}
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
AddPoint(e.Location);
}
else if (e.Button == MouseButtons.Right)
{
RemoveFirstPoint();
AddPoint(e.Location);
}
this.Invalidate();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Point[] mouseClicks = GetPoints();
if (count >= 2)
{
e.Graphics.DrawLines(Pens.Blue, mouseClicks);
}
foreach (var point in mouseClicks)
{
e.Graphics.FillEllipse(Brushes.Blue, point.X - 2, point.Y - 2, 4, 4);
}
}
}
The main method to look at is GetPoints() which returns an array of points that have been defined by mouse clicks and ignores any points that have not been defined yet.
There is an alternate implementation commented with uses Array.Copy() instead of a loop which would make for a better imlpementation.
I'm having performance issues while drawing a Polyline using mouse down, move, and up events in C# WPF. Please see code. My code is working fine. The problem which I'm facing is, that when ever I want to draw a continuous line after some time the movement of line become slower. I'd like to know if you have a better solution to draw a line in a faster way or if there is a better solution to improve the existing code.
Thanks.
Polyline freeLine;
Point _startPoint;
public void canvas_MouseDown(object sender, MouseButtonEventArgs e)
{
_startPoint = e.GetPosition(canvas);
freeLine = new Polyline();
freeLine.StrokeDashCap = PenLineCap.Square;
freeLine.Stroke = color;
freeLine.StrokeThickness = thickness;
canvas.Children.Add(freeLine);
freeLine.StrokeLineJoin = PenLineJoin.Round;
}
public void canvas_MouseMove(object sender, MouseButtonEventArgs e)
{
Point currentPoint = e.GetPosition(canvas1);
if (_startPoint != currentPoint)
{
arrowfreeLine.Points.Add(currentPoint1);
}
}
public void canvas_MouseUp(object sender, MouseButtonEventArgs e)
{
freeLine = null;
}
What you can do is reduce the number of points by discarding those that are very close to each other. In the example below I remove all points that are within 10 pixels of the previous significant point.
private Brush _color = new SolidColorBrush(Colors.Red);
private double _thickness = 4.0;
private int _previousSignificantPoint = 0;
private Polyline _freeLine;
private void canvas_MouseDown(object sender, MouseButtonEventArgs e)
{
// Capture mouse
canvas.CaptureMouse();
Point startPoint = e.GetPosition(canvas);
_freeLine = new Polyline
{
StrokeDashCap = PenLineCap.Square,
StrokeLineJoin = PenLineJoin.Round,
Stroke = _color,
StrokeThickness = _thickness
};
// Add first point
_freeLine.Points.Add(startPoint);
// make it the first "significant" point
_previousSignificantPoint = 0;
canvas.Children.Add(_freeLine);
}
private void canvas_MouseMove(object sender, MouseEventArgs e)
{
// Make sure the mouse is pressed and we have a polyline to work with
if (_freeLine == null) return;
if (e.LeftButton != MouseButtonState.Pressed) return;
// Get previous significant point to determine distance
Point previousPoint = _freeLine.Points[_previousSignificantPoint];
Point currentPoint = e.GetPosition(canvas);
// If we have a new significant point (distance > 10) remove all intermediate points
if (Distance(currentPoint, previousPoint) > 10)
{
for(int i = _freeLine.Points.Count - 1; i > _previousSignificantPoint; i--)
_freeLine.Points.RemoveAt(i);
// and set the new point as the latest significant point
_previousSignificantPoint = _freeLine.Points.Count;
}
_freeLine.Points.Add(currentPoint);
}
private void canvas_MouseUp(object sender, MouseButtonEventArgs e)
{
// release mouse capture
canvas.ReleaseMouseCapture();
_freeLine = null;
}
private static double Distance(Point pointA, Point pointB)
{
return Math.Sqrt(Math.Pow(pointA.X - pointB.X, 2) + Math.Pow(pointA.Y - pointB.Y, 2));
}
Result:
I have a Windows Form, My task is to add drag function to the form. "VendorMasterList" is a label. I have added mouse move, mouse down and mouse up events to that label.
If i tried to drag, the form moves down and then only able to drag the way i want. My question is why its going down?This is my code
private Point startPoint = new Point(0, 0);
private bool isDragging = false;
private void lblHeader_MouseDown(object sender, MouseEventArgs e)
{
isDragging = true; // _dragging is your variable flag
startPoint = new Point(e.X, e.Y);
}
private void lblHeader_MouseUp(object sender, MouseEventArgs e)
{
isDragging = false;
}
private void lblHeader_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
Point p = PointToScreen(e.Location);
this.Location = new Point(p.X - this.startPoint.X, p.Y - this.startPoint.Y);
}
}
Is there any way to fix this?
Newly added image
I haven' tried your code, but what I see is only partially correct :-)
You save the initial coordinate, which is relative to the upper left corner of the control that's clicked, but you need to save screen coordinates.
Then the mouse is moved and again you get a point relative to that corner. You're missing a few relevant things:
You need to calculate a delta between the two points based on the screen coordinates, otherwise moving the window underneath the cursor gives you wrong values.
You need to add this delta to the form's current location
You need to save the new mouse position to calculate the next delta correctly
The current mouse position based on screen coordinates can be obtained through Cursor.Position.
So your code should read:
private void lblHeader_MouseDown(object sender, MouseEventArgs e)
{
isDragging = true; // _dragging is your variable flag
startPoint = Cursor.Position;
}
private void lblHeader_MouseUp(object sender, MouseEventArgs e)
{
isDragging = false;
}
private void lblHeader_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
Point p = Cursor.Position;
int deltaX = p.X - startPoint.X;
int deltaY = p.Y - startPoint.Y;
startPoint = p;
this.Left += deltaX;
this.Top += deltaY;
}
}
I just placed a panel on a form and implented the above, which worked.
#IInspector is right when he says that this approach is actually not very good, as it doesn't take into account that Windows can actually handle all this for you.
An alternative approach (and the better one) I'd take is:
Override the WndProc method as shown below
Done. No more mouse handling
The following is an example of how you could override WndProc to do what you need:
protected override void WndProc(ref Message m)
{
const UInt32 WM_NCHITTEST = 0x0084;
const UInt32 HTCAPTION = 0x2;
bool handled = false;
if (m.Msg == WM_NCHITTEST)
{
if (<cursor is within the caption area>)
{
m.Result = (IntPtr)HTCAPTION;
handled = true;
}
}
if (!handled)
base.WndProc(ref m);
}
To restrict the area in which the window can be moved, you could use the following. Of course, #IInspectable will disagree, but you went this road :-)
private void lblHeader_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
Point p = Cursor.Position;
int deltaX = p.X - startPoint.X;
int deltaY = p.Y - startPoint.Y;
startPoint = p;
int fX = this.Left + deltaX;
int fY = this.Top + deltaY;
if (fX >= 0 && fX + this.Width < Area.Width) this.Left = fX;
if (fY >= 0 && f> + this.Height < Area.Height) this.Top = fY;
}
}
I'm using WinForms. In my form i have a picturebox with an image. How can i paint the picturebox but not the area inside the expanding square. Here is my code. Currently i could create the expanding square but i don't know how to paint the picturebox white outside of that square.
int _cropX, _cropY, _cropWidth, _cropHeight;
private State _currentState;
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (Crop_Checkbox.Checked == true)
{
if (_currentState == State.Crop)
{
Cursor = Cursors.Cross;
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
//X and Y are the coordinates of Crop
pictureBox1.Refresh();
_cropWidth = e.X - _cropX;
_cropHeight = e.Y - _cropY;
pictureBox1.CreateGraphics().DrawRectangle(_cropPen, _cropX, _cropY, _cropWidth, _cropHeight);
}
}
}
else
{
Cursor = Cursors.Default;
}
}
private void Crop_Checkbox_CheckedChanged(object sender, EventArgs e)
{
if (Crop_Checkbox.Checked == true)
{
this.Cursor = Cursors.Cross;
}
}
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
if (Crop_Checkbox.Checked == true)
{
if (_currentState == State.Crop)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
Cursor = Cursors.Cross;
_cropX = e.X;
_cropY = e.Y;
_cropPen = new Pen(Color.FromArgb(153, 180, 209), 3); //2 is Thickness of line
_cropPen.DashStyle = DashStyle.DashDotDot;
pictureBox1.Refresh();
}
}
}
else
{
Cursor = Cursors.Default;
}
}
public Pen _cropPen;
private enum State
{
Crop
}
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
if (Crop_Checkbox.Checked == true)
{
//Paint picturebox...
}
else
{
Cursor = Cursors.Default;
}
}
This is best done with a GraphicsPath:
using System.Drawing.Drawing2D;
..
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Rectangle r1 = pictureBox1.ClientRectangle; // note I don't use width or height!
Rectangle r2 = new Rectangle(50, 30, 80, 40);
GraphicsPath gp = new GraphicsPath(FillMode.Alternate);
gp.AddRectangle(r1); // first the big one
gp.AddRectangle(r2); // now the one to exclude
e.Graphics.FillPath( Brushes.Gold, gp);
}
Note that I..
..use the Paint event for persistent grphics
..only paint onto the surface of the PictureBox, not into its image. See here for the difference!
You can add more rectangles or other shapes to exclude.
If you want image and surface combined, either draw into the image or ask the PictureBox to DrawToBitmap..
Download the project
I am trying to make a panel with a background color which should be able to be drawn in runtime when the user holds down left mouse button and moves it around. All works find when the user is starting from top left and go to bottom right just like the image shows:
But I want the user to be able to make the panel from bottom right to top left. Just like when you select something on your computer with your mouse
Here is my code for now:
public void parent_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
Point tempLoc = e.Location;
this.Location = new Point
(
Math.Min(this.Location.X, tempLoc.X),
Math.Min(this.Location.Y, tempLoc.Y)
);
this.Size = new Size
(
Math.Abs(this.Location.X - tempLoc.X),
Math.Abs(this.Location.Y - tempLoc.Y)
);
this.Invalidate();
}
}
I think this is where I go wrong, and I simply can't find the right algorithm for it:
this.Size = new Size
(
Math.Abs(this.Location.X - tempLoc.X),
Math.Abs(this.Location.Y - tempLoc.Y)
);
But if I use a rectangle it works fine, but I want my panel to be able to do it as well.
You need to just check the minimums and maximums of your starting point versus your mousemoving point. The problem with the code is you are using the control location as a starting point, but if you move the mouse from bottom-right to top-left, your location needs to change. A control can't have a negative size.
Here is how I re-wrote it (I removed unnecessary stuff for testing):
public class SelectionTool : Panel {
Form parent;
Point _StartingPoint;
public SelectionTool(Form parent, Point startingPoint) {
this.DoubleBuffered = true;
this.Location = startingPoint;
//this.endingPoint = startingPoint;
_StartingPoint = startingPoint;
this.parent = parent;
this.parent.Controls.Add(this);
this.parent.MouseMove += new MouseEventHandler(parent_MouseMove);
this.BringToFront();
this.Size = new Size(0, 0);
}
public void parent_MouseMove(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Left) {
int minX = Math.Min(e.Location.X, _StartingPoint.X);
int minY = Math.Min(e.Location.Y, _StartingPoint.Y);
int maxX = Math.Max(e.Location.X, _StartingPoint.X);
int maxY = Math.Max(e.Location.Y, _StartingPoint.Y);
this.SetBounds(minX, minY, maxX - minX, maxY - minY);
this.Invalidate();
}
}
protected override void OnPaint(PaintEventArgs e) {
base.OnPaint(e);
this.BackColor = Color.Blue;
}
}
Here is the code I used to test it on a form:
private SelectionTool _SelectPanel = null;
private void Form1_MouseMove(object sender, MouseEventArgs e) {
if (e.Button == System.Windows.Forms.MouseButtons.Left) {
if (_SelectPanel == null)
_SelectPanel = new SelectionTool(this, e.Location);
}
}
private void Form1_MouseUp(object sender, MouseEventArgs e) {
if (_SelectPanel != null) {
_SelectPanel.Dispose();
_SelectPanel = null;
}
}