首页 > 代码库 > C#实现无物理边距真正可打印区域的绘图\打印程序开发

C#实现无物理边距真正可打印区域的绘图\打印程序开发

经常在开发实际的应用程序中,需要用到图形绘制和打印程序。如何实现完整的精确打印和绘图是需要注意许多细节地方的。最近在遇到打印问题的时候,仔细研究一阵,总结这篇博文,写得有点杂乱,看文要还请费点神。

基本功能:窗体绘图与鼠标交互

 打印预览与打印输出

开发平台:VisualStudio 2010 C#

1绘图坐标系统

1.1绘图系统坐标转换(屏幕窗口/打印机)

   绘图程序涉及到多种坐标系统,总体上可分为三个坐标系:世界坐标系、页面坐标系以及设备坐标系。想要将图形图像会知道最终的设备上,中间需要做各种坐标转换,下面将详细介绍绘图系统中的坐标转换关系

1、世界坐标

实际的绘图区域,如港口码头的2000米长度的范围、测井3000米的测量深度等。通常的实际应用中如果用到有打印这样的精确绘制功能,则还需要注意由世界坐标映射到逻辑坐标的比例,有两种方式:

(1)根据绘图窗口大小动态的计算映射比例,一般绘图都是这样,这种方式可以让用户更加方便的阅览全局绘图;

(2)设定映射比例为一个定值,计算出相应的转换坐标,如测井中绘图的MD 1:200这样的参数,这样绘图区域大小是不变的,超出窗口位置要设定滚动条拖动显示。

通常还需要根据需要,把以上两种方式都实现出来,供选择使用,如Adobe PDF浏览器既具有整页缩放功能,也有按比例缩放这样的功能。

2、页面坐标

绘图页面总体设计大小,如默认选择A4的纸张(210mm * 297mm),此时系统建立基于所选纸张大小区域作为逻辑坐标范围。

3、设备坐标

(1)屏幕窗口:根据具体绘图区域获取,如屏幕窗口绘图的客户区ClientRectangle等,单位是pixel。

(2)打印机:绘图区域也可以是打印机的实际绘制区域,但此时要注意单位,VS.NET默认的是单位1/100 inch,即VC++编程用到映射模式中的的MM_LOENGLISH映射方式,逻辑单位为0.01 inch(如下表中颜色标识项所示)。

Windows定义了的8种映射方式

MM_TEXT

像素点

与设备有关

MM_LOMETRIC

0. 1mm

0.1

MM_HIMETRIC

0. 01mm

0.01

MM_LOENGLISH

0. 01英寸

0.254

MM_HIENGLISH

0.001英寸

0.0254

MM_TWIPS

1/1440英寸

0.0176

MM_ISOTROPIC

任意(x=y)

可选

可选

可设

MM_ANISOTROPIC

任意(x!=y)

可选

可选

可设

注:MM_TWIPS经常在打印机上,单位是1/20磅(1磅=1/72英寸)。

三个坐标系转换关系如下图所示:

技术分享

由于打印机的分辨率参数经常是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。此时存在四种屏幕显示方式,如下表所示。

技术分享

1.2屏幕坐标系

在屏幕窗体上绘图的坐标系原点在左上角位置:

Origin = ( 0, 0)(Pixel)

逻辑坐标和设别坐标都基于该Origin点进行计算,具体的绘制转换计算关系如下图所示:

技术分享

   其中需要注意设置的是逻辑坐标,根据打印机选择的纸张,设定页面坐标系,同时考虑了打印机物理边距和打印纸页面边距,真正用于坐标转换的,只有PlotArea区域的尺寸大小。

1.3打印坐标系

在实际的物理打印中,经常出现以前问题:

Problem由于实际的物理打印机可能存在物理边距问题,使得实际的物理打印坐标存在偏移(PrintOffset),因此在获取页面边距后,需要进一步考虑物理边距,才能更好的计算实际的打印区域。

 

技术分享

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

 

1.4绘图操作流程

  绘图工作开始后,需要进行一系列的绘图参数设定,其中绘图坐标系的设定最为重要。大概可分为以下几个步骤:

