C# 点位运动与 GDI+ 详解

150 阅读5分钟

前言

本文将介绍在 C# 中如何使用 GDI+ 来实现简单的点位运动。

主要包括以下内容:

GDI+ 绘图基础与关键类介绍

如何在窗体中进行平面坐标操作

利用计时器实现连续运动

高级思路:更加精准的运动控制与自定义轨迹

基础背景

GDI+(Graphics Device Interface Plus)是 Windows 绘图的基础库,用于在窗体上进行 2D 绘图。C# 通过 System.Drawing 命名空间提供了 GDI+ 的接口。

常用的绘图类和结构包括:

Graphics:图形绘制的核心类,支持绘制直线、矩形、文本等。

Pen:绘制线段等轮廓所需的画笔。

Brush:绘制填充区域时所需的画刷。

Point / PointF:表示坐标系中的一个点。

Rectangle / RectangleF:表示一个矩形区域。

在进行运动控制时,通常需要在指定的坐标轨迹上移动点或图形,并根据计时器或反馈线程来刷新位置,从而达到动画或稳定控制的目的。

2、示例:在 WinForms 窗体中移动一个点

以下示例演示如何在 WinForms 窗体中使用 GDI+ 并结合 Timer 实现一个点的运动。该示例包含完整的窗体类代码,可以直接复制并创建一个新的 WinForms 项目测试。

2.1、创建 WinForms 项目

打开 Visual Studio,创建一个新的“Windows 窗体应用程序”项目。

在默认生成的 Form1 中添加以下代码,或直接替换所有代码。

2.2、完整示例代码

using System;
using System.Drawing;
using System.Windows.Forms;

namespace PointMotionDemo
{
    public partial class Form1 : Form
    {
        // 点的位置
        private float _x = 50f;
        private float _y = 50f;

        // 水平与垂直方向速度
        private float _speedX = 2f;
        private float _speedY = 2f;

        // 窗体边界
        private float _minX, _maxX, _minY, _maxY;

        // 计时器
        private Timer _timer;

        public Form1()
        {
            InitializeComponent();

            // 设置双缓冲,减少闪烁
            this.DoubleBuffered = true;

            // 设定窗体大小
            this.ClientSize = new Size(600, 400);

            // 初始化计时器
            _timer = new Timer();
            _timer.Interval = 20; 
            // 20ms 刷新一次,相当于每秒 50 帧
            _timer.Tick += Timer_Tick;
            _timer.Start();

            // 计算窗体中点的合法运动边界
            _minX = 0;
            _maxX = this.ClientSize.Width;
            _minY = 0;
            _maxY = this.ClientSize.Height;
        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            // 移动点
            _x += _speedX;
            _y += _speedY;

            // 判断边界并反向
            if (_x < _minX || _x > _maxX)
            {
                _speedX = -_speedX;
            }
            if (_y < _minY || _y > _maxY)
            {
                _speedY = -_speedY;
            }

            // 重绘
            this.Invalidate();
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            Graphics g = e.Graphics;

            // 绘制移动的点(用一个小圆表示)
            float radius = 10f;
            RectangleF rect = new RectangleF(_x - radius / 2,
            _y - radius / 2, radius, radius);
            using (Brush brush = new SolidBrush(Color.Red))
            {
                g.FillEllipse(brush, rect);
            }

            // 画一个简单的坐标说明
            string info = $"点位置: ({_x:F1}, {_y:F1})";
            using (Brush textBrush = new SolidBrush(Color.Black))
            {
                g.DrawString(info, this.Font, 
                textBrush, new PointF(10, 10));
            }
        }
    }
}

代码说明

1、Timer 的使用:通过 Interval 控制刷新间隔,在 Tick 事件中更新点位坐标并调用 Invalidate() 进行重绘。

2、OnPaint 方法:使用 Graphics 对象绘制点和文字。

3、点位运动逻辑:最基础的做法是每次在坐标上累加速度。如果到达预设边界则将速度反向。

