MSChart控件Demo中打印表格的Bug

最近因为项目中使用到了图形及图形打印功能,因此想到使用.NET平台下的Chart控件,在其给出的Demo中,有非常丰富的示例可以满足实际开发要求。由于项目中有一个要求是在根据数据源画出柱状图形的同时,显示柱状图所对应的表格(数据)。发现在其ChartFeatures/Customization and Events下的Custom Painting a DataTable示例与要求比较相似。其显示如下:

1

图(1) 柱状图及表格显示效果

图形下面是数据源,上面是根据数据源画出的柱状图形,比较美观。但是在打印时,却出现了问题,其打印效果如下:

2

图(2) 打印时的呈现效果

 

数据表格在打印的时候,显示不完整。为了找到原因,先分析上面图形是如何形成的。

对于.NET提供的控件,是由其OnPaint事件完成绘制工作的。而此Chart控件在OnPaint时,只绘制了柱状图形部分,其数据表格部分,是在其PostPaint事件时绘制的。Chart控件其自身的打印方法,提供了图形打印功能,即可以只调用Chart.Printing.Print()函数,完成整个图形的打印。当然,在打印的过程中,会产生图形的重绘,引发OnPiant事件和PostPaint事件。为了找到产生此种错误的原因,只有仔细读取代码,可是代码中也没有找到明显的错误,花了几乎一天的时候,最后才想到,问题可能就出在打印时的重绘。因为打印时使用的坐标是设备坐标,而显示的时候则使用的是页面坐标。在两个坐标转换过程中,由于PageUnit的不同而导致显示的不正常。

     坐标系类型,在GDI时代分为逻辑坐标和物理坐标,而GDI+中分别为:

  1. 世界坐标(World Coordinate):要测量的点距离文档区域左上角的位置(以像素为单位);
  2. 页面坐标(Page Coordinate):要测量的点距离客户区域左上角的位置(以像素为单位);
  3. 设备坐标(Device Coordinate):它类似于页面坐标,但是其测量单位不一定是像素,而是用户通过调用PageUnit属性,指定它的单位。它除了默认的像素外,还包括英寸和毫米。如果我们为某个图形设定了其PageUnit为英寸或毫米,那么不管它在哪种设备上显示的大小都是一样的,不会因为分辨率的不同而大小不一致。

在跟踪代码调试过程中发现,图形显示时,使用的是PageUnit.Dispaly枚举值“指定显示设备的度量单位。 通常,视频显示使用的单位是像素;打印机使用的单位是 1/100 英寸。”,而在打印的时候,会引发两次Paint事件,一次是在将显示内容重绘到打印机上,一次是因为输出到打印机的窗口显示时,引发了当前图形显示窗口的重绘。在输出到打印机时,Graphics.PageUnit值为Pixel。与OnPaint事件中使用的PageUnit.Display不同,似乎是找到了问题的根本所在。

但是,从图(2)中可以看出,打印时,每个单元格宽度的计算是正确的,而高度的计算则不完全准确。如果是因为PageUint的原因,则应该整个表格显示都不正确。看来,问题还得继续找。

既然打印和显示都与像素有关系,那么就说说像素。

/*
对于计算机的屏幕设备而言,像素(Pixel)或者说px是一个最基本的单位,就是一个点。
其它所有的单位,都和像素成一个固定的比例换算关系。
所有的长度单位基于屏幕进行显示的时候,都统一先换算成为像素的多少,然后进行显示。
所以,就计算机的屏幕而言,相对长度和绝对长度没有本质差别。
任何单位其实都是像素,差别只是比例不同。
*/

对于像素的直接反映:“点”,就是要显示的内容,但是对于一个点占居多大的尺寸,不同的设备有不同的标准。显示器和打印机,其分辨率是不一样的,即DPI(每英寸显示的点数)可能不一样,如果使用PageUnit.Pixel时,在转换过程中则可能出现问题。

继续调试代码。

分别查看显示和打印时,Graphics.Dpix的值,分别为96和300。问题终于找到了。

原来在打印的时候,Graphics会根据当前的输出设备,自动计算显示宽度与高度。但是,在PostPaint事件中,当画表格矩形框的时候,使用的宽度为axisFont.Height,而不是axisFontSize.Height。前面指定的字体大小是一定的,而后面的Size大小,则是经过Graphics根据当前的PageUnit和Dpix自动换算得到的。通过调试,计算得到两者的比正好是96:300。因此,只需要将PaintDataTable函数中使用axisFont.Height的地方,用axisFontSize.Height代替就可以了。

3

图(3) 代码更改后的打印效果

当然也有其它方法,如可能通过判断当前使用的PageUnit,而确定需要不需要对高度进行调整与改变。