Step 1:设定世界坐标系。给定需要绘制的世界坐标区域大小,相当于绘图的视野范围;

Step 2:设定页面坐标系。即纸张大小,相当于逻辑坐标系。这点在打印机的绘制程序中尤为重要,同时为了实现所见即所得的绘图思路,此处的窗体绘图跟打印绘图应该有良好的对应关系,所以设定的纸张应该一致。

Step 3:设定绘图对象Graphics,可以是打印机绘图,也可以是屏幕窗口绘图。

Step 4:设定设备坐标系。给定绘图设备的实际区域,如屏幕窗口大小、打印机的实际可打印区域等。

Step 5:输入绘图数据。

Step 6:绘图输出

简单的绘制流程图如下:

技术分享

1.5交互操作中屏幕坐标系与打印坐标系的转换

(1)鼠标屏幕拾取转换,讲鼠标坐标值(Pixel)转换为页面坐标值(mm),需要考虑滚动条位置、物理边距和屏幕显示缩放系数等,页面坐标(mm)转换为世界坐标(M)直接调用函数即可。伪代码如下:

// 需要考虑滚动条位置

Graphics g = panel1.CreateGraphics();

int i_PosHor = panel1.HorizontalScroll.Value;

int i_PosVer =panel1.VerticalScroll.Value;

int i_OffsetX = (int)(m_LPtoDP_X(v_f_LogicHardMarginX, g) /v_f_ScaleZoomX) - i_PosHor;

int i_OffsetY = (int)(m_LPtoDP_Y(v_f_LogicHardMarginY, g) /v_f_ScaleZoomY) - i_PosVer;

float v_f_LogicMousePosDownX =m_DPtoLP_X(e.X + i_PosHor, g) * v_f_ScaleZoomX - v_f_LogicHardMarginX;

float v_f_LogicMousePosDownY =m_DPtoLP_Y(e.Y + i_PosVer, g) * v_f_ScaleZoomY - v_f_LogicHardMarginY;

转换为世界坐标:

float v_f_WorldMousePosDownX= m_LPtoWP_X(v_f_LogicMousePosDownX);

 

(2)世界坐标转换为页面坐标和设备坐标伪代码:

Graphics g = panel1.CreateGraphics();

float v_f_WorldX = 2000;

float v_f_LogicTempX = m_WPtoLP_X(fX);

float v_f_DeviceTempX =m_LPtoDP_X(v_f_TempX, g) / v_f_ScaleZoomX;

1.6提高绘图效率

   现在一般的GDI和GDI+绘图都没有问题,关键是提高绘图的效率,防止绘图刷新时的闪烁问题,在此参考了两篇高质量的网文如下:

(1)使用bitblt提高GDI+绘图的效率(转)

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.7异或作图法响应鼠标实时绘制

   异或作图法不同于普通的绘图方法中的刷新需要按照Invalidate的区域全部重新绘制,它只是采用覆盖绘制的方式,实现了擦除原有轨迹来达到刷新绘制,这样就极大的提高了绘图效率,避免不必要的区域性重绘耗费资源成本,也能十分有效的避免因为小区域或线型实时绘制等任务造成的整个绘图窗体闪烁问题。需要注意的是,异或作图法跟普通的画法感觉最大的别扭就是需要“连续”的两次绘图,然后这两次绘图结果根据用户设定的异或方式,进行异或计算,得出擦除、反色或覆盖等结果。

   其实现方式很简单,直接调用GDI的相关函数进行设定即可,下图为已实现异或填充绘图。

技术分享

文章已经很长了,不想一一列举,以下是一些有用的参考链接:

1<基础的异或作图方法>VC橡皮筋绘图技术的实现(异或模式绘图) 

http://xvdongming001.blog.163.com/blog/static/739891892008613516138/

2<C#实现异或作图方法>C#调用GDI实现.NETXORANDOR模式的贴图(填充不规则图形) 

http://blog.163.com/xuanmingzhiyou@yeah/blog/static/14247767620116201178195/