3、进阶:多点运动与轨迹控制

1、多点运动:可以为窗体中的多个点分别设置位置和速度,每次刷新时分别更新每个点并在绘制时循环调用 FillEllipse

2、复杂轨迹:在 _timer.Tick 中,可以根据某些数学规则(如正弦函数 sin(t))来计算坐标,或者根据预先定义的离散点数据进行插值,实现更加精细的轨迹。

3、定向到指定坐标:对于精确移动到目标坐标,可根据当前位置与目标位置计算方向向量并以固定速度移动,或实现类似 PID (比例-积分-微分)控制来实现平滑过渡。

多点运动

using Timer = System.Windows.Forms.Timer;

namespace AppMPointMotion
{
    public partial class Form1 : Form
    {
        // 定义一个点的类  
        private class MovingPoint
        {
            public float X { get; set; }
            public float Y { get; set; }
            public float SpeedX { get; set; }
            public float SpeedY { get; set; }
            public Color Color { get; set; }
        }

        // 点的集合  
        private List<MovingPoint> _points;

        // 窗体边界  
        private float _minX, _maxX, _minY, _maxY;

        // 计时器  
        private Timer _timer;

        public Form1()
        {
            InitializeComponent();

            // 设置双缓冲,减少闪烁  
            this.DoubleBuffered = true;

            // 设定窗体大小  
            this.ClientSize = new Size(600, 400);

            // 初始化点的集合  
            _points = new List<MovingPoint>
            {
                new MovingPoint
                {
                    X = 50f,
                    Y = 50f,
                    SpeedX = 2f,
                    SpeedY = 2f,
                    Color = Color.Red
                },
                new MovingPoint
                {
                    X = 100f,
                    Y = 100f,
                    SpeedX = -3f,
                    SpeedY = 3f,
                    Color = Color.Blue
                },
                new MovingPoint
                {
                    X = 200f,
                    Y = 200f,
                    SpeedX = 4f,
                    SpeedY = -4f,
                    Color = Color.Green
                }
            };

            // 初始化计时器  
            _timer = new Timer();
            _timer.Interval = 20; 
            // 20ms 刷新一次,相当于每秒 50 帧  
            _timer.Tick += Timer_Tick;
            _timer.Start();

            // 计算窗体中点的合法运动边界  
            _minX = 0;
            _maxX = this.ClientSize.Width;
            _minY = 0;
            _maxY = this.ClientSize.Height;
        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            // 移动每个点  
            foreach (var point in _points)
            {
                point.X += point.SpeedX;
                point.Y += point.SpeedY;

                // 判断边界并反向  
                if (point.X < _minX || point.X > _maxX)
                {
                    point.SpeedX = -point.SpeedX;
                }
                if (point.Y < _minY || point.Y > _maxY)
                {
                    point.SpeedY = -point.SpeedY;
                }
            }

            // 重绘  
            this.Invalidate();
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            Graphics g = e.Graphics;

            // 绘制移动的点(用小圆表示)  
            float radius = 10f;
            foreach (var point in _points)
            {
                RectangleF rect = new RectangleF(point.X - radius / 2,
                point.Y - radius / 2, radius, radius);
                using (Brush brush = new SolidBrush(point.Color))
                {
                    g.FillEllipse(brush, rect);
                }
            }

            // 画点位置信息  
            float yOffset = 10f;
            foreach (var point in _points)
            {
                string info = $"点位置: ({point.X:F1}, {point.Y:F1})";
                using (Brush textBrush = new SolidBrush(point.Color))
                {
                    g.DrawString(info, this.Font, textBrush, 
                    new PointF(10, yOffset));
                    yOffset += 20f;
                }
            }
        }
    }
}

正弦曲线轨迹

private float _time = 0f;

