我们先看下一段代码,这行代码是说在轴上对应的值转化成它对应的像素值。
public float Transform( double x ) { // Must take into account Log, and Reverse Axes double denom = ( _maxLinTemp - _minLinTemp ); double ratio; if ( denom > 1e-100 ) ratio = ( Linearize( x ) - _minLinTemp ) / denom; else ratio = 0; if ( _isReverse == ( _ownerAxis is XAxis || _ownerAxis is X2Axis ) ) return (float) ( _maxPix - ( _maxPix - _minPix ) * ratio ); else return (float) ( _minPix + ( _maxPix - _minPix ) * ratio ); }
这段代码很好理解,我们假设这是X轴,轴没有反转,X轴上的坐标最小值minLinTemp,对应像素是minpix,坐标最大值是maxLinTemp,对应像素是maxpix,现有一点在x轴的坐标是xlin,那么它对应的像素是xpix,它们满足这样一个关系,(xlin-minLinTemp)/(maxLinTemp-minLinTemp)=(xpix-minpix)/(maxpix-minpix),根据这个关系我们能得到对应的xpix的关系。如果是Y轴,因为我们坐标系上的Y轴和在设备上的坐标系是反着来的,那么它们的关系要改着这样了,(xlin-minLinTemp)/(maxLinTemp-minLinTemp)=(maxpix-xpix)/(maxpix-minpix).
然后我们查看一下Chart图上的元素是如何绘制的,我们这里看线条的绘制过程。线条的元素LineItem里会封装一个Line的对象,这个对象主要是负责具体画什么样的线,如直线,基数样条等,我们看一下普通的基数样条的绘制代码。
public void DrawCurve( Graphics g, GraphPane pane, CurveItem curve, float scaleFactor ) { Line source = this; if ( curve.IsSelected ) source = Selection.Line; // switch to int to optimize drawing speed (per Dale-a-b) int tmpX, tmpY, lastX = int.MaxValue, lastY = int.MaxValue; double curX, curY, lowVal; PointPair curPt, lastPt = new PointPair(); bool lastBad = true; IPointList points = curve.Points; ValueHandler valueHandler = new ValueHandler( pane, false ); Axis yAxis = curve.GetYAxis( pane ); Axis xAxis = curve.GetXAxis( pane ); bool xIsLog = xAxis._scale.IsLog; bool yIsLog = yAxis._scale.IsLog; // switch to int to optimize drawing speed (per Dale-a-b) int minX = (int)pane.Chart.Rect.Left; int maxX = (int)pane.Chart.Rect.Right; int minY = (int)pane.Chart.Rect.Top; int maxY = (int)pane.Chart.Rect.Bottom; using ( Pen pen = source.GetPen( pane, scaleFactor ) ) { if ( points != null && !_color.IsEmpty && this.IsVisible ) { //bool lastOut = false; bool isOut; bool isOptDraw = _isOptimizedDraw && points.Count > 1000; // (Dale-a-b) we'll set an element to true when it has been drawn bool[,] isPixelDrawn = null; if ( isOptDraw ) isPixelDrawn = new bool[maxX + 1, maxY + 1]; // Loop over each point in the curve for ( int i = 0; i < points.Count; i++ ) { curPt = points[i]; if ( pane.LineType == LineType.Stack ) { if ( !valueHandler.GetValues( curve, i, out curX, out lowVal, out curY ) ) { curX = PointPair.Missing; curY = PointPair.Missing; } } else { curX = curPt.X; curY = curPt.Y; } // Any value set to double max is invalid and should be skipped // This is used for calculated values that are out of range, divide // by zero, etc. // Also, any value <= zero on a log scale is invalid if ( curX == PointPair.Missing || curY == PointPair.Missing || System.Double.IsNaN( curX ) || System.Double.IsNaN( curY ) || System.Double.IsInfinity( curX ) || System.Double.IsInfinity( curY ) || ( xIsLog && curX <= 0.0 ) || ( yIsLog && curY <= 0.0 ) ) { // If the point is invalid, then make a linebreak only if IsIgnoreMissing is false // LastX and LastY are always the last valid point, so this works out lastBad = lastBad || !pane.IsIgnoreMissing; isOut = true; } else { // Transform the current point from user scale units to // screen coordinates tmpX = (int) xAxis.Scale.Transform( curve.IsOverrideOrdinal, i, curX ); tmpY = (int) yAxis.Scale.Transform( curve.IsOverrideOrdinal, i, curY ); // Maintain an array of "used" pixel locations to avoid duplicate drawing operations // contributed by Dale-a-b if ( isOptDraw && tmpX >= minX && tmpX <= maxX && tmpY >= minY && tmpY <= maxY ) // guard against the zoom-in case { if ( isPixelDrawn[tmpX, tmpY] ) continue; isPixelDrawn[tmpX, tmpY] = true; } isOut = ( tmpX < minX && lastX < minX ) || ( tmpX > maxX && lastX > maxX ) || ( tmpY < minY && lastY < minY ) || ( tmpY > maxY && lastY > maxY ); if ( !lastBad ) { try { // GDI+ plots the data wrong and/or throws an exception for // outrageous coordinates, so we do a sanity check here if ( lastX > 5000000 || lastX < -5000000 || lastY > 5000000 || lastY < -5000000 || tmpX > 5000000 || tmpX < -5000000 || tmpY > 5000000 || tmpY < -5000000 ) InterpolatePoint( g, pane, curve, lastPt, scaleFactor, pen, lastX, lastY, tmpX, tmpY ); else if ( !isOut ) { if ( !curve.IsSelected && this._gradientFill.IsGradientValueType ) { using ( Pen tPen = GetPen( pane, scaleFactor, lastPt ) ) { if ( this.StepType == StepType.NonStep ) { g.DrawLine( tPen, lastX, lastY, tmpX, tmpY ); } else if ( this.StepType == StepType.ForwardStep ) { g.DrawLine( tPen, lastX, lastY, tmpX, lastY ); g.DrawLine( tPen, tmpX, lastY, tmpX, tmpY ); } else if ( this.StepType == StepType.RearwardStep ) { g.DrawLine( tPen, lastX, lastY, lastX, tmpY ); g.DrawLine( tPen, lastX, tmpY, tmpX, tmpY ); } else if ( this.StepType == StepType.ForwardSegment ) { g.DrawLine( tPen, lastX, lastY, tmpX, lastY ); } else { g.DrawLine( tPen, lastX, tmpY, tmpX, tmpY ); } } } else { if ( this.StepType == StepType.NonStep ) { g.DrawLine( pen, lastX, lastY, tmpX, tmpY ); } else if ( this.StepType == StepType.ForwardStep ) { g.DrawLine( pen, lastX, lastY, tmpX, lastY ); g.DrawLine( pen, tmpX, lastY, tmpX, tmpY ); } else if ( this.StepType == StepType.RearwardStep ) { g.DrawLine( pen, lastX, lastY, lastX, tmpY ); g.DrawLine( pen, lastX, tmpY, tmpX, tmpY ); } else if ( this.StepType == StepType.ForwardSegment ) { g.DrawLine( pen, lastX, lastY, tmpX, lastY ); } else if ( this.StepType == StepType.RearwardSegment ) { g.DrawLine( pen, lastX, tmpY, tmpX, tmpY ); } } } } catch { InterpolatePoint( g, pane, curve, lastPt, scaleFactor, pen, lastX, lastY, tmpX, tmpY ); } } lastPt = curPt; lastX = tmpX; lastY = tmpY; lastBad = false; //lastOut = isOut; } } } } }
代码有些多,但是过程还是很清晰的,前面一部分代码都是来获取相关数据,然后遍历对应CurveItem里的IPointList数据,把每个Point数据(是我们看到的数据)对应当前CurveItem里的X轴与Y轴上的像素值(给机器来看的),这里用到的转换就是上面所说的那部分代码,根据我们选择StepType不同来绘制每二点决定的线。这样就绘制出了我们要的线条,别的元素具体上不同,但是都是这种逻辑。其实如果要多了解这一方面的逻辑,可以看下有关坐标系统和转换的知识。
PS:读源码有个地方要注意,不要只是跟着代码往下来,看一会不是分支过多就是跑题了,看的时候你要自己跟着想,如果是我来实现这一功能,应该怎么来实现,然后和看的代码比对。