3<C#实现异或作图方法C#中利用GDI作图解决异或问题

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<C#使用其他GDI方法接口定义>C#中使用GDI的简单总结

http://www.cnblogs.com/canson/archive/2011/07/09/2101862.html

5C#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表示的第二、第三和第四个字节分别为红色、绿色和蓝色颜色分量 

ColorCOLORREF

COLORREFColor

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里
 CDC mem;
 CBitmap memmap;
 mem.CreateCompatibleDC(pDC);
 memmap.CreateCompatibleBitmap(m_pDC,1000,1000);
 CBitmap *memoldmap=mem.SelectObject(&memmap);
 /*
在这里画你的字,对mem的```
 */
 pDC->BitBlt(0,0,1000,1000,&mem,0,0,SRCCOPY); 
 mem.SelectObject(memoldmap);
 memmap.DeleteObject();

重载BOOL CXXXView::OnEraseBkgnd(CDC* pDC) 
直接return TRUE;

 

方案二:利用文字的点阵图输出

在背景上输出和擦除文字

http://eyinlu.blog.163.com/blog/static/242321612011627921975/

在背景上输出文字,并且可以不留痕迹的擦除。
微软提供的 CDC 函数, TextOut 和 DrawText 不支持界面像素的异或运算操作,所以无法从背景中擦除输出的文字。要达到这种目的,只好使用支持像素的异或运算的图形函数了。
有两种方式可以选择:
 1.    输出轮廓路径,然后指定画刷填充封闭的路径区域。
 2.    得到文字的点阵图,使用向屏幕花点的方式输出文字。
