双缓冲可以用来解决与多个画图操作相关的闪烁问题,它的原理是构建一个虚拟的画板,在虚拟的画板上画完之后再更新的实际需要显示的画板上。
多图层则是为了更好的控制绘板,精准的控制让某一图层消失或者重画。
简易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));
这个方法的参数是重新绘制的区域,区域外的内容其不去更新。良好使用可以提升性能