GDI下用双缓冲实现橡皮筋技术C-sharp

橡皮筋效果在图形系统中是很常见也很实用的功能。
在唐荣锡的《计算机图形学教程》中介绍为:“所谓橡皮筋技术就是在起点确定后,光标移出去定终点时,在屏幕上始终显示一条连结起点和光标中心的的直线,这条直线随着光标中心位置的变动而变动。

可以知道,一个实现过程如下:

  1. 按下鼠标左键:记录该点坐标。
  2. 移动鼠标:画出上一个端点到鼠标所在点的直线,并删除或者覆盖掉上一条直线。
  3. 再次点击左键时,又选择一个点,相当于回到第一步

用双缓冲技术实现的效果是比较好的,而用自动撤销线即:
ControlPaint.DrawReversibleLine(Point start,Point end, Color BackColor)
闪烁比较严重。
橡皮筋技术画多边形.jpg

下面给出C#中用双缓冲实现橡皮筋的代码,代码中注释还是相对详细的。该代码在VS2017下运行良好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Threading.Tasks;
using System.Windows.Forms;
/*用双缓冲技术实现GDI+下的橡皮筋效果
*控件:rubberInGDIplus--主窗体;intimePoiLbl--实时坐标展示
* 左键选点,中键删除最后一个选择的点;右键完成多边形选择;Del键可删除所有点;
* */
namespace rubberInGDIplus {
public partial class rubberEffectForm : Form {
Pen rubPen = new Pen(Color.SpringGreen, 2);//橡皮筋效果用笔;rubber pen
Point readPoi; //intime point
bool useRubber = true;
Graphics gp,gh;
private Bitmap bitmap = null;//虽然可以不用怎么多的Bitmap和 Graphics
public List<Point> poilst = new List<Point>(); //多边形端点

public rubberEffectForm() {
InitializeComponent();
this.Paint += new PaintEventHandler(this.rubberEffectForm_Paint); //初始化载入的像素方格
this.MouseClick += new MouseEventHandler(this.rubberEffectForm_Click); //监听点击事件
this.MouseMove += new MouseEventHandler(this.rubberEffectForm_MouseMove); //监听鼠标移动事件
this.KeyUp += new KeyEventHandler(this.rubberEffectForm_KeyUp);//键盘按键事件
//激活双缓冲技术
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);

}

private void rubberEffectForm_Load(object sender, EventArgs e) {

}
private void rubberEffectForm_Paint(object sender, PaintEventArgs e) {
gh = e.Graphics;
bitmap = new Bitmap(ClientSize.Width, ClientSize.Height);

gp = Graphics.FromImage(bitmap);
gp.Clear(this.BackColor);
gp.SmoothingMode = SmoothingMode.AntiAlias;//设置抗锯齿平滑模式
if (useRubber && readPoi != null) {//橡皮筋在使用中
int plct = poilst.Count;
if (plct == 0) {//还没有点
} else if (plct == 1) {//只存了一个点
gp.DrawLine(rubPen, poilst[0], readPoi);
} else {//两点及以上
for (int i = 0; i < plct; i++) {
if (i == plct - 1) {
gp.DrawLine(rubPen, poilst[0], readPoi);
gp.DrawLine(rubPen, poilst[i], readPoi);
} else {
gp.DrawLine(rubPen, poilst[i], poilst[i + 1]);
}
}
}
} else if (useRubber == false && readPoi != null) {//按下中键后
int plct = poilst.Count;
if (plct == 0 | plct == 1) {
} else {//两点及以上
for (int i = 0; i < plct; i++) {
if (i == plct - 1) {
gp.DrawLine(rubPen, poilst[0], poilst[i]);
} else {
gp.DrawLine(rubPen, poilst[i], poilst[i + 1]);
}
}
}
}


gh.DrawImage(bitmap, 0, 0);//display

}

private void rubberEffectForm_Click(object sender, MouseEventArgs e) {

if (e.Button == MouseButtons.Left) {//鼠标左击
useRubber = true;
Point readPoint = this.PointToClient(Control.MousePosition);//基于工作区的坐标
readPoi = readPoint;
intimePoiLbl.Text = readPoint.ToString();
//drawVertex(gp, readPoint); //画端点(顶点) 由于橡皮筋的覆盖,端点看不出来
poilst.Add(readPoint);//加点到list<point>里

} else if (e.Button == MouseButtons.Right) { //右键
useRubber = false;
this.Refresh();

//drawRim();//画边框
} else if (e.Button == MouseButtons.Middle) { //中键
int plast = poilst.Count - 1;
poilst.RemoveAt(plast);
useRubber = true;
this.Refresh();
}

}

private void rubberEffectForm_MouseMove(object sender, MouseEventArgs e) {
readPoi = this.PointToClient(Control.MousePosition);//基于工作区的坐标
Graphics gw = this.CreateGraphics();
if (useRubber) { //在橡皮筋模式内
gw.Clear(BackColor);
int plct = poilst.Count;
if (plct == 0) {// ==0: pass
} else if (plct == 1) {
gw.DrawLine(rubPen, poilst[0], readPoi);
} else {//两点及以上
for (int i = 0; i < plct; i++) {
if (i == plct - 1) {//画到最后一点了
gw.DrawLine(rubPen, poilst[i], readPoi);
gw.DrawLine(rubPen, poilst[0], readPoi);
} else {
gw.DrawLine(rubPen, poilst[i], poilst[i + 1]);
}
}
}

} else {
}
intimePoiLbl.Text = readPoi.ToString();
}

private void rubberEffectForm_KeyUp(object sender, KeyEventArgs e) {
if (e.KeyCode == Keys.Delete) {
poilst.Clear(); //画的点也要清除
this.Refresh();
} else if (e.KeyCode == Keys.Back | e.KeyCode == Keys.Escape) {
int plast = poilst.Count - 1;
poilst.RemoveAt(plast);
useRubber = true;
this.Refresh();
}

}

#region 可用可不用的函数
//画端点(顶点)
private void drawVertex(Graphics g, Point poi) {
Size sz = new Size(4, 4);
g.FillEllipse(Brushes.Red, new Rectangle(poi, sz));
}
private void drawRim() {//画边框
if (poilst.Count == 0)
return;
Point[] poi = new Point[poilst.Count];
for (int i = 0; i < poilst.Count; i++) {
poi[i] = poilst[i];
}
gp.DrawPolygon(new Pen(Color.Blue, 2), poi);
}

#endregion

}
}


还可以设置鼠标指针的形状,在窗体属性的Cursor中,由Default变为Cross,这样就像ArcGIS或者CorelDRAW的效果了。
设置Cursor.jpg

完整工程以及更新文件可以参见我的GitHub-rubberInGDIplus

我觉得它具有的功能是:能够很好地作为图形学以及矢量多边形小软件的框架

具体的应用项目可以看我的扫描线填充多边形的代码。

  • scanLineToFillPolygon:GitHub

完成效果-五角星.jpg