下面给出第二种方式的实现代码。
调用两次下面的函数可以擦除输出的文字,还可以显示按任意角度旋转的文字。
注意:传入的设备上下文变量 pDC ,其中的 ROP2 属性应该指定为 R2_XORPEN 或 R2_NOTXORPEN 。
void  CDicfexView :: DrawVectorText ( CDC * pDC , CString & strText , CPoint  pntPosition , int nAngle ) 
 { 
      if ( strText . IsEmpty ()) 
         return ; 
   
      CClientDC dc ( this ); 
   
      // 确认要按文本输出的坐标映射模式
      if ( pDC -> GetMapMode () != MM_TEXT ) 
      { 
          // 转换到原坐标映射
          dc . SetROP2 ( pDC ->  GetROP2 () ); 
   
          pDC -> LPtoDP ( &  pntPosition ); 
          pDC = & dc ; 
      } 
   
      DWORD dwSize ; 
         
      MAT2 stM2 ; 
      GLYPHMETRICS stGM  ;         
      TEXTMETRIC stTM ; 
   
      // 创建字体
     CFont font ; 
      LOGFONT stFont ; 
      memset (& stFont ,0, sizeof ( stFont )); 
      // 设置字体结构的属性;
      stFont . lfHeight =12; 
      stFont . lfWeight = FW_NORMAL ; 
      stFont . lfClipPrecision = CLIP_LH_ANGLES ; 
      strcpy (( char *) stFont . lfFaceName ,  "Courier New" ); 
      stFont . lfEscapement =  nAngle * 10; 
      stFont . lfOrientation =  nAngle * 10; 
   
      font . CreateFontIndirect ( & stFont ); 
   
      CFont * pOldFont = pDC -> SelectObject (&  font ); 
     pDC -> SelectObject (& font ); 
      pDC -> GetTextMetrics (& stTM ); 
   
      // 坐标变换矩阵 , 但这种转换时 , 由于精度舍入的原因 , 取得的字体点阵的质量很差 , 
      // 给  stFont.lfEscapement 变量赋值 , 似乎微软做过优化 , 取得字体点阵勉强能说的过去 , 
      /*double nEscapement = nAngle * 3.1415926 / 180.0; 
     stM2.eM11 = FloatToFixed(cos(nEscapement)); 
      stM2.eM12 = FloatToFixed(sin(nEscapement)); 
      stM2.eM21 = FloatToFixed(-sin(nEscapement)); 
      stM2.eM22 = FloatToFixed(cos(nEscapement));*/ 
   
      stM2 . eM11 = FloatToFixed (1.0); 
      stM2 . eM12 = FloatToFixed (0.0); 
      stM2 . eM21 = FloatToFixed (0.0); 
      stM2 . eM22 = FloatToFixed (1.0); 
   
      int nChar = 0;  
     int nSx = pntPosition . x ; 
     int nSy = pntPosition . y ; 
   
      int nFontSpace = 0; 
   
      int nCx = 0; 
     int nCy = 0; 
   
     for ( int i = 0; i < strText . GetLength (); i ++) 
     { 
         if ( strText . GetAt ( i ) >=  0) 
             nChar =  strText . GetAt ( i ); 
         else 
         { 
                // 宽字节
             int th =  strText . GetAt ( i ); 
             int tl =  strText . GetAt ( i + 1); 
             nChar = ((  th & 0x00ff)<<8) + ( tl & 0x00ff); 
             i ++; 
         } 
    
          // 得到字体轮廓信息的尺寸
          dwSize = pDC -> GetGlyphOutline  ( nChar , GGO_BITMAP ,& stGM , 0L , NULL ,& stM2 ); 
          // 定义缓冲区
          BYTE * pBuffer = new BYTE [  dwSize ]; 
          memset ( pBuffer , 0, dwSize  ); 
          // 取得字体轮廓
          pDC -> GetGlyphOutline (  nChar , GGO_BITMAP , & stGM , dwSize , pBuffer , & stM2 ); 
      
          int nStride = dwSize / stGM  . gmBlackBoxY ; 
   
          // 轮廓数据
          OUTLINETEXTMETRIC     stOtm ;  
          memset ( & stOtm , 0,  sizeof ( stOtm ) );  
          stOtm . otmSize = sizeof (  stOtm );  
          pDC ->  GetOutlineTextMetrics ( sizeof ( stOtm ), & stOtm ); 
   
          //x 的偏移量
          int nXOffset = stGM .  gmptGlyphOrigin . x ;     
          //y 的偏移量     字的顶部         - y 方向原点  都是相对于  baseline  
          int nYOffset = stOtm .  otmAscent - stGM . gmptGlyphOrigin . y ;    
          
          for ( int y =0; y < stGM  . gmBlackBoxY ; ++ y ) 
          { 
                for ( int x =0; x < nStride ; ++ x ) 
                { 
                     for ( int k =0; k < 8; ++ k ) 
                     { 
                         if ( ( pBuffer [ y * nStride + x ] >> (7- k ) ) & 1 ) 
                         { 
                              int nX = nCx + 8 * x + k + nXOffset ; 
                              int nY = nCy + y + nYOffset ; 
   
                              pDC -> SetPixel ( nSx + nX , nSy + nY , RGB (255,200,200) ); 
                         } 
                     } 
                } 
          } 
   
          // 设置下一个字符的位置
          nCx += stGM . gmCellIncX ; 
         nCx += nFontSpace ; 
         nCy += stGM . gmCellIncY ; 
   
          delete [] pBuffer ; 
     }   
     pDC -> SelectObject ( pOldFont ); 
 }

方案三:直接用Label控件显示信息,让Label跟随鼠标移动显示文字内容

直接修改Label控件的Left和Top属性,更新其Text属性内容,然后改变控件位置,实现实时显示。默认情况下,控件不支持透明背景色。在属性框里设置background属性为transparent。同时修改Visible属性进行显示和隐藏。

2数据管理

绘图内容数据管理按照数据库方式的拓扑结构进行管理,而单个对象则采用面向对象方式进行存储和管理操作。

3窗体绘图显示设计

技术分享

工具栏主要操作项:

技术分享

技术分享

4绘图打印

4.1公制与英制单位的换算问题

   利用PageSetupDialog对话框设置纸张的类型、页边距等信息后,再次进入页面设置的对话框,发现里面的页边距全部改变了,再进入又改变了,这是为什么呢?

