我想在c#中制作一个箭头,例如,在5秒内从位置到B位置。我想将地图图像放在表单中,当我单击一个按钮时,我想以几秒钟的间隔将箭头从位置绘制到B位置。当它处于水平位置时,我已经制作了一个箭头,但是当我尝试使它倾斜时,它会使我绘制一个三角形,而不是箭头,我不知道该如何修复它。在这里,我从宽度为300
的位置的箭头制作了一个箭头,我尝试用斜箭头做同样的东西,但是当我放置不同的位置时,它会使我一个三角形而不是箭头。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace WindowsFormsApp4
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(400, 273);
this.Text = "";
this.Resize += new System.EventHandler(this.Form1_Resize);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
}
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.FillRectangle(Brushes.White, this.ClientRectangle);
Pen p = new Pen(Color.Black, 5);
p.StartCap = LineCap.Round;
for(int i=1; i<=300;i++)
{
System.Threading.Thread.Sleep(2);
g.DrawLine(p, 12, 30, i, 30);
Cursor.Current = Cursors.Default;
}
p.EndCap = LineCap.ArrowAnchor;
g.DrawLine(p, 12, 30, 310, 30);
p.Dispose();
}
private void Form1_Resize(object sender, System.EventArgs e)
{
Invalidate();
}
}
}
代码的基本问题是您在Paint
事件处理程序内执行整个动画循环。这意味着您绘制的每一行之间永远不会清除窗口,因此您可以获得您所绘制的行副本的所有查看。
从您的问题中不清楚您期望在屏幕上看到什么。但是,代码的另一个潜在问题是,该行的移动终点不是从行的起点开始,而是在同一Y坐标的点上,您希望线路结束。这意味着线的箭头末端横穿导致最终终点的水平线,而不是从线的起点逐渐延伸。
还有一个小点,您似乎对DrawLine()
方法的做法感到困惑。您声明行的宽度为300,但实际上DrawLine()
方法的第二个参数只是另一点。该行的宽度由您用于绘制行的Pen
定义。包含行的盒子的宽度由起点和终点定义,但在这种情况下不是300,而是(在线的最后长度)中,您的start x坐标之间的差异和结束x坐标(即288)。
可以通过运行Paint
事件处理程序的循环来解决上述基本问题,该 更新描述该行的值,然后调用Invalidate()
,以便可以称为Paint
事件处理程序绘制只是动画的当前状态。
假设您真正想要的是要从起点延伸,而不是跨越水平线,我下面显示的示例也以这种方式实现了动画,此外还可以解决基本问题。我没有采取任何行动改变行的长度或宽度。
public Form1()
{
InitializeComponent();
this.DoubleBuffered = true;
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(400, 273);
this.Resize += new System.EventHandler(this.Form1_Resize);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.Form1_Paint);
var task = AnimateLine();
}
private readonly Point _lineStart = new Point(12, 30);
private readonly Point _lineFinalEnd = new Point(300, 60);
private const int _animateSteps = 300;
private Point _lineCurrentEnd;
private bool _drawArrow;
private async Task AnimateLine()
{
Size size = new Size(_lineFinalEnd) - new Size(_lineStart);
for (int i = 1; i <= _animateSteps; i++)
{
await Task.Delay(2);
Size currentSize = new Size(
size.Width * i / _animateSteps, size.Height * i / _animateSteps);
_lineCurrentEnd = _lineStart + currentSize;
Invalidate();
}
_drawArrow = true;
Invalidate();
}
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
using (Pen p = new Pen(Color.Black, 5))
{
p.StartCap = LineCap.Round;
p.EndCap = _drawArrow ? LineCap.ArrowAnchor : p.EndCap;
g.DrawLine(p, _lineStart, _lineCurrentEnd);
}
}
请注意,重复的擦除和重新刷新窗户将使窗户闪烁。这是任何一种动画的基本问题,修复程序是为窗口启用双重屏障,因此this.DoubleBuffered = true;
语句添加到构造函数中。
其他值得一提的是:
- 使用
await Task.Delay()
调用,以便循环可以在循环的每次迭代中产生UI线程,从而允许UI线程提高Paint
事件,并允许在动画期间其他任何其他UI活动仍能工作。您可以在如何以及何时使用async
和await
文章中找到有关该C#功能的更多信息,当然,通过阅读文档。 - 无论您使用
Thread.Sleep()
还是Task.Delay()
,指定2 ms的延迟不是很有用。Windows线程调度程序不会将线程安排到该精度。在正常情况下,在50毫秒后,甚至在CPU承受沉重的负载下,可以唤醒2毫秒的线程。2 ms延迟也没有提供有用的动画帧速率;那将是一个500 Hz的刷新率,比人脑的需求更快或更快,以感知一个平稳的动画。
我的上面的示例无助于解决这个问题,但是您应该以略有不同的范围探索实现循环,以便指定合理的动画间隔(例如每50或100 ms),而不是动画步骤的数量,试图延迟该间隔,然后使用Stopwatch
来实际测量真正的延迟是什么,并根据经过的实际时间计算动画中的进度。这将使您能够精确控制动画的总持续时间,以及对动画使用的刷新率的精确控制。