一時興起,想寫個模擬地鐵駕駛的游戲,但是很多東西都不會,資源哪里有?例如列車前進時周圍景物的動態效果怎么做出來,速度控制桿上各個幅度代表了多大的加速度。我很佩服也很羡慕《申城脈動》的作者,能寫出那樣的一個游戲和獲得那么多的資源。地鐵族上面資料不少的,但不會找出來,廢話不多說。
里面要用到的速度儀表盤,上網看見別人畫了不少很炫的,但提供下載沒有。對GDI+不太了解的我只能自己寫。幸虧看到一篇博文,是別人學生時代的實驗報告的(呵呵!這就是差別)。里面的代碼我沒看,我只看了一幅圖片就夠了。之前只困惑於儀表盤上面的刻度是怎么畫出來的,那篇文章里面有一幅圖就是一幅三角函數的圖。之前一直沒想到這里要用回初中時學的三角函數。
下面將從控件的屬性,還有繪制的過程做一個介紹。從而介紹完這個儀表盤的制作。
數據類型 |
屬性名稱 |
描述 |
int |
LongGraduateLength |
長刻度長度 |
int |
ShortGraduateLength |
短刻度長度 |
float |
Short2Long |
短刻度轉長刻度進率 |
float |
ShortgraduateSpan |
短刻度間隔,實際數值的間隔 |
float |
Range |
量程 |
int |
Angle |
整個刻度的角度,角度制的度數 |
int |
Width |
控件寬度,與高度相等 |
int |
Height |
控件高度,與寬度相等 |
float |
Value |
當前值 |
繪制控件都是在OnPaint事件里執行GDI+的代碼。
protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); DrawGraduation(e.Graphics); DrawArc(e.Graphics); }
第一個方法是畫表盤的刻度,以及上面的示數;第二個方法是畫表盤的指針,包括當前的值。
先介紹第一個方法
1 private void DrawGraduation(Graphics g) 2 { 3 int startDec = 270 - (360 - Angle) / 2; 4 int endDec = 270 + (360 - Angle) / 2 - 360;//計算出起始角度和終止角度 5 float dealta = Angle * ShortgraduateSpan / Range; //計算出每個刻度間的角度間隔 6 StringFormat sf=new StringFormat(){ Alignment= StringAlignment.Center,LineAlignment= StringAlignment.Center}; 7 8 for (float i = startDec; i >= endDec; i -= dealta)//通過循環畫出表盤上的刻度 9 { 10 if ((i - startDec)*Range/Angle % Short2Long == 0) //判斷此位置畫的是大刻度還是小刻度 11 { 12 //畫大刻度 13 g.DrawLine(Pens.White, 14 Radius + (float)Math.Cos(Degree2Radian(i)) * Radius, 15 Radius - (float)Math.Sin(Degree2Radian(i)) * Radius, 16 Radius + (float)Math.Cos(Degree2Radian(i)) * (Radius - LongGraduateLength), 17 Radius - (float)Math.Sin(Degree2Radian(i)) * (Radius - LongGraduateLength) 18 ); 19 //寫刻度上的文字 20 g.DrawString(((startDec - i) * Range / Angle).ToString(), this.Font, Brushes.White, 21 Radius + (float)Math.Cos(Degree2Radian(i)) * (Radius - LongGraduateLength-15), 22 Radius - (float)Math.Sin(Degree2Radian(i)) * (Radius - LongGraduateLength-15), 23 sf 24 );//sf是文字的格式,這里設置成居中 25 } 26 else 27 //畫小刻度 28 g.DrawLine(Pens.White, 29 Radius + (float)Math.Cos(Degree2Radian(i)) * Radius, 30 Radius - (float)Math.Sin(Degree2Radian(i)) * Radius, 31 Radius + (float)Math.Cos(Degree2Radian(i)) * (Radius - ShortGraduateLength), 32 Radius - (float)Math.Sin(Degree2Radian(i)) * (Radius - ShortGraduateLength) 33 ); 34 } 35 }
畫刻度線還考了一回初中的三角函數。
只要有刻度線所在的角的角度,刻度線的起點和終點的坐標都可以確定下來。起點就是半徑上圓心的另一端。通過sin*R可以得到起點的相對圓心的縱坐標;cos*R可以得到起點相對圓心的橫坐標。終點的同樣道理,通過sin*(R-L)可以得到終點的相對圓心的縱坐標;cos*(R-L)可以得到終點相對圓心的橫坐標。
.NET Framework提供的三角函數有點坑,方法的描述上是說角度值,但是實際上要傳是弧度值。所以我這里還要做一個角度制和弧度制的轉換。
private double Degree2Radian(double degree) { return degree * Math.PI / 180; }
下面到另一個方法
1 private void DrawArc(Graphics g) 2 { 3 //示數和外部的圓圈 4 for(float i=0;i<3;i+=0.1f) 5 g.DrawEllipse(Pens.White, Radius - 25+i, Radius - 25+i, 50-2*i, 50-2*i); 6 g.DrawString(this.Value.ToString(), new Font(this.Font.FontFamily, 20,FontStyle.Bold), Brushes.White, Radius, Radius, new StringFormat() { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }); 7 8 //旋轉畫布,形成一定的傾角 9 Matrix matrix = new Matrix(); 10 matrix.RotateAt(Value * Angle / Range - (270 - (360 - Angle) / 2), new PointF(Radius, Radius), MatrixOrder.Append); 11 g.Transform = matrix; 12 13 //繪制指針上面的主體矩形 14 Rectangle rec = new Rectangle(Radius + 25, Radius - 3,Radius-55-ShortGraduateLength, 6); 15 g.DrawRectangle(Pens.White, rec); 16 g.FillRectangle(Brushes.White, rec); 17 18 //繪制指針上的三角形 19 g.DrawPolygon(Pens.White,CreateTriangle(Radius*2 -30-ShortGraduateLength,Radius ,6,10,false)); 20 g.FillPolygon(Brushes.White, CreateTriangle(Radius*2 -30-ShortGraduateLength, Radius , 6, 10, false)); 21 22 //繪制指針末端的小矩形 23 rec = new Rectangle(Radius * 2 - 25 - ShortGraduateLength,Radius-1,20,2); 24 g.DrawRectangle(Pens.White, rec); 25 g.FillRectangle(Brushes.White, rec); 26 }
這里指針實際上有四個圖形組成,指針起始的一個圓形,主體的那個粗的矩形,達到漸細效果的三角形和末端的一個小矩形
.NET Framework又沒有提供畫三角形的方法,所以畫三角形它只能通過畫多邊形的方法來繪制。
我這里定義了一個只符合這里使用的方法,用於繪制一個等腰三角形,傳入底邊的中點坐標,底邊和高的長度,是水平還是垂直,來構造一個三角形,最后返回三個頂點的坐標
1 private Point[] CreateTriangle(int x, int y,int bottom, int height, bool isHorizontal) 2 { 3 Point p1, p2, ph; 4 if (isHorizontal) 5 { 6 p1 = new Point(x - bottom/2, y); 7 p2 = new Point(x + bottom/2, y); 8 ph = new Point(x, y + height); 9 } 10 else 11 { 12 p1 = new Point(x, y - bottom/2); 13 p2 = new Point(x, y + bottom/2); 14 ph = new Point(x + height, y); 15 } 16 return new Point[] {p1,p2,ph }; 17 }
附一幅完成的效果圖,最后把整個控件的源碼粘上來。
覺得這個控件一點兒也不通用,只能滿足極個別的需求。大家盡管提一下意見,讓我改進改進。

1 public class Dashboard:Control 2 { 3 public Dashboard() 4 { 5 this.Width = 300; 6 this.Height = 300; 7 this.BackColor = Color.Black; 8 DoubleBuffered = true; 9 this.Font = new Font(this.Font.FontFamily, 15f); 10 } 11 12 private int longGraduateLength=20,shortGraduateLength=10,angle=300; 13 private float short2Long=2,shortgraduateSpan=5,range=100; 14 private Color lineColor=Color.White; 15 16 public int LongGraduateLength 17 { 18 get { return longGraduateLength; } 19 set 20 { 21 longGraduateLength = value < 1 ? 1 : value; 22 } 23 } 24 25 public int ShortGraduateLength 26 { 27 get { return shortGraduateLength; } 28 set 29 { 30 shortGraduateLength = value < 1 ? 1 : value; 31 } 32 } 33 34 public float Short2Long 35 { 36 get { return short2Long; } 37 set 38 { 39 short2Long = value < 1 ? 1 : value; 40 } 41 } 42 43 public float ShortgraduateSpan 44 { 45 get { return shortgraduateSpan; } 46 set 47 { 48 shortgraduateSpan = value < 1 ? 1 : value; 49 } 50 } 51 52 public float Range 53 { 54 get { return range; } 55 set 56 { 57 range = value < 1 ? 1 : value; 58 } 59 } 60 61 public int Angle 62 { 63 get { return angle; } 64 set 65 { 66 angle = value < 1 ? 1 : value; 67 } 68 } 69 70 public Color LineColor 71 { 72 get { return lineColor; } 73 set { lineColor = value; } 74 } 75 76 public new int Width 77 { 78 get { return base.Width; } 79 set 80 { 81 if (value != base.Height) base.Height = value; 82 base.Width = value; 83 } 84 } 85 86 public new int Height 87 { 88 get { return base.Height; } 89 set 90 { 91 if (value != base.Width) base.Width = value; 92 base.Height = value; 93 } 94 } 95 96 private float currValue; 97 public float Value 98 { 99 get { return currValue; } 100 set 101 { 102 float preValue = currValue; 103 if (value < 0) currValue = 0; 104 else if (value > Range) currValue = Range; 105 else currValue = value; 106 if (preValue != currValue) 107 { 108 if(ValueChanged!=null) ValueChanged(this, new EventArgs()); 109 this.Refresh(); 110 } 111 112 } 113 } 114 115 protected override void OnPaint(PaintEventArgs e) 116 { 117 base.OnPaint(e); 118 //刻度 示數 指針 示數 119 DrawGraduation(e.Graphics); 120 DrawArc(e.Graphics); 121 } 122 123 private int Radius 124 { 125 get { return Width / 2; } 126 } 127 128 private Point core; 129 private Point Core 130 { 131 get 132 { 133 if (core.X != this.Radius || core.Y != this.Radius) 134 core = new Point(this.Radius, this.Radius); 135 return core; 136 } 137 } 138 139 private void DrawGraduation(Graphics g) 140 { 141 //換算角度范圍 142 //半徑*sin=外點 143 //(半徑-刻度長)*sin=內點 144 //內點標數 145 int startDec = 270 - (360 - Angle) / 2; 146 int endDec = 270 + (360 - Angle) / 2-360; 147 float dealta=Angle*ShortgraduateSpan/Range; 148 StringFormat sf=new StringFormat(){ Alignment= StringAlignment.Center,LineAlignment= StringAlignment.Center}; 149 150 for (float i = startDec; i >= endDec; i -= dealta) 151 { 152 if ((i - startDec)*Range/Angle % Short2Long == 0) 153 { 154 g.DrawLine(Pens.White, 155 Radius + (float)Math.Cos(Degree2Radian(i)) * Radius, 156 Radius - (float)Math.Sin(Degree2Radian(i)) * Radius, 157 Radius + (float)Math.Cos(Degree2Radian(i)) * (Radius - LongGraduateLength), 158 Radius - (float)Math.Sin(Degree2Radian(i)) * (Radius - LongGraduateLength) 159 ); 160 g.DrawString(((startDec - i) * Range / Angle).ToString(), this.Font, Brushes.White, 161 Radius + (float)Math.Cos(Degree2Radian(i)) * (Radius - LongGraduateLength-15), 162 Radius - (float)Math.Sin(Degree2Radian(i)) * (Radius - LongGraduateLength-15), 163 sf 164 ); 165 } 166 else 167 g.DrawLine(Pens.White, 168 Radius + (float)Math.Cos(Degree2Radian(i)) * Radius, 169 Radius - (float)Math.Sin(Degree2Radian(i)) * Radius, 170 Radius + (float)Math.Cos(Degree2Radian(i)) * (Radius - ShortGraduateLength), 171 Radius - (float)Math.Sin(Degree2Radian(i)) * (Radius - ShortGraduateLength) 172 ); 173 } 174 } 175 176 private void DrawArc(Graphics g) 177 { 178 for(float i=0;i<3;i+=0.1f) 179 g.DrawEllipse(Pens.White, Radius - 25+i, Radius - 25+i, 50-2*i, 50-2*i); 180 g.DrawString(this.Value.ToString(), new Font(this.Font.FontFamily, 20,FontStyle.Bold), Brushes.White, Radius, Radius, new StringFormat() { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }); 181 182 183 Matrix matrix = new Matrix(); 184 matrix.RotateAt(Value * Angle / Range - (270 - (360 - Angle) / 2), new PointF(Radius, Radius), MatrixOrder.Append); 185 g.Transform = matrix; 186 187 Rectangle rec = new Rectangle(Radius + 25, Radius - 3,Radius-55-ShortGraduateLength, 6); 188 g.DrawRectangle(Pens.White, rec); 189 g.FillRectangle(Brushes.White, rec); 190 191 g.DrawPolygon(Pens.White,CreateTriangle(Radius*2 -30-ShortGraduateLength,Radius ,6,10,false)); 192 g.FillPolygon(Brushes.White, CreateTriangle(Radius*2 -30-ShortGraduateLength, Radius , 6, 10, false)); 193 194 rec = new Rectangle(Radius * 2 - 25 - ShortGraduateLength,Radius-1,20,2); 195 g.DrawRectangle(Pens.White, rec); 196 g.FillRectangle(Brushes.White, rec); 197 } 198 199 private Point[] CreateTriangle(int x, int y,int bottom, int height, bool isHorizontal) 200 { 201 Point p1, p2, ph; 202 if (isHorizontal) 203 { 204 p1 = new Point(x - bottom/2, y); 205 p2 = new Point(x + bottom/2, y); 206 ph = new Point(x, y + height); 207 } 208 else 209 { 210 p1 = new Point(x, y - bottom/2); 211 p2 = new Point(x, y + bottom/2); 212 ph = new Point(x + height, y); 213 } 214 return new Point[] {p1,p2,ph }; 215 } 216 217 private double Degree2Radian(double degree) 218 { 219 return degree * Math.PI / 180; 220 } 221 222 protected override void OnResize(EventArgs e) 223 { 224 base.OnResize(e); 225 this.Refresh(); 226 } 227 228 public delegate void OnValueChanged(object sender, EventArgs e); 229 230 public event OnValueChanged ValueChanged; 231 }