此外,对此Demo中的两个建议:

  • 指定了接受的Chart控件的ChartArea.Name必须为”Default”,这样,就不方便使用。可以将“Default”更改为area.Name。
  • 在表格打印的开头,首先打印出来显示的颜色,此处的宽度指定为10,则在显示与打印的时候,其中间还是相差96:300的比例。因此,可在函数开始判断一下,如果当前的Graphics的PageUnit为Pixel,则将宽度指定为10*300/96;

至此,困扰了我一两天的问题解决了。由于之前没有接触过打印及图形显示方面的技术,解决起来很费劲,而且Chart控件是在.NET3.5SP1之后才有的,用的人不是太多,有些问题,在网上找也找不到。现在贴出来,但愿会对碰到此问题的朋友有所帮助,也希望大家拍砖指教。

待决问题:仔细看图1,图2,图3,会发现图3下面没有“This is the x Axis”。这个是我在调试过程中,发现高度调整的时候,会部分掩盖AxisX.Title,一时没有找到好的解决办法。不知各位有什么好方法,还请指点。

 

PS:下面把相关的代码贴出,注意此函数在\WinSamples2010\WinSamples\Utilities\ChartDataTableHelper\ChartDataTableHelper.cs中。

 

  1:  /// <summary>
  2:          /// This method does all the work for the painting of the data table.
  3:          /// </summary>
  4:          private void PaintDataTable(System.Windows.Forms.DataVisualization.Charting.ChartPaintEventArgs e)
  5:          {
  6:              ChartArea area = (ChartArea) e.ChartElement; 
  7:   
  8:              // get the rect of the chart area
  9:              RectangleF rect = e.ChartGraphics.GetAbsoluteRectangle( area.Position.ToRectangleF() );
 10:   
 11:              // get the inner plot position
 12:              ElementPosition elemPos = area.InnerPlotPosition;
 13:   
 14:              // find the coordinates of the inner plot position
 15:              float x = rect.X + (rect.Width / 100 * elemPos.X);
 16:              float y = rect.Y + (rect.Height / 100 * elemPos.Y);
 17:              float ChartAreaBottomY = rect.Y + rect.Height;
 18:   
 19:              // offset the bottom by the width+1 of the scrollbar if it is visible
 20:              if(area.AxisX.ScrollBar.IsVisible && !area.AxisX.ScrollBar.IsPositionedInside)
 21:                  ChartAreaBottomY -= ((float)area.AxisX.ScrollBar.Size+1);
 22:   
 23:              float width = (rect.Width / 100 * elemPos.Width);
 24:              float height = (rect.Height / 100 * elemPos.Height);
 25:   
 26:              // find the height of the font that will be used
 27:              Font axisFont = area.AxisX.LabelStyle.Font;
 28:              //axisFont = new Font(axisFont.FontFamily, axisFont.Size, axisFont.Style,GraphicsUnit.Point);
 29:              float tempHeight = axisFont.Height;
 30:              float tempWidth = 20;
 31:              if (e.ChartGraphics.Graphics.PageUnit == GraphicsUnit.Pixel)
 32:              { 
 33:                  tempHeight = (float)(axisFont.Height * 3.15);
 34:                  tempWidth =(float) (20 * 3.125);
 35:              }
 36:              string testString = "ForFontHeight";
 37:              SizeF axisFontSize = e.ChartGraphics.Graphics.MeasureString(testString, axisFont);
 38:   
 39:              // find the height of the font that will be used
 40:              Font titleFont = area.AxisX.TitleFont;
 41:              testString = area.AxisX.Title;
 42:              SizeF titleFontSize = e.ChartGraphics.Graphics.MeasureString(testString, titleFont);
 43:   
 44:              int seriesCount = 0;
 45:   
 46:              // for each series that is attached to the chart area,
 47:              // draw some boxes around the labels in the color provided
 48:              for(int i = e.Chart.Series.Count-1; i >= 0; i--)
 49:              {
 50:                  if(area.Name == e.Chart.Series[i].ChartArea)
 51:                  {
 52:                      seriesCount++;
 53:                  }
 54:              }
 55:   
 56:              // now, if a box was actually drawn, then draw 
 57:              // the verticle lines to separate the columns of the table.
 58:              if(seriesCount > 0)
 59:              {
 60:                  for(int i = 0; i < e.Chart.Series.Count; i++)
 61:                  {
 62:                      if(area.Name == e.Chart.Series[i].ChartArea)
 63:                      {
 64:                          double min = area.AxisX.Minimum;
 65:                          double max = area.AxisX.Maximum;
 66:   
 67:                          // modify the min value for the current axis view
 68:                          if(area.AxisX.ScaleView.Position-1 > min)
 69:                              min = area.AxisX.ScaleView.Position-1;
 70:   
 71:                          // modify the max value for the currect axis view
 72:                          if( (area.AxisX.ScaleView.Position + area.AxisX.ScaleView.Size + 0.5) < max)
 73:                              max = area.AxisX.ScaleView.Position + area.AxisX.ScaleView.Size + 0.5;
 74:   
 75:   
 76:                          // find the starting point that will be display.
 77:                          // this is dependent on the current axis view.
 78:                          // this sample assumes the same number of points in each
 79:                          // series so always take from the zeroth series
 80:                          int pointIndex = 0;
 81:                          foreach(DataPoint pt in ChartObj.Series[0].Points)
 82:                          {
 83:                              if(pt.XValue > min)
 84:                                  break;
 85:   
 86:                              pointIndex++;
 87:                          }
 88:   
 89:                          bool TableLegendDrawn = false;
 90:   
 91:                          for(double AxisValue = min; AxisValue < max; AxisValue++)
 92:                          {
 93:                              float pixelX = (float)e.ChartGraphics.GetPositionFromAxis(area.Name, AxisName.X, AxisValue);
 94:                              float nextPixelX = (float)e.ChartGraphics.GetPositionFromAxis(area.Name, AxisName.X, AxisValue + 1);
 95:                              float pixelY = ChartAreaBottomY - titleFontSize.Height - (seriesCount * axisFontSize.Height);
 96:   
 97:                              PointF point1 = PointF.Empty;
 98:                              PointF point2 = PointF.Empty;
 99:   
100:                              // Set Maximum and minimum points
101:                              point1.X = pixelX;
102:                              point1.Y = 0;
103:   
104:                              // Convert relative coordinates to absolute coordinates.
105:                              point1 = e.ChartGraphics.GetAbsolutePoint(point1);
106:                              point2.X = point1.X;
107:                              point2.Y = ChartAreaBottomY - titleFontSize.Height;
108:                              point1.Y = pixelY;
109:   
110:                              // Draw connection line
111:                              e.ChartGraphics.Graphics.DrawLine(new Pen(borderColor), point1,point2);
112:   
113:   
114:                              point2.X = nextPixelX;
115:                              point2.Y = 0;
116:                              point2 = e.ChartGraphics.GetAbsolutePoint(point2);
117:   
118:                              StringFormat format = new StringFormat();
119:                              format.Alignment = StringAlignment.Center;
120:                              format.LineAlignment = StringAlignment.Center;
121:   
122:                              // for each series draw one value in the column
123:                              int row = 0;
124:                              foreach(Series ser in ChartObj.Series)
125:                              {
126:                                  if(area.Name == ser.ChartArea)
127:                                  {
128:                                      if(!TableLegendDrawn)
129:                                      {
130:                                          // draw the series color box 
131:                                          e.ChartGraphics.Graphics.FillRectangle(new SolidBrush(ser.Color),
132:                                              x - tempWidth, row * (tempHeight) + (point1.Y), tempWidth, axisFontSize.Height);
133:   
134:                                          e.ChartGraphics.Graphics.DrawRectangle(new Pen(borderColor),
135:                                              x - tempWidth, row * (tempHeight) + (point1.Y), tempWidth, axisFontSize.Height);
136:   
137:                                          e.ChartGraphics.Graphics.FillRectangle(new SolidBrush(tableColor),
138:                                              x,
139:                                              row * (tempHeight) + (point1.Y),
140:                                              width, 
141:                                              axisFontSize.Height); 
142:   
143:                                          e.ChartGraphics.Graphics.DrawRectangle(new Pen(borderColor), 
144:                                              x,
145:                                              row * (tempHeight) + (point1.Y),
146:                                              width, 
147:                                              axisFontSize.Height); 
148:   
149:                                      }
150:   
151:                                      if(pointIndex < ser.Points.Count)
152:                                      {
153:                                          string label = ser.Points[pointIndex].YValues[0].ToString();
154:                                          RectangleF textRect = new RectangleF(point1.X, row * (axisFontSize.Height) + (point1.Y + 1), point2.X - point1.X, axisFontSize.Height);
155:                                          e.ChartGraphics.Graphics.DrawString(label, axisFont, new SolidBrush(area.AxisX.LabelStyle.ForeColor), textRect, format);
156:                                      }
157:   
158:                                      row++;
159:   
160:                                  }
161:                              }
162:   
163:                              TableLegendDrawn = true;
164:   
165:                              pointIndex++;
166:                          }
167:   
168:                          // do this only once so break!
169:                          break;
170:                      }
171:                  }
172:              }
173:          }
174:   
175:   
176:   
posted @ 2010-07-30 12:47  Ritchie(乞戈)  阅读(4563)  评论(8编辑  收藏  举报