private void Timer_Tick(object sender, EventArgs e)
{
    // 正弦曲线轨迹  
    _time += 0.1f;
    _x = ClientSize.Width / 2 + (float)(100 * Math.Sin(_time));
    _y = ClientSize.Height / 2 + (float)(100 * Math.Cos(_time));

    this.Invalidate();
}

贝塞尔曲线轨迹

private PointF[] _controlPoints;  
private float _t = 0f;  

private void InitializeBezierCurve()  
{  
    _controlPoints = new PointF[]  
    {  
        new PointF(50, 50),  
        new PointF(200, 10),  
        new PointF(350, 200),  
        new PointF(500, 300)  
    };  
}  

private PointF CalculateBezierPoint(float t)  
{  
    // 三次贝塞尔曲线插值  
    float u = 1 - t;  
    float tt = t * t;  
    float uu = u * u;  
    float uuu = uu * u;  
    float ttt = tt * t;  

    PointF p = new PointF(  
        uuu * _controlPoints[0].X +  
        3 * uu * t * _controlPoints[1].X +  
        3 * u * tt * _controlPoints[2].X +  
        ttt * _controlPoints[3].X,  
        uuu * _controlPoints[0].Y +  
        3 * uu * t * _controlPoints[1].Y +  
        3 * u * tt * _controlPoints[2].Y +  
        ttt * _controlPoints[3].Y  
    );  

    return p;  
}  

private void Timer_Tick(object sender, EventArgs e)  
{  
    _t += 0.01f;  
    if (_t > 1) _t = 0;  

    PointF point = CalculateBezierPoint(_t);  
    _x = point.X;  
    _y = point.Y;  

    this.Invalidate();  
}

物理模拟轨迹(带阻尼)

private float _vx = 2f;
private float _vy = 2f;
private float _damping = 0.99f;
private float _gravity = 0.5f;

private void Timer_Tick(object sender, EventArgs e)
{
    // 简单物理模拟  
    _vy += _gravity;
    _x += _vx;
    _y += _vy;

    // 边界反弹并衰减速度  
    if (_x < _minX || _x > _maxX)
    {
        _vx = -_vx * _damping;
    }
    if (_y < _minY || _y > _maxY)
    {
        _vy = -_vy * _damping;
    }

    this.Invalidate();
}

性能与注意事项

双缓冲技术

使用 双缓冲(Double-Buffered) 技术可以显著减少绘图时的闪烁现象。

在 WinForms 中,可以通过设置窗体的 DoubleBuffered 属性为 true 来启用双缓冲。

public Form1()
{
    InitializeComponent();
    this.DoubleBuffered = true;
}

绘图效率

在 OnPaint 方法中进行复杂操作时,需要注意绘图效率,避免在绘制线程中进行过度计算。复杂的计算应尽量提前或在后台线程中完成,以减轻主线程的负担。

复杂运动控制

如果需要进行更复杂或专业的运动控制(如 CNC、运动控制卡等),则需结合第三方库或硬件控制接口。GDI+ 适用于简单的图形绘制和基本动画,对于高级应用场景可能需要其他工具或库的支持。

总结

利用 GDI+ 在 C# 窗体中实现点位运动是学习 2D 图形绘制和基本运动控制的良好起点。通过定时刷新坐标并重绘图形,可以实现各种动画效果和交互功能。

以下是一些关键点:

基础绘图类:了解并掌握 Graphics、Pen、Brush、Point 和 Rectangle 等常用绘图类。

计时器应用:利用 Timer 控件定期更新点的位置,从而实现连续运动。

双缓冲技术:使用双缓冲技术减少绘图时的闪烁问题。

高效绘图:在 OnPaint 方法中保持高效的绘图操作,避免复杂的计算。

另外,更高级的工具或算法(如 WPF 的动画机制、DirectX/OpenGL、PID 控制算法等)都可以在此基础上结合,创造更丰富的应用场景。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:技术老小子

出处:mp.weixin.qq.com/s/0ZlnniFnnvgb625xnE7dhw

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!