其实原因很简单,单位的不同造成了这个现象。我们可以再看看上图中“页边距”一项明确的注明了单位采用的是“毫米”,说明在页面设置对话框中使用的是公制长度计量单位,而在.net中采用的是英制的计量单位。英制中长度的基本计量单位是英寸,公制中长度的基本计量单位是厘米,打印时默认的长度单位为 1/100英寸。因此假设我们在页面设置对话框中设置上部边距为10mm(如下左图),但.net把它转换成了英制单位,数值是1/2.54 * 100=39个1/100英寸,(1英寸约等于2.54厘米,1厘米=10毫米)所以,这时上部页边距的数值变成了39,当你再次打开页面设置对话框时,系统将认为上部页边距是39个1/100厘米,也就是3.9毫米(如下右图),按下“确定”按钮后,.net将再次对页边距进行转换,这时上部边距就约为15个 1/100英寸,这样结果当然与我们设置的相差甚远。

技术分享

知道了原因,解决问题就很好办了。其实微软也考虑到了这个问题,提供了一个用于单位转换的类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的系数关系。

4.2绘图单位转换问题

这里需要统一打印时的度量单位,打印文档的DefaultPageSettings中的参数都是以1/100英寸为单位,而窗口界面绘图中的尺寸获取(width和Height)是以像素为单位,而中文操作系统中以毫米为单位(如打印设置页面),在打印时如何统一单位是必须要进行的。

技术分享

而程序设定的坐标转换是基于世界坐标(单位: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);

4.3关于物理页边距

相同的打印绘图内容,如果是PDF虚拟打印机,则是严格的根据页面边距打印出来;而物理打印机出现偏差。如前面所阐述的打印坐标系原点,如果设定为true,怎会出现以下问题:

// 打印文档

       publicPrintDocument v_PrintDocument = newPrintDocument();

// 边距

       v_PrintDocument.OriginAtMargins = true;

利用Adobe PDF虚拟打印机打印出来的PDF文档,然后在PDF文档中打印出来的页边距是准确的,标准的默认25.4mm页面边距,如下图所示:

技术分享

若选择实际的物理打印机,此时的打印机存在物理边距,错误打印结果如下:

技术分享

考虑到物理边距以后,打印事件参数(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向左向上平移的,所以有空白的地方出现在预览图像的右边和下边。

技术分享

打印的时候出现物理边界的影响,导致绘图能完整打印出来,如下图所示:

技术分享

4.4误差?绘制问题

// 绘制0边距位置

Graphics.DrawLine(Pens.Red, 0, 0, m_LPtoDP_X(v_PrintDocument.DefaultPageSettings.PaperSize.Width* 0.254f), 0);

Graphics.DrawLine(Pens.Blue,  0, 0, 0,m_LPtoDP_Y(v_PrintDocument.DefaultPageSettings.PaperSize.Height * 0.254f));

用Adobe PDF虚拟打印结果如下:在设置页边距为0的时候,可以准确的打印到0位置(如左边距),但是如果存在页边距的情况,打印0位置会出现偏差(如上边距),不知道为什么?

 

技术分享

Solve不是误差,而是打印机绘图原点坐标问题2013/11/24,已解决

4.5打印结果

4.5.1页面设置

测试采用普通的A4纸张,并设置为横向,如下图所示:

技术分享

 

4.5.2打印预览

查看打印阅览,跟绘图内容是否相同,测试结果一致。

 

技术分享

4.5.3打印成果图

选用两种打印成果(虚拟打印 + 物理打印),如下图所示:

1AdobePDF虚拟打印机,无物理边距打印

 

技术分享

(2)实际物理打印机HP LaserJet P4515打印机,物理边距 (4.8, 4.1)mm,以前两张图是不同时期打印,图纸上的物理边距线存在打印不完全现象,这就是经常出现的打印不完全现象,可以看出两张图纸的上和顶部边距不同,这是人工装载纸盒的时候,可能存在一定偏差,造成打印不完全等各种现象。但是两张图都保证了页面边距完全的效果,都是有25.4mm的默认边距,实现真正的可打印区域的完全打印功能。

技术分享

 

技术分享

C#实现无物理边距真正可打印区域的绘图\打印程序开发