C#实现无物理边距 可打印区域的绘图\打印 z
经常在开发实际的应用程序中,需要用到图形绘制和打印程序。如何实现完整的精确打印和绘图是需要注意许多细节地方的。最近在遇到打印问题的时候,仔细研究一阵,总结这篇博文,写得有点杂乱,看文要还请费点神。
基本功能:窗体绘图与鼠标交互
打印预览与打印输出
开发平台:VisualStudio 2010 (C#)
由于打印机的分辨率参数经常是600Dpi或者更高的1200Dpi等数值,远比屏幕的96Dpi或者120Dpi数值大得多,为了保证能有效的实现所见即所得效果,即屏幕窗口绘图跟实际打印结果一致,需要处理不同分辨率问题。此处有两种解决方案:一种是计算打印机和屏幕的分辨率比例,然后屏幕绘图的结果,在向打印机上绘图时候进行比例缩放计算,这种方法是又世界坐标直接映射到像素在做计算;另一种则是基于页面坐标绘图,所有绘图坐标单位设置映射到页面坐标(mm单位),随后进行的绘图就直接计算LPtoDP转换即可。两种方法都是主动计算转换的像素,也可换成简便的开发平台自带的绘图系统,即设置Graphics的PageUnit属性为毫米即可。
设置Graphics.PageUnit为默认的Pixel单位,此后所有绘图单位都是基于像素单位,此处设备坐标大小,即多少像素是程序计算。为了方便计算页面坐标同设备坐标之间的转换,可以建立函数处理,类似MFC中的逻辑坐标与设备坐标之间的转换函数LPtoDP,如上图所示的②坐标转换过程。
设定窗口到视口的绘图单位为像素单位,两种坐标系设定如下:
(1)屏幕绘图:
Graphics g = panel1.CreateGraphics();
g.PageUnit= GraphicsUnit.Pixel;
(2)打印机绘图:
PrintPageEventArgse
Graphics g = e.Graphics;
g.PageUnit= GraphicsUnit.Pixel;
4、坐标转换差异测试2013/11/17
坐标轴绘图测试代码(注:边框线是用m_WPtoDP_X绘制的,而坐标轴是用两种方法进行绘制,由此看出两种绘图方式存在的差异)
for (float fX = 0; fX <= WorldSize.Width;fX += v_f_MarkLong)
(1)测试由世界坐标到页面坐标转换,再由页面坐标到设备坐标转换,并加上缩放系数最后得到窗口范围内的坐标值绘图如下所示。
v_f_TempX = m_WPtoLP_X(fX);
v_f_TempX = m_LPtoDP_X(v_f_TempX);
v_f_TempX *= v_f_ScaleZoomX;
左边为小窗体时的首尾绘图,存在明显误差;右边为窗体放大后的首尾绘图,误差明显减小,但仍然存在一定误差。
2)测试直接由世界坐标转换到设备坐标窗口绘图,结果如下:
v_f_TempX = m_WPtoDP_X(fX);
实际效果证明窗体缩放对误差大小基本没有影响,基本没有误差。
结论:直接由世界坐标到设备坐标映射减少了坐标转换的计算次数,有效避免了转换将计算过程中的误差累积,效果较好。其余的坐标转换则可以在需要的时候,直接使用,避免多次叠加使用。但是在实际使用过程中,还是使用了第一种方法(所有绘图都用相同方法,保证了绘图的一致性,不存在差异),这样避免了总是设定设备坐标(DP)的区域参数,在窗体缩放大小等变化的时候,减少计算量。
5、绘图显示缩放问题
进一步采用(1)的绘制方法注意的问题,即采用页面坐标到设备坐标映射比例绘图,此时基于页面坐标的区域进行绘图,会造成与设备坐标之间的绘图区域部匹配问题,例如A3纸张大小的区间需要的设备区域是2000*3000, 而实际的绘图窗口大小事800*600,因此会出现绘图显示不完全的情况,所以再次引进缩放系数v_f_ScaleZoomX和v_f_ScaleZoomY。此时存在四种屏幕显示方式,如下表所示。
其中需要注意设置的是逻辑坐标,根据打印机选择的纸张,设定页面坐标系,同时考虑了打印机物理边距和打印纸页面边距,真正用于坐标转换的,只有PlotArea区域的尺寸大小。
Analysis:分析打印系统的坐标系统成为必要,上述问题中明显出现的坐标平移情况,同时导致右边和下边出现了打印不完全的情况。最后得出C#的提供的平台中,打印的坐标原点是需要设定的,由于存在打印机的物理边距问题,实际物理打印坐标原点分两种情况:
(1)原点在物理边距线上,Origin = (HardMaeginX, HardMarginY )(1/100inch)
(2)原点在页面边距线上,Origin = (MaeginLeft, MarginTop )(1/100inch)
打印文档类关于打印坐标原点位置属性的MSDN解释:
public bool OriginAtMargins { get; set; }
---trueif the graphics origin starts at the page margins;对应于(2)
---falseif the graphics origin is at the top-left corner of the printable page. 对应于(1)
具体坐标如下图所示:打印纸的总体尺寸为黑色坐标系(827*1169)0.01
inch,而真正可打印的区域为v_PrintDocument.DefaultPageSettings.PrintableArea提供的绿色坐标系区域,此时应该把PrintableArea作为真正的打印计算坐标系。
Solution:知道物理边距影响因素后,打印问题的处理通常有两种方案:
方案一:普遍采用的讲绘图内容进行位移
通常的做法是将可打印区域进行Offset平移,平移量为打印机物理边距(HardMarginX / HardMarginY),这样结合默认的打印纸张的页面边距(MarginLeft /MarginRight / MarginTop / MarginBottom)也能按照预览时的绘图一模一样的打印出来。
这种做法比较简单,只需要进行位移一下Graphics即可。但是注意,此时的页面边距不能为0,否则任然会出现打印绘图缺失现象,物理边距是不可避免的。要避免这种情况就需要判断页面边距的值,不能小于物理边距的值才行。
方案二:可以重新定义页面坐标,对页面尺寸进行裁剪
方案一在计算了左边和上边的不可打印区域,但是没有计算右边和下边的不可打印区域。这样位移和绘图的时候,就需要考虑是否打印出来的图正好在正中等问题。要精确控制就是将可打印区域裁剪到实际物理可打印的区域,如上图所示的绿色区域。
设定OriginAtMargins = false使得打印机的绘图Graphics的坐标原点在有效打印区域的边界上,即坐标原点为物理边距上,而不是在页面边距上。于是,我们绘制的时候,X坐标0到100,实际上是从HardMarginX+0到HardMarginX+100,在计算页面坐标的时候,考虑物理边距进行排除后,就能实现在可打印区域内(排除了物理不可打印区域)进行精确的绘图。
但是也要注意,在屏幕绘图和打印预览的时候,需要将坐标进行向下向右平移物理尺寸,使得绘图效果与打印效果一致。同时,针对不同的Dpi造成的误差,避免的办法是定义单位是mm,然后采用LPtoDP进行转换,在转换的过程中,会用到Graphics的Dpi属性进行计算,屏幕Dpi和打印机Dpi不同,但是得到最后的绘图效果将会统一,实现所见所得。
物理边距的误差
至于物理边距问题,有可能打印出来的实际纸张上的物理边距跟程序获取的边距有误差,原因可能是打印机自身有关,也可能跟纸张在纸盒中放置有偏移有关。但这都算是不可避免的误差了,也不知道如何校正,我们只需要实现实际绘图内容打印完整,且绘制内容的精确定位绘制即可!
?当纸张方向改变的时候,程序获取到的HardMarginX和HardMarginY会有所改变,不知是何原因啊? LandScape为false的时候:HardMarginX = 23,HardMarginY = 16 LandScape为true的时候:HardMarginX = 19,HardMarginY = 16 希望有网友能解决这问题的麻烦留个言指导一二哦。 |
其他关于物理边距讨论的一部分链接
(1) How to Find the ActualPrintable area
http://stackoverflow.com/questions/8761633/how-to-find-the-actual-printable-area-printdocument
(2) I am Loss in Printing Margins
http://bytes.com/topic/c-sharp/answers/275603-im-loss-printing-margins
(3)How to print in full page withoutmargins
http://www.tech-archive.net/Archive/DotNet/microsoft.public.dotnet.framework.drawing/2005-08/msg00311.html
(4)使用.Net 下的打印控件进行预览和打印时的模型初探
http://blog.csdn.net/windcoder/article/details/8178096
http://www.cnblogs.com/carekee/articles/2178308.html
引用(略)
(2)绘图效率完整解决方案——三种手段提高GDI/GDI+绘图效率
http://www.cnblogs.com/fyhui/archive/2011/06/09/2076298.html
现在的cpu飞快,其实数学计算一般很快,cpu大部分时间是在处理绘图,而绘图有三种境界:1>每次重绘整体Invalidate(); 2>每次局部绘制Invalidate(Rect); 3>有选择的局部绘制。 不能说,一定是第三种方式好,得视情况,境界高程序肯定就复杂,如果对效率要求不高或者绘图量小当然直接用第一种方式。然而,稍微专业点的绘图程序,第一第二种方式肯定满足不了要求,必须选用第三种方式。而第三种方式的手段多样,也得根据实际情况拿相应的解决之道。这里讲解一般的三种手段,他们可以联合使用。 1. 缓存——Bitmap或者DoubleBuffer。缓存就是先把绘制的图形绘制到一张内存位图上,然后在一次性的贴位图,他可以提高绘图速度,也能避免闪烁。DoubleBuffer=true是C#窗体的属性,设置了此属性估计系统本身会起用无效区的内存位图缓存,而不需要程序员Bitmap处理。 2. 合理利用无效区域。无效区域就是系统保存当前变化需要重绘的区域,可以在OnPaint()中,e.ClipRectangle直接获得,也可以通过其他方式获得。Windows系统只会重绘无效区域内的绘图信息,然而我们用户的绘制代码一般是绘制整个区域的,很多时候无效区域只是一小部分区域,虽然执行了所有的绘图代码,但是Windows系统只会重新更新无效区域内的绘图。这里有两个利用点:1>用户请求重绘时,只请求重绘指定区域的,而不是整个区域,如Invalidate(Rect);2>在用户绘图代码Graphics g; g.DrawLine\g.DrawString\g.FillRectangle...前,先判断绘图的内容是否在无效区域,如果不是就不直接g.Draw...绘图代码。 3. 直接贴图。一般绘图或者重绘是Windows根据无效区域绘制的,如果在鼠标移动时需要重绘通过Windows系统处理Paint消息,有时满足不了要求,比如①鼠标移动绘制十字测量线就得用异或线而不是Paint消息,又比如②鼠标移动绘制跟随的信息提示框需要频繁擦除上次覆盖的背景,又比如③台球滚动时台球与球桌背景的关系。类似的这些问题如何解决?首先肯定不能利用Windows原来的绘图机制。其中一种解决方式是,不断的帧间变化区域贴内存位图——②中的信息框每次鼠标位置变化时可以重新g.Draw...或者贴早生成的信息框内存位图,②中被信息框覆盖的背景应该把本来的大背景截取此需要擦除区域的位置大小位图贴回来就是擦除背景了。由于每次大背景发生变化时,都应会重新生成大背景内存位图,所以可以是变化的背景。 这三种方式可以一起使用,应该可以解决中等的绘图项目的效率问题。中大型的绘图,必须记住两点1>只绘制电脑屏幕能显示的部分;2>只绘制变化的部分。 |
文章已经很长了,不想一一列举,以下是一些有用的参考链接:
(1)<基础的异或作图方法>VC橡皮筋绘图技术的实现(异或模式绘图)
http://xvdongming001.blog.163.com/blog/static/739891892008613516138/
(2)
http://blog.163.com/xuanmingzhiyou@yeah/blog/static/14247767620116201178195/
(3)
http://www.tp5u.com/winForm/1794.html
校正:这篇文章中公布了一个异或法绘制直线的方法,但是其中关于MoveToEx的GDI库调用函数存在问题,需要增加ref关键字引用,
[DllImport("gdi32.dll")]
private static extern bool MoveToEx(IntPtr hDC, int x, int y, ref POINTAPI
lpPoint);
调用函数:MoveToEx(hDC, 10, 10, ref ptsOld);
具体参考地址:
GDI32.DLL API函数MoveToEx 在C#2.0中的调用问题
http://bbs.csdn.net/topics/90040089
(4)
http://www.cnblogs.com/canson/archive/2011/07/09/2101862.html
(5)C#Color 和 VC++COLORREF 转化
http://blog.csdn.net/whchina/article/details/2639389
http://responsibility.blog.sohu.com/86726377.html
如果使用MFC与.NET混合编程,就会遇到这个问题,通过MFC编写的控件,由.NET调用,则控件中背景色的设置,需要颜色的转换。
COLORREF类型颜色的值COLORREFcr=RGB(123,200,12);
其中的R、G、B三个分量的排列顺序是BGR。
.NET中通过数据类型Color表示颜色,该类有一个函数FromArgb(int,int,int),可以通过输入RGB三个值得到一个Color类型的颜色。同时也有一个ToArgb()函数,得到一个32位的整数值,
32位ARGB值的字节顺序为AARRGGBB。由AA表示的最高有效字节(MSB)是alpha分量值。分别由RR、GG和BB表示的第二、第三和第四个字节分别为红色、绿色和蓝色颜色分量
Color到COLORREF |
COLORREF到Color |
uint GetCustomColor(Color color) { int nColor = color.ToArgb(); int blue = nColor & 255; int green = nColor >> 8 & 255; int red = nColor >> 16 & 255; return Convert.ToUInt32( blue << 16 | green << 8 | red); } |
Color GetArgbColor(int color) { int blue = color & 255; int green = color >> 8 & 255; int red = color >> 16 & 255 ; return Color.FromArgb(blue, green, red); } 或者直接通过下面的代码: Color.FromArgb(nColorRef&255, nColorRef>>8&255,nColorRef>>16&255); |
注:(1)注意COLORREF中颜色的排列是BGR,红色分量在最后面;(2)上面的代码使用C#编写。 |
|
最后还有.NET自带的函数:ColorTranslator |
(6)异或绘图模式设置的Index值
绘图模式(drawing mode)指前景色的混合方式,它决定新画图的笔和刷的颜色(pbCol)如何与原有图的颜色(scCol)相结合而得到结果像素色(pixel)。
可使用CDC类的成员函数SetROP2 (ROP = Raster OPeration光栅操作)来设置绘图模式:
其中,R2_COPYPEN(覆盖)为缺省绘图模式,R2_XORPEN(异或)较常用。
CDC::SetROP2
int SetROP2(int nDrawMode);
返回值:绘图模式的前一次取值。可以取联机文档“Windows SDK”中提供的任意值。
参 数: nDrawMode 指定新的绘制模式,可以为下列值之一:
绘制模式 |
定义说明 |
索引值 |
R2_BLACK |
像素始终为黑色 |
1 |
R2_WHITE |
像素始终为白色 |
16 |
R2_NOP |
像素保持不变 |
11 |
R2_NOT |
像素为屏幕颜色的反色 |
6 |
R2_COPYPEN |
像素为笔的颜色 |
13 |
R2_NOTCOPYPEN |
像素为笔颜色的反色 |
4 |
R2_MERGEPENNOT |
像素为笔颜色或屏幕颜色反色 |
14 |
R2_MASKPENNOT |
像素为笔颜色与屏幕颜色反色 |
5 |
R2_MERGENOTPEN |
像素为笔颜色反色或屏幕颜色 |
12 |
R2_MASKNOTPEN |
像素为笔颜色反色与屏幕颜色 |
3 |
R2_MERGEPEN |
像素为笔颜色或屏幕颜色 |
15 |
R2_NOTMERGEPEN |
R2_MERGEPEN的反色 |
2 |
R2_MASKPEN |
像素为笔颜色与屏幕颜色 |
9 |
R2_NOTMASKPEN |
R2_MASKPEN的反色 |
8 |
R2_XORPEN |
像素为笔颜色异或屏幕颜色。连续异或两次会变为原来颜色 |
7 |
R2_NOTXORPEN |
R2_XORPEN的反色(同或) |
10 |
说明:
设置绘图模式。绘图模式指出笔与被填充对象的颜色是怎样同显示表面的颜色组合的。绘图模式只用于光栅设备,不用于矢量设备。绘图模式是双重的光栅操作代码,代表了两个变量所有可能的布尔组合,分别使用AND、OR、XOR(异或)和NOT运算符。
(7)字符串绘制,能否异或?(答曰:不能)
字符串输出绘制不能采用异或的方式进行擦除更新,那么需要实时的动态位置显示信息的时候如何解决?目前看到三种方案:
方案一:利用局部更新文字形成的位图方法(参考CSDN论坛,忘了地址)
如何通过两次绘制的方式从屏幕上擦除文字
OnDraw里
|
方案二:利用文字的点阵图输出
在背景上输出和擦除文字 http://eyinlu.blog.163.com/blog/static/242321612011627921975/
在背景上输出文字,并且可以不留痕迹的擦除。 |
方案三:直接用Label控件显示信息,让Label跟随鼠标移动显示文字内容
直接修改Label控件的Left和Top属性,更新其Text属性内容,然后改变控件位置,实现实时显示。默认情况下,控件不支持透明背景色。在属性框里设置background属性为transparent。同时修改Visible属性进行显示和隐藏。
工具栏主要操作项:
知道了原因,解决问题就很好办了。其实微软也考虑到了这个问题,提供了一个用于单位转换的类PrinterUnitConvert,如下所示:
If(System.Globalization.RegionInfo.CurrentRegion.IsMetric) Then
'如果使用的是公制单位
'将英制单位的数据转换成公制单位的数据
psd.PageSettings.Margins= PrinterUnitConvert.Convert (psd.PageSettings.Margins, PrinterUnit.Display,PrinterUnit.TenthsOfAMillimeter)
EndIf
pap.DefaultPageSettings= psd.PageSettings
Margins属性中保存的页面的上(Top)、下(Bottom)、左(Left)、右(Right)的页边距数值,利用 PrinterUnitConvert的Convert方法都可以转换,在上例中,PrinterUnit.Display是指1/100英寸的单位, PrinterUnit.TenthsOfAMillimeter是指1/100毫米的单位,这样就可以将英制单位转换为公制单位。
当然我们也可以自己编写代码进行转换,但请注意,转换时英制的单位是1/100英寸,转换后要以毫米为单位。
注意:转换时只须对纸张的页边距进行转换,纸张本身的宽度和高度在你选择一种纸张类型的时候,它已经自动帮你转换成英制单位了,千万不要画蛇添足。 以上我们介绍了如何利用PageSetupDialog对话框设置页面、公制与英制单位的换算,已经为打印程序的编写建立了一个良好的基础。接下来,我们就来介绍如何实现具体的特殊打印功能。
.NET的升级版本也可以一句话就解决问题:
// 打印页面设置
publicPageSetupDialogv_PrintPageSetDlg = newPageSetupDialog();
// 英制单位转换为公制单位
v_PrintPageSetDlg.EnableMetric = true;
结果如下:
但是注意:此时显示的25.4mm,实际上获取Margins属性的时候,仍然是100个1/100英寸,因此存在0.254的系数关系。
而程序设定的坐标转换是基于世界坐标(单位:m)、逻辑坐标(单位:mm)以及设备坐标(单位:pixel),因此需要将C#提供的英制单位1/100英寸转换为mm,然后最终转换为像素绘图坐标。
// mm转Pixel
publicfloat m_LPtoDP_X(floatv_f_Logicmm_X)
{
floatd_X = (float)((v_f_Logicmm_X / 25.4) *v_Graphic.DpiX);
returnd_X;
}
NOTE:在绘图时基于像素的时候,Graphics的绘图单位也必须要设定为Pixel,尤其是打印机事件参数的e.Graphics。
e.Graphics.PageUnit = GraphicsUnit.Pixel;
获取打印机相关信息后,将1/100英寸转换为像素:
float f_DeviceMargin_Left =m_LPtoDP_X(v_PrintDocument.DefaultPageSettings.Margins.Left * 0.254f);
float f_DeviceMargin_Top =m_LPtoDP_Y(v_PrintDocument.DefaultPageSettings.Margins.Top *0.254f);
若选择实际的物理打印机,此时的打印机存在物理边距,错误打印结果如下:
考虑到物理边距以后,打印事件参数(PrintPageEventArgs e)的绘图区域e.MarginBounds 需要进行向左和向上的偏移处理。
NOTE:打印机绘图用MarginBounds,而不是直接用PaperSize,那样计算起来很麻烦
classPrinterBounds
{
[DllImport("gdi32.dll")]
privatestaticexternInt32 GetDeviceCaps(IntPtrhdc, Int32 capindex);
privateconstintPHYSICALOFFSETX = 112;
privateconstintPHYSICALOFFSETY = 113;
publicreadonlyRectangleBounds;
publicreadonlyintHardMarginLeft;
publicreadonlyintHardMarginTop;
publicPrinterBounds(PrintPageEventArgs e)
{
IntPtrhDC = e.Graphics.GetHdc();
HardMarginLeft =GetDeviceCaps(hDC, PHYSICALOFFSETX);
HardMarginTop = GetDeviceCaps(hDC,PHYSICALOFFSETY);
e.Graphics.ReleaseHdc(hDC);
HardMarginLeft = (int)(HardMarginLeft * 100.0 / e.Graphics.DpiX);
HardMarginTop = (int)(HardMarginTop * 100.0 / e.Graphics.DpiY);
Bounds = e.MarginBounds;
Bounds.Offset(-HardMarginLeft, -HardMarginTop);
}
}
在.NET以后的版本中,也可以直接用打印文档类的获取物理边界属性
DefaultPageSettings.HardMarginX, DefaultPageSettings.HardMarginY
然后进行打印的时候,就基本不会存在边距相差太大的情况,不过还是存在1点几个毫米的误差。
(1)页面边距25.4mm时预览结果:
打印结果:左图为打印图纸左侧,右图为打印图纸右侧。
(2)边距为0mm时预览,由于计算绘图区域的时候,有物理边界存在,导致得到的绘图区域是经过了Offset向左向上平移的,所以有空白的地方出现在预览图像的右边和下边。
打印的时候出现物理边界的影响,导致绘图能完整打印出来,如下图所示:
Solve:不是误差,而是打印机绘图原点坐标问题2013/11/24,已解决
(2)实际物理打印机HP LaserJet P4515打印机,物理边距 (4.8, 4.1)mm,以前两张图是不同时期打印,图纸上的物理边距线存在打印不完全现象,这就是经常出现的打印不完全现象,可以看出两张图纸的上和顶部边距不同,这是人工装载纸盒的时候,可能存在一定偏差,造成打印不完全等各种现象。但是两张图都保证了页面边距完全的效果,都是有25.4mm的默认边距,实现真正的可打印区域的完全打印功能。
http://blog.sciencenet.cn/blog-244606-747345.html