C# Graphics 实现双缓冲多图层

双缓冲可以用来解决与多个画图操作相关的闪烁问题,它的原理是构建一个虚拟的画板,在虚拟的画板上画完之后再更新的实际需要显示的画板上。
多图层则是为了更好的控制绘板,精准的控制让某一图层消失或者重画。

简易MyGraphics类

internal class MyGraphics
    {
        // 定义一个 List 存储所有图层
        private List<Layer> Layers = new List<Layer>();

        public int LayerCount => Layers.Count;

        /// <summary>
        /// 创建指定大小的图层
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        public void CreateLayer(int width, int height, string name = "未命名图层")
        {
            if (name == "未命名图层")
                name += LayerCount;
            var layer = new Bitmap(width, height);
            Layers.Add(new Layer() { name = name, bitmap = layer });
        }

        public void InitLayer(int width, int height)
        {
            while (LayerCount < 5)
            {
                CreateLayer(width, height);
            }
        }

        /// <summary>
        ///  是否存在图层
        /// </summary>
        /// <param name="name">图层名</param>
        /// <returns></returns>
        public bool isExist(string name)
        {
            return Layers.Where(x => x.name == name).ToList().Count > 0;
        }

        // 获取指定索引的图层
        public Bitmap GetLayer(int index)
        {
            return index > LayerCount - 1 ? null : Layers[index].bitmap;
        }

        public Bitmap GetLayer(string name)
        {
            var list = Layers.Where(x => x.name == name).ToList();
            return list.Count == 0 ? null : list[0].bitmap;
        }

        // 清空指定索引的图层
        public void ClearLayer(int index)
        {
            var g = Graphics.FromImage(Layers[index].bitmap);
            g.Clear(Color.Transparent);
        }

        public void ClearLayer(string name)
        {
            var list = Layers.Where(x => x.name == name).ToList();

            if (list.Count <= 0) return;

            var g = Graphics.FromImage(list[0].bitmap);
            g.Clear(Color.Transparent);
        }

        public void ClearAll()
        {
            foreach (var g in Layers.Select(layer => Graphics.FromImage(layer.bitmap)))
            {
                g.Clear(Color.Transparent);
            }
        }


        public void Dispose()
        {
            foreach (var layer in Layers)
            {
                layer.bitmap.Dispose();
            }

            Layers.Clear();
        }

        // 将所有图层绘制到画布上
        public void Draw(Graphics g)
        {
            foreach (var layer in Layers)
            {
                g.DrawImage(layer.bitmap, 0, 0);
            }
        }
    }

    public class Layer
    {
        public string id = Guid.NewGuid().ToString();

        public string name;

        public Bitmap bitmap;
    }

简易myGraphics类中很多方法我进行了修改和完善,此处所贴代码已足够完成基本功能。
如果想直接copy,直接新建一个类myGraphics.cs 再把代码copy进去。

具体使用方法:

/// <summary>
        ///  背景图层初始化
        /// </summary>
        private void BackgroundLayerInit()
        {
            var g = myGraphics.CreateLayer(pictureBox1.Size, "背景图层", false);

            var rect = new Rectangle(new Point(0, 0), pictureBox1.Size);

            var b1 = new SolidBrush(Color.Black);//定义单色画刷
            g.FillRectangle(b1, rect);//填充这个矩形
        }

        private void GraphLayerInit()
        {
            var g = myGraphics.CreateLayer(pictureBox1.Size, "图形图层");
            Pen p = new Pen(Color.Blue, 0.05f);//定义了一个蓝色,宽度为的画笔

            DrawPrimitive(myPrimitiveList, g, p);
        }

先创建图层,并在图层上绘制你想要的内容

private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            lock (LockObject.GetLayerGraphicsLock)
            {
                var g = e.Graphics;
                myGraphics.Draw(g);
            }
        }

然后在窗体或者控件的OnPaint事件中进行绘制,
这个OnPaint事件会在初始化时执行一次,后续并不会主动触发,需要调用Invalidate()触发该事件。

你可以发现我这里面加了一个锁,这个锁是为了避免OnPaint事件绘制期间,其他线程再次触发。

public Graphics GetLayerGraphics(string name)
        {
            // 绘制的地方和此处同时加锁, 禁止在绘图时获取Graphics对象
            lock (LockObject.GetLayerGraphicsLock)
            {
                var list = Layers.Where(x => x.name == name).ToList();
                if (list.Count == 0) return null;
                var g = Graphics.FromImage(list[0].bitmap);
                GraphicsEvent.GetLayerGraphicsEvent?.Invoke(g);
                return g;
            }
        }

所以这个锁另外一个位置是加在MyGraphics类里面的,绘制期间禁止外部获取图层对象。简易版myGraphics类中获取layer返回的是bitmap对象,实际上我每次获取到bitmap对象之后100%会转化为graphics对象,所以在此处我直接新写了一个方法,直接给我返回某个图层的Graphics 对象,并且在里面添加了事件。如此,可以把一些获取myGraphics之后的重复操作封装为方法在外部订阅此事件。

private void OnGetLayerGraphics(Graphics g)
        {
            g.TranslateTransform(lastEX, lastEY);
            g.ScaleTransform(coefficient, -coefficient);
        }

我的代码中订阅的是转换坐标系的操作,每次我获取Graphics 对象后,会主动转换坐标系,这个与主题无关,只是随口一提,可以无视。

上面展示了初始化图层,下面展示变化某一个图层

private void UpdateLaserLocation(float laserX, float laserY)
        {
            var g = myGraphics.GetClearlyLayerGraphics("激光图层");

            var x1 = laserX;
            var y1 = laserY;
            var radius = 10f / coefficient;

            g.DrawEllipse(whitePen, x1 - radius, y1 - radius, radius * 2, radius * 2);
            //laserGraphics.FillEllipse(Brushes.Cyan, x, y, radius, radius);
            //laserGraphics.DrawEllipse(Pens.Red, new Rectangle((int)x +1, (int)y + 1, 1, 1));
            g.DrawLine(getMyPen(Color.Yellow), x1, y1 - radius, x1, y1 + radius);
            g.DrawLine(getMyPen(Color.Yellow), x1 - radius, y1, x1 + radius, y1);
            lastLaserX = x1;
            lastLaserY = y1;
            //下面应该加一句这个触发重新绘制,但是我的逻辑在外面加了 所以此处省略
            // 我的绘画容器是pictureBox1控件,所以用触发该控件的OnPaint事件
            // 如果你的容器是Form就把pictureBox1改为Form对象就行
           // pictureBox1.Invalidate(new Rectangle(new Point(0, 0), pictureBox1.Size));
        }

首先获取图层,获取图层之后,如果是bitmap对象转化为graphics对象,调用graphics对象的clear方法清除图层。因为我这里已经把这些操作封装进GetClearlyLayerGraphics()这个方法里面了,我调用这个方法就直接获取到了graphics对象并且已经完成了Clear操作。
下面再重新绘制
绘制完成之后调用Invalidate()方法触发绘画容器的OnPaint事件
我此处注释掉了,因为我的逻辑是在激光图层变更是和另外一个图层一起变更的,那个图层会调用Invalidate(),所以这里就不需要加。

pictureBox1.Invalidate(new Rectangle(new Point(0, 0), pictureBox1.Size));

这个方法的参数是重新绘制的区域,区域外的内容其不去更新。良好使用可以提升性能

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值