这个示例允许用户绘制和移动线段。它允许您根据鼠标下方的内容执行三种不同的操作。
- 当鼠标位于某个线段上时,光标会变成手的形状。然后您可以单击并拖动来移动该线段。
- 当鼠标位于线段的终点上时,光标会变成箭头。然后您可以单击并拖动以移动终点。
- 当鼠标悬停在空白处时,您可以单击并拖动来绘制新的线段。
程序使用MouseDown、MouseMove和MouseUp事件处理所有这些情况,但在一组事件处理程序中处理所有可能的组合会造成混乱。为了便于管理,程序使用单独的MouseMove和MouseUp事件处理程序来执行不同的任务。
这篇文章分为以下几个部分,与程序的基本状态相对应。
- 绘画
- 什么都没动
- 绘制新线段
- 移动端点
- 移动线段
- 下一步是什么?
绘画
程序将线段端点的坐标存储在列表Pt1和Pt2中。
// The points that make up the line segments.
private List Pt1 = new List<Point>();
private List Pt2 = new List<Point>();
绘制新线段时,变量IsDrawing为真,程序将新线段的端点存储在变量NewPt1和NewPt2中。
// Points for the new line.
private bool IsDrawing = false;
private Point NewPt1, NewPt2;
Paint事件处理 程序只是循环遍历Pt1和Pt2列表,绘制线段及其端点。然后绘制新线(如果您正在绘制一条线)。
// Draw the lines.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
// Draw the segments.
for (int i = 0; i < Pt1.Count; i++)
{
// Draw the segment.
e.Graphics.DrawLine(Pens.Blue, Pt1[i], Pt2[i]);
}
// Draw the end points.
foreach (Point pt in Pt1)
{
Rectangle rect = new Rectangle(
pt.X - object_radius, pt.Y - object_radius,
2 * object_radius + 1, 2 * object_radius + 1);
e.Graphics.FillEllipse(Brushes.White, rect);
e.Graphics.DrawEllipse(Pens.Black, rect);
}
foreach (Point pt in Pt2)
{
Rectangle rect = new Rectangle(
pt.X - object_radius, pt.Y - object_radius,
2 * object_radius + 1, 2 * object_radius + 1);
e.Graphics.FillEllipse(Brushes.White, rect);
e.Graphics.DrawEllipse(Pens.Black, rect);
}
// If there's a new segment under constructions, draw it.
if (IsDrawing)
{
e.Graphics.DrawLine(Pens.Red, NewPt1, NewPt2);
}
}
什么都没动
如果鼠标移动而您没有移动线段或终点,则会执行以下事件处理程序。
// The mouse is up. See whether we're over an end point or segment.
private void picCanvas_MouseMove_NotDown(object sender,
MouseEventArgs e)
{
Cursor new_cursor = Cursors.Cross;
// See what we're over.
Point hit_point;
int segment_number;
if (MouseIsOverEndpoint(e.Location, out segment_number,
out hit_point))
new_cursor = Cursors.Arrow;
else if (MouseIsOverSegment(e.Location, out segment_number))
new_cursor = Cursors.Hand;
// Set the new cursor.
if (picCanvas.Cursor != new_cursor)
picCanvas.Cursor = new_cursor;
}
此代码调用后面描述的MouseIsOverEndPoint和MouseIsOverSegment方法来查看鼠标是否位于任何有趣的东西上。然后它显示相应的光标。(如果位于端点上,则显示箭头;如果位于线段上,则显示移交;如果位于空处,则显示交叉。)
如果您没有移动任何内容并按下鼠标,则会执行以下事件处理程序。
// See what we're over and start doing whatever is appropriate.
private void picCanvas_MouseDown(object sender, MouseEventArgs e)
{
// See what we're over.
Point hit_point;
int segment_number;
if (MouseIsOverEndpoint(e.Location, out segment_number,
out hit_point))
{
// Start moving this end point.
picCanvas.MouseMove -= picCanvas_MouseMove_NotDown;
picCanvas.MouseMove += picCanvas_MouseMove_MovingEndPoint;
picCanvas.MouseUp += picCanvas_MouseUp_MovingEndPoint;
// Remember the segment number.
MovingSegment = segment_number;
// See if we're moving the start end point.
MovingStartEndPoint =
(Pt1[segment_number].Equals(hit_point));
// Remember the offset from the mouse to the point.
OffsetX = hit_point.X - e.X;
OffsetY = hit_point.Y - e.Y;
}
else if (MouseIsOverSegment(e.Location, out segment_number))
{
// Start moving this segment.
picCanvas.MouseMove -= picCanvas_MouseMove_NotDown;
picCanvas.MouseMove += picCanvas_MouseMove_MovingSegment;
picCanvas.MouseUp += picCanvas_MouseUp_MovingSegment;
// Remember the segment number.
MovingSegment = segment_number;
// Remember the offset from the mouse
// to the segment's first point.
OffsetX = Pt1[segment_number].X - e.X;
OffsetY = Pt1[segment_number].Y - e.Y;
}
else
{
// Start drawing a new segment.
picCanvas.MouseMove -= picCanvas_MouseMove_NotDown;
picCanvas.MouseMove += picCanvas_MouseMove_Drawing;
picCanvas.MouseUp += picCanvas_MouseUp_Drawing;
IsDrawing = true;
NewPt1 = new Point(e.X, e.Y);
NewPt2 = new Point(e.X, e.Y);
}
}
此方法使用MouseIsOverEndPoint和MouseIsOverSegment方法来查看鼠标是否位于任何有趣的对象上。如果鼠标位于端点或线段上,则代码开始移动该对象。
注意代码如何卸载picCanvas_MouseMove_NotDown事件处理程序并为其启动的操作 安装新的MouseMove和MouseUp事件处理程序。
以下代码显示MouseIsOverEndPoint和MouseIsOverSegment方法。
// See if the mouse is over an end point.
private bool MouseIsOverEndpoint(Point mouse_pt,
out int segment_number, out Point hit_pt)
{
for (int i = 0; i < Pt1.Count; i++ )
{
// Check the starting point.
if (FindDistanceToPointSquared(mouse_pt, Pt1[i]) <
over_dist_squared)
{
// We're over this point.
segment_number = i;
hit_pt = Pt1[i];
return true;
}
// Check the end point.
if (FindDistanceToPointSquared(mouse_pt, Pt2[i]) <
over_dist_squared)
{
// We're over this point.
segment_number = i;
hit_pt = Pt2[i];
return true;
}
}
segment_number = -1;
hit_pt = new Point(-1, -1);
return false;
}
// See if the mouse is over a line segment.
private bool MouseIsOverSegment(Point mouse_pt,
out int segment_number)
{
for (int i = 0; i < Pt1.Count; i++)
{
// See if we're over the segment.
PointF closest;
if (FindDistanceToSegmentSquared(
mouse_pt, Pt1[i], Pt2[i], out closest)
< over_dist_squared)
{
// We're over this segment.
segment_number = i;
return true;
}
}
segment_number = -1;
return false;
}
这些方法只是调用FindDistanceToPointSquared和FindDistanceToSegmentSquared方法。FindDistanceToPointSquared很简单。有关FindDistanceToSegmentSquared工作原理的说明,请参阅文章“在 C# 中查找点和线段之间的最短距离”。
该程序测试距离的平方,因此不需要计算平方根,因为平方根相对较慢。请注意,当且仅当 x 2 < y 2时,x < y 才成立,因此此测试仍可确定对象是否在鼠标所需的距离内。
绘制新线段
以下代码显示了绘制新线段时处于活动状态的 MouseMove和MouseUp事件处理程序。
// We're drawing a new segment.
private void picCanvas_MouseMove_Drawing(object sender,
MouseEventArgs e)
{
// Save the new point.
NewPt2 = new Point(e.X, e.Y);
// Redraw.
picCanvas.Invalidate();
}
// Stop drawing.
private void picCanvas_MouseUp_Drawing(object sender,
MouseEventArgs e)
{
IsDrawing = false;
// Reset the event handlers.
picCanvas.MouseMove -= picCanvas_MouseMove_Drawing;
picCanvas.MouseMove += picCanvas_MouseMove_NotDown;
picCanvas.MouseUp -= picCanvas_MouseUp_Drawing;
// Create the new segment.
Pt1.Add(NewPt1);
Pt2.Add(NewPt2);
// Redraw.
picCanvas.Invalidate();
}
当鼠标移动时,MouseMove事件处理程序会更新NewPt2的值以保存鼠标的当前位置。然后,它使程序的PictureBox无效,以便其Paint事件处理程序绘制当前段和正在进行的新段。
当释放鼠标时,MouseUp事件处理程序将恢复“不移动任何内容”事件处理程序,将新段的点添加到Pt1和Pt2列表中,并使PictureBox无效以重新绘制。
移动端点
以下代码显示了移动端点时处于活动状态的 MouseMove和MouseUp事件处理程序
3
当鼠标移动时,MouseMove事件处理程序会更新移动点的位置,然后使PictureBox无效并使其重新绘制。MouseUp事件处理程序只是恢复“不移动任何内容”事件处理程序并重新绘制。
移动线段
以下代码显示了移动端点时处于活动状态的 MouseMove和MouseUp事件处理程序。
3
当鼠标移动时,MouseMove事件处理程序会更新线段端点的位置并重绘以显示新位置。MouseUp事件处理程序只是恢复“不移动任何内容”事件处理程序并重绘。
另:允许您将线段和端点捕捉到网格。
当绘图PictureBox调整大小或者选中或取消选中“对齐网格”复选框时,程序将调用以下代码所示的 MakeBackgroundGrid方法。
如果未选中复选框,则此方法将picCanvas控件的背景设置为 null。否则,它将制作一个适合PictureBox 的位图,在其上绘制点以显示网格,并将PictureBox的BackgroundImage属性设置为位图。
程序的另一个变化是它处理新点的方式。每当程序要对某个点执行某些操作时,它都会调用以下SnapToGrid方法将该点的坐标捕捉到网格(如果合适)。
该方法将其x和y参数四舍五入为网格大小的最接近倍数。
以下代码显示了程序如何使用SnapToGrid方法的示例。当用户移动线段的终点时,将执行此事件处理程序。
private void MakeBackgroundGrid()
{
if (!chkSnapToGrid.Checked)
{
picCanvas.BackgroundImage = null;
}
else
{
Bitmap bm = new Bitmap(
picCanvas.ClientSize.Width,
picCanvas.ClientSize.Height);
for (int x = 0; x < picCanvas.ClientSize.Width;
x += grid_gap)
{
for (int y = 0; y < picCanvas.ClientSize.Height;
y += grid_gap)
{
bm.SetPixel(x, y, Color.Black);
}
}
picCanvas.BackgroundImage = bm;
}
}
// Snap to the nearest grid point.
private void SnapToGrid(ref int x, ref int y)
{
if (!chkSnapToGrid.Checked) return;
x = grid_gap * (int)Math.Round((double)x / grid_gap);
y = grid_gap * (int)Math.Round((double)y / grid_gap);
}
// We're moving an end point.
private void picCanvas_MouseMove_MovingEndPoint(
object sender, MouseEventArgs e)
{
// Move the point to its new location.
int x = e.X + OffsetX;
int y = e.Y + OffsetY;
SnapToGrid(ref x, ref y);
if (MovingStartEndPoint)
Pt1[MovingSegment] = new Point(x, y);
else
Pt2[MovingSegment] = new Point(x, y);
// Redraw.
picCanvas.Invalidate();
}