MSChart控件Demo中打印表格的Bug
最近因为项目中使用到了图形及图形打印功能,因此想到使用.NET平台下的Chart控件,在其给出的Demo中,有非常丰富的示例可以满足实际开发要求。由于项目中有一个要求是在根据数据源画出柱状图形的同时,显示柱状图所对应的表格(数据)。发现在其ChartFeatures/Customization and Events下的Custom Painting a DataTable示例与要求比较相似。其显示如下:
图(1) 柱状图及表格显示效果
图形下面是数据源,上面是根据数据源画出的柱状图形,比较美观。但是在打印时,却出现了问题,其打印效果如下:
图(2) 打印时的呈现效果
数据表格在打印的时候,显示不完整。为了找到原因,先分析上面图形是如何形成的。
对于.NET提供的控件,是由其OnPaint事件完成绘制工作的。而此Chart控件在OnPaint时,只绘制了柱状图形部分,其数据表格部分,是在其PostPaint事件时绘制的。Chart控件其自身的打印方法,提供了图形打印功能,即可以只调用Chart.Printing.Print()函数,完成整个图形的打印。当然,在打印的过程中,会产生图形的重绘,引发OnPiant事件和PostPaint事件。为了找到产生此种错误的原因,只有仔细读取代码,可是代码中也没有找到明显的错误,花了几乎一天的时候,最后才想到,问题可能就出在打印时的重绘。因为打印时使用的坐标是设备坐标,而显示的时候则使用的是页面坐标。在两个坐标转换过程中,由于PageUnit的不同而导致显示的不正常。
坐标系类型,在GDI时代分为逻辑坐标和物理坐标,而GDI+中分别为:
- 世界坐标(World Coordinate):要测量的点距离文档区域左上角的位置(以像素为单位);
- 页面坐标(Page Coordinate):要测量的点距离客户区域左上角的位置(以像素为单位);
- 设备坐标(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) 代码更改后的打印效果
当然也有其它方法,如可能通过判断当前使用的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:
---------------------------------------------