經常在開發實際的應用程序中,需要用到圖形繪制和打印程序。如何實現完整的精確打印和繪圖是需要注意許多細節地方的。最近在遇到打印問題的時候,仔細研究一陣,總結這篇博文,寫得有點雜亂,看文要還請費點神。
基本功能:窗體繪圖與鼠標交互
打印預覽與打印輸出
開發平台:VisualStudio 2010 (C#)
繪圖程序涉及到多種坐標系統,總體上可分為三個坐標系:世界坐標系、頁面坐標系以及設備坐標系。想要將圖形圖像會知道最終的設備上,中間需要做各種坐標轉換,下面將詳細介紹繪圖系統中的坐標轉換關系
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種映射方式
映射方式 |
邏輯單位 |
X 軸增加 |
Y 軸增加 |
毫米 |
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。此時存在四種屏幕顯示方式,如下表所示。
在屏幕窗體上繪圖的坐標系原點在左上角位置:
Origin = ( 0, 0)(Pixel)
邏輯坐標和設別坐標都基於該Origin點進行計算,具體的繪制轉換計算關系如下圖所示:
其中需要注意設置的是邏輯坐標,根據打印機選擇的紙張,設定頁面坐標系,同時考慮了打印機物理邊距和打印紙頁面邊距,真正用於坐標轉換的,只有PlotArea區域的尺寸大小。
在實際的物理打印中,經常出現以前問題:
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
(4)使用.Net 下的打印控件進行預覽和打印時的模型初探
http://blog.csdn.net/windcoder/article/details/8178096
繪圖工作開始后,需要進行一系列的繪圖參數設定,其中繪圖坐標系的設定最為重要。大概可分為以下幾個步驟:
Step 1:設定世界坐標系。給定需要繪制的世界坐標區域大小,相當於繪圖的視野范圍;
Step 2:設定頁面坐標系。即紙張大小,相當於邏輯坐標系。這點在打印機的繪制程序中尤為重要,同時為了實現所見即所得的繪圖思路,此處的窗體繪圖跟打印繪圖應該有良好的對應關系,所以設定的紙張應該一致。
Step 3:設定繪圖對象Graphics,可以是打印機繪圖,也可以是屏幕窗口繪圖。
Step 4:設定設備坐標系。給定繪圖設備的實際區域,如屏幕窗口大小、打印機的實際可打印區域等。
Step 5:輸入繪圖數據。
Step 6:繪圖輸出
簡單的繪制流程圖如下:
(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;
現在一般的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>只繪制變化的部分。 |
異或作圖法不同於普通的繪圖方法中的刷新需要按照Invalidate的區域全部重新繪制,它只是采用覆蓋繪制的方式,實現了擦除原有軌跡來達到刷新繪制,這樣就極大的提高了繪圖效率,避免不必要的區域性重繪耗費資源成本,也能十分有效的避免因為小區域或線型實時繪制等任務造成的整個繪圖窗體閃爍問題。需要注意的是,異或作圖法跟普通的畫法感覺最大的別扭就是需要“連續”的兩次繪圖,然后這兩次繪圖結果根據用戶設定的異或方式,進行異或計算,得出擦除、反色或覆蓋等結果。
其實現方式很簡單,直接調用GDI的相關函數進行設定即可,下圖為已實現異或填充繪圖。
文章已經很長了,不想一一列舉,以下是一些有用的參考鏈接:
(1)<基礎的異或作圖方法>VC橡皮筋繪圖技術的實現(異或模式繪圖)
http://xvdongming001.blog.163.com/blog/static/739891892008613516138/
(2)<C#實現異或作圖方法>C#調用GDI實現.NET中XOR、AND和OR模式的貼圖(填充不規則圖形)
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
(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屬性進行顯示和隱藏。
繪圖內容數據管理按照數據庫方式的拓撲結構進行管理,而單個對象則采用面向對象方式進行存儲和管理操作。
工具欄主要操作項:
利用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的系數關系。
這里需要統一打印時的度量單位,打印文檔的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);
相同的打印繪圖內容,如果是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向左向上平移的,所以有空白的地方出現在預覽圖像的右邊和下邊。
打印的時候出現物理邊界的影響,導致繪圖能完整打印出來,如下圖所示:
// 繪制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,已解決
測試采用普通的A4紙張,並設置為橫向,如下圖所示:
查看打印閱覽,跟繪圖內容是否相同,測試結果一致。
選用兩種打印成果(虛擬打印 + 物理打印),如下圖所示:
(1)AdobePDF虛擬打印機,無物理邊距打印
(2)實際物理打印機HP LaserJet P4515打印機,物理邊距 (4.8, 4.1)mm,以前兩張圖是不同時期打印,圖紙上的物理邊距線存在打印不完全現象,這就是經常出現的打印不完全現象,可以看出兩張圖紙的上和頂部邊距不同,這是人工裝載紙盒的時候,可能存在一定偏差,造成打印不完全等各種現象。但是兩張圖都保證了頁面邊距完全的效果,都是有25.4mm的默認邊距,實現真正的可打印區域的完全打印功能。