理解繪圖規則
一般來說,Windows的一個優點(實際上是現代操作系統的優點)是它可以讓開發人員不考慮特定設備的細節。例如:不需要理解硬盤設備驅動程序,只需在相關的.NET類中調用合適的方法,就可以編程讀寫磁盤上的文件。這個規則也適用於繪圖。計算機在屏幕上繪圖時,把指令發送給視頻卡。問題是市面上有幾百種不同的視頻卡,大多數有不同的指令集合功能。如果把這個i考慮在內,在應用程序中為每個視頻卡驅動程序編寫在屏幕上繪圖的特定代碼,這樣的應用程序就根本不可能編寫出來。這就是為什么在Windows最早的版本中有Windows Graphical Device Interface(GDI)的原因。
GDI+提供了一個抽象層,隱藏了不同視頻卡之間的區別,這樣就可以調用Windows API函數完成指定的任務了,GDI還在內部指出在運行特定的代碼時,如果讓客戶機的視頻卡完成要繪制的圖形。GDI還可以完成其他任務。大多數計算機都有多個顯示設備---監視器、打印機。GDI成功的使應用程序所使用的打印機看起來與屏幕一樣。如果要打印某些東西,而不是顯示他們,只需告訴系統輸出設備是打印機,再用相同的方式調用相同的Windows API函數可以。
可以看出DC(設備環境)是一個功能非常強大的對象,在GDI下,所有的繪圖工作都必須通過設備環境完成。DC甚至可用於不涉及在屏幕或其他硬件設備上繪圖的其他操作,例如在內存中修改圖像。
GDI開發人員提供了一個相當高級的API,但它仍是一個基於舊Windows API並且有C語言風格函數的API,所以使用起來不是很方便。GDI+在很大程度上是GDI和應用程序之間的一層,提供了更直觀、基於繼承性的對象模型。盡管GDI+基本上是GDI的一個包裝器,但Microsoft已經能通過GDI+提供新的功能了並宣稱他又一些性能方面的改進。
1.GDI+命名空間
(不說了,自己看去吧!!!)
2.設備環境和Graphics對象
GDI使用設備環境(DC)對像識別輸出設備。DC對象存儲特定設備的信息並把GDI API函數調用轉換為要發送給設備的命令。還可以通過DC對象確定對應的設備有什么功能(如打印機是彩色還是黑白的)。如果要求設備完成它不能完成的任務,設備對象就會檢測到並采取措施。
DC對象不僅可以硬件還可以用作到Windows的一個橋梁。例如如果Windows知道只有一小部分應用程序窗口需要重新繪制,DC就可以捕獲和撤銷在該地區外的繪圖工作。因為DC與Windows的關系非常密切,通過Dc來工作就可以用其他方式簡化代碼。
繪制圖形
下面舉例來說明如何在應用程序的主窗口中繪圖。DisplayAtStartup
創建一個C# 應用程序並在啟動窗體時在構造函數中繪制它。這並不是在屏幕上繪圖的最佳方式,這個示例並不能在啟動后按照需要重新繪制窗體。這樣只是不必作太多的工作就可以說明一些問題。
首先把窗體的背景色設置為白色。如果使用設計視圖設置背景色,系統會自動添加代碼:
private void InitializeComponent()
{
this.AutoScaleBaseSize = new System.Drawing.Size(5,13);
this.BackColor = System.Drawing.Color.White;
this.ClientSize = new System.Drawing.Size(292,266);
this.Name = "Form1";
this.Text = "Form1";
}
接着給Form1構造函數添加代碼。使用窗體的CreateGraphics()方法創建一個Graphics對象,其中包括繪圖時需要的使用的Windows DC。創建的DC即與顯示設備相關也與窗口相關。
public Form1()
{
InitializeComponent();
Graphics dc = this.CreateGraphics();
this.Show();
Pen bluePen = new Pen(Color.Blue,3);
dc.DrawRectangle(bluePen,0,0,50,50); //矩形
Pen redPen = new Pen(Color.Red,2);
dc.DrawEllipse(redPen,0,50,80,60); // 橢圓
}
然后調用Show()方法顯示窗口。必須讓窗口立即顯示,因為在其顯示之前不能作任何工作。(沒有繪圖的地方)
最后顯示一個矩形和橢圓。注意其中坐標(x,y)表示從窗口的客戶區域左上角向右的X個像素,向下的Y個像素。
(其中DrawRectangle()和DrawEillipse()這兩個函數前面已經講過不再重復了。)
上面程序窗體如果最小化再恢復,繪制好的圖形就不見了。如果在該窗體上拖動另一個窗口,使之只遮擋一部分圖形,再把該窗口拖離這個窗體,臨時被遮擋的部分就消失了,只剩下一半橢圓或矩形了!原因是:如果窗體的一部分被隱藏了,Windows通常會立即刪除與其中顯示的內容相關的所有信息。在窗口的某一部分消失時,那些像素也就丟失了(即Windows釋放了保存這些像素的內存)。
但要注意窗口的一部分被隱藏了,當它檢測到窗口不再被隱藏時,就請求擁有該窗口的應用程序重新繪制其內容。這個規則有一些例外----窗口的一小部分被擋住的時間比較短(顯示菜單時)。一般情況下應用程序就需要在以后重新繪制它。
由於本示例把繪圖代碼放在Form1的構造函數中,故不能在啟動后再次調用該構造函數進行重新繪制。
使用OnPaint()繪制圖形
Windows會利用Paint事件通知應用程序完成重新繪制的要求。Paint事件的Form1處理程序處理虛方法OnPaint()的調用,同時傳給他一個參數PaintEventArgs。也就是說只要重寫OnPaint()執行畫圖操作。
下面創建一個Windows應用程序DrawShapes來完成這個操作。
protected override void OnPaint(PaintEventarges e)
{
base.OnPaint(e);
Graphics dc = e.Graphics;
Pen bluePen = new Pen(Color.Blue,3);
dc.DrawRectangle(bluePen,0,0,50,50);
Pen redpen = new Pen(Color.Red,2);
dc.DrawEllipse(redPen,0,50,80.60);
}
PaintEventArgs是一個派生自EventArgs的類,一般用於傳送有關事件的信息。PaintEventArgs有另外兩個屬性,其中一個比較重要的是Graphics實例,它們主要用於優化繪制窗口中需要繪制的部分。這樣就不必調用CreateGraphics(),在OnPaint()方法中獲取DC。
在完成我們的繪圖后,還要調用基類OnPaint()方法,因為Windows在繪圖過程中可能會執行一些他自己的工作。
這段代碼的結果與前面的示例結果相同,但當最小化或隱藏它時,應用程序會正確執行。
使用剪切區域
DrawShapes示例說明了在窗口中繪制的主要規則,但它並不是很高效。原因是它試圖繪制窗口中的所有內容,而沒有考慮需要繪制多少內容。如下圖所示,運行DrawShapes示例,當該示例在屏幕上繪制時,打開一個窗口,把它移動到DrawShapes窗體上,使之隱藏一部分窗體。
到現在為止一切正常。但移動上面的窗口時,DrawShapes窗口會再次全部顯示出來,WIndows通常會給窗體發送一個Paint事件,要求它重新繪制本身。矩形和橢圓都位於客戶區域的左上角,所以在任何時候都是可見的。在本例中不需要重新繪制這部分,而只要重新繪制白色背景區域。但是,Windows並不知道這一點,他認為應引發Paint事件,調用OnPaint()方法的執行代碼。OnPiant()不必重新繪制矩形和橢圓。
在本例中,沒有重新繪制圖形。原因是我們使用了設備環境。Windows將利用重新繪制某些區域所需要的信息預先初始化設備環境。在GDI中,被標記出來的重繪區域稱為無效區域,但在GDI+中,該術語改為剪切區域,設備環境知道這個區域的內容,它截取在這個區域外部的繪圖操作,且不把相關的繪圖命令傳送給顯卡。這聽起來不錯,但仍有一個潛在的性能損失。在確定是在無效區域外部繪圖前,我們不知道必須進行多少設備環境處理。在某些情況下,要處理的任務比較多,因為計算哪些像素需要改變什么顏色,將會占用許多處理器時間。
其底線是讓Graphics實例完成在無效區域外部的繪圖工作,肯定會浪費處理器時間,減慢應用程序的運行。在設計優良的應用程序中,代碼將執行一些檢查,以查看需要進行哪些繪圖工作,然后調用相關的Graphics實例方法。下面將編寫一個示例DrawShapesClipping,修改DisplayShapes示例,只完成需要的重新繪制工作。在OnPaint()代碼中,進行一個簡單的測試,看看無效區域是否需要繪制的區域重疊,如果是就調用繪圖方法。
首先,需要獲得剪切區域的信息。這需要使用PaintEventArgs的另一個屬性。這個屬性叫做ClipRectangle,包含要重繪區域的坐標,並包裝在一個結構實例System.Drawing.Rectangle中。Rectangle是一個相當簡單的結構,包含4個屬性:Top、Bottom、Left、Right。它們分別含矩形的上下的垂直坐標,左右的水平坐標。
接着,需要確定進行什么測試,以決定是否進行繪制。這里進行一個簡單的測試。注意,在我們的繪圖過程中,矩形和橢圓完全包含在(0,0)到(80,130)的矩形客戶區域中,實際上,點(82,132)就已經在安全區域中了,因為線條大約偏離這個區域一個像素。所以我們要看看剪切區域的左上角是否在這個矩形區域內。如果是,就重新繪制如果不是就不必麻煩了。
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics dc = e.Graphics;
if (e.ClipRectangle.Top < 32 && e.ClipRectangle.Left < 82)
{
Pen bluePen = new Pen(Color.Blue,3);
dc.DrawRectangle(bluePen,0,0,50,50);
Pen redPen = new Pen(Color.Red,2);
dc.DrawEllipse(redPen,0,50,80,60);
}
}
注意:這個結果和前一個結果完全相同,只是進行了早期測試,確定不需要重繪制的區域,提高了性能。還要注意這個是否進行繪圖測試是非常粗略的。還可以進行更精細的測試,確定矩形和橢圓是否要重新繪制。這里有一個平衡。可以在OnPaint()中進行更復雜的測試,以提高性能,也可以使OnPaint()代碼復雜一些。進行一些測試總是值得的,因為編寫一些代碼,可以更多的解除Graphics實例之外的會址內容,Graphics實例只是盲目地執行繪圖命令。
測量坐標和區域
GDI+使用幾個類似的結構來表示坐標或區域。下面介紹幾個結構,他們都是在System.Drawing命名空間中定義的。
結構 | 主 要 公 共 屬 性|
| Point | X,Y |
| PointF | |
| Size | Width,Height |
| SizeF | |
| Rectangle | Left,Right,Top,Bottom,Width,Height|
| RectangleF | ,X,Y,Location,Size |
(1) Point、PointrF結構
從概念上講,Point在這個結構中是最簡單的,在數學上,它完全等價於一個二維矢量,包含兩個公共整形屬性,表示它與某個特定位置的水平和垂直距離(在屏幕上),如下圖。
為了從點A到點B,需要水平移動20個單位,並向下垂直移動10個單位,在圖中標為x和y,這就是他們的一般含義。創建一個Point結構,表示他們:
Point ab = new Point(20,10);
Console.WriteLine("Moved{0} across,{1} down",ab.x,ab.y);
X 和 Y都是讀寫屬性,也可以在Point中設置這些值:
Point ab = new Point();
ab.X = 20;
ab.Y = 10;
Console.WirteLine("Moved{0} across,{1} down",ab.X,ab.Y);
注意:按照慣例,水平和垂直坐標表示為x和y(小寫),但對應的Point屬性是X和Y(大寫),因為在C#中,公共屬性的一般約定是名稱以大寫字母開頭。
PointF與Point完全相同,但X和Y屬性的類型是float,而不是int。PointF屬性用於坐標不是整數值得情況。已經為這些結構定義了數據類型轉換,這樣就可以把Point隱式轉換為PointF(這個轉換是結構之間的)。但沒有相應的逆過程,要把Point轉換為Point,必須顯示的復制值或使用下面的3個轉換方法:Round(),Truncate(),Ceiling()。
PointF abFloat = new PointF(25.5F,10.9F);
// converting to Point
Point ab = new Point();
ab.X = (int)abFloat.X;
ab.Y = (int)abFloat.Y;
Point ab1 = Point.Round(abFloat);
Point ab2 = Point.Truncate(abFloat);
point ab3 = Point.Ceiling(abFloat);
// but conversion back to PointF is implicit
PointF abFloat2 = ab;
在默認情況下,GDI+把單位看作是屏幕(或打印機,無論圖形設備是什么,都可以這樣認為)的像素,這就是Graphics對象方法把它們接受到的坐標看作其參數的方式。例如:點 new Point(20,10)表示在屏幕上水平移動20個像素,向下垂直移動10個像素。通常這些像素從窗體客戶區域的左上角開始測量,如上圖。但是,情況並不是如此。在某些情況下,需要以窗口的左上角(包括其邊框)為原點來繪圖,甚至以屏幕的左上角為原點。除特殊說明,大多數可以假定像素是相對於客戶區域的左上角。
(2) Size、SizeF結構
Size結構用於int類型,SizeF用於float類型。
在許多情況下Size結構與Point結構是相同的。有兩個整形屬性,表示水平和垂直距離----區別是兩個屬性的名稱是:Width和Heihgt。
Size ab = new Size(20,10);
Console.WriteLine("Moved {0} across,{1} down",ab.Width,ab.Height);
嚴格的講,Size在數學上與Point表示的含義相同;但在概念上它使用的方式略有不同。Point用於說明實體在什么地方,而Size用於說明實體有多大。但是Size和 Piont是緊密相關的,目前甚至支持他們之間的顯示轉換:
Point point = new Point();
Size size = (Size)point;
Point anotherPoint = (Point)size;
例如:前面繪制的矩形,其左上角的坐標是(0,0),大小是(50,50)。這個矩形的大小是(50,50),可以用一個Size實例來表示。其右下角的坐標也是(50,50),但它由一個Point來表示。
Point和Size結構的相加運算符都已經重載了,所以可以把一個Size加到Point結構上,得到另一個Point結構:
static void Main(string[] args)
{
Point topLeft = new Point(10,10);
Size rectangleSize = new Size(50,50);
Point bottomRight = topLeft + rectangleSize;
Console.WriteLine("topLeft = " + topLeft);
Console.WriteLine("bottomRight = " + bottomRight);
Console.WirteLine("Size = " + rectangleSize);
}
運行結果:
topLeft = {X=10,Y=10}
bottomRight = {X=60,Y=60}
Size = {Width=50,Height=50}
這個結果說明Point和Size的ToString()方法已被重寫並以{X,Y}的格式顯示。
還可以進行Point和Size之間的顯示數據類型轉換:
Point topLeft = new Point(10,10);
Size s1 = (Size)topLeft;
Point p1 = (Point)s1;
說明:s1.Width被賦予topLeft.X,s1.Height被賦予topLeft.Y的值。最后p1與topLeft的值相同。
接上一章內容
(3)Rectangle 和 RectangleF
這兩個結構表示一個矩形區域。與Point和Size一樣,這里只介紹Rectangle結構,Rectangle與RectangleF基本相同,但它的屬性類型是float類型,而Rectangle的屬性類型是int類型。
Rectangle可以看作由一個Point和一個Size組成,其中Point表示矩形的左上角,Size表示其大小。它的一個結構函數把Point和Size作為其參數。
下面重新編寫前面DrawShapes示例代碼,繪制一個矩形:
Graphics dc = e.Graphics;
Pen bluePen = new Pen(Color.Blue,3);
Point topLeft = new Point(0,0);
Size howBig = new Size(50,50);
Rectangle rectangleArea = new Rectangle(topLeft,howBig);
dc.DrawRectangle(bluePen,rectangleArea);
(4)Region
Region表示屏幕上一個有復雜圖形的區域。如下圖:
可以想象,初始化Region實例的過程相當復雜。從廣義上看,可以指定哪些簡單的圖形組成這個區域,或者繪制這個區域的邊界的路徑。這種處理就需要Region類。
在進行更高級的繪圖工作前介紹幾個調試問題。(有一些幫助大家還是看看吧!)
如果在本章的示例中設置了斷點,就會注意到調試圖形程序不是那樣簡單。因為進入和退出調試程序常常會把Paint信息傳送給應用程序。結果是OnPaint重載方法上設置的斷點會讓應用程序反復地繪制本身這樣程序就不能完成任何工作。
這是典型的一種情況。要明白程序為什么應用程序沒有正確顯示,可以在OnPaint上設置斷點。應用程序會像期望的那樣,遇到斷點后進入調試程序。此時在 前景上會顯示開發環境MDI窗體。如果把開發環境設置為滿屏顯示,以便更易於觀察所有的調試信息,就會完全隱藏目前正在調試的應用程序。
接着檢查某些變量的值,希望找出某些有用的信息。然后按F5,告訴程序繼續執行,告訴應用程序繼續執行,完成某些處理后,看看應用程序在顯示其他內容時會 發生什么。但首先發生的是應用程序顯示在前景中,Windows檢測到窗體再次可見,並提示給他發送了一個Paint事件。當然這表示程序遇到了斷點。如 果這就是我們希望的結果,那就很好。但更常見的是,我們希望以后在應用程序繪制了某些有趣的內容之后再遇到斷點。我們根本沒有在OnPaint中設置斷 點,應用程序也不會顯示它在最初的啟動窗口中顯示的內容之外的其他內容。
有一種方式可以解決這個問題。如果有足夠大的屏幕,最簡單的方式就是恢復開發環境窗口,而不是把它設置為最大化,使之遠離應用程序窗口,這樣應用程序就不 會被擋住了。但在大多數情況下,這並不是一個有效的解決方案,因為這樣會使開發環境窗口過小。另一個解決方案使用相同的規則,即使應用程序聲明為在調試時 放在最上層。方法是在Form類中設置屬性TopMost,這很容易在InitialzeComponet方法中完成:
priavte void InitialzeComponent()
{ this.TopMost = true; }
也可以在 Visual Studio 2005的屬性窗口中設置這個屬性。
窗口這是為TopMost 表示應用程序不會被其他窗口擋住(除了其他放在最上層的窗口)。它總是放在其他窗口的上面,甚至在另一個應用程序得到焦點時,也是這樣。這是任務管理器的執行方式。
利用這個技巧是必須小心,因為我們不能確定Windows何時會決定應為某種原因引發Paint事件。如果在某些特殊的情況下,OnPaint出了問題 (例如:應用程序在選擇某個菜單項后繪圖,但此時出了問題)。最好的方式是在OnPaint中編寫一些虛擬代碼,測試某些條件,這些條件只在特殊情況下才 為True。然后在if 塊中設置斷點,如下所示:
protected override void OnPaint(PaintEventArgs e)
{ // Condition() evaluates to true when we want to break
if (Condition() == true)
{ int ii = 0; // <-- SET BREAKPOINT!!! }
繪制可滾動的窗口---介紹如何繪制的內容不適合窗口的大小,需要做哪些工作。
下面擴展DrawShapes示例,來解釋滾動的概念。為了使該示例更符合實際,首先創建一個BIgShapes示例,該示例將矩形和橢圓畫大一些。此時將使用Point,Size,Rectange結構定義繪圖域,說明如何使用他們。Form1類的相關部分如下所示:
// member fields
private Point rectangleTopLeft = new Point(0,0);
private Size rectangleSize = new Size(200,200);
private Point ellipseTopLeft = new Point(50,200);
private Size ellipseSize = new Size(200,150);
private Pen bluePen = new Pen(Color.Blue,3);
private Pen redPen = new Pen(Color.Red,2);
private override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics.dc = e.Graphics; // member fields
if (e.ClipRectangle.Top < 350 || e.ClipRectangle.Left < 250)
{
Rectangle rectangleArea = new Rectangle(rectangleTopLeft,rectangleSize);
Rectangle ellipseArea = new Rectangle(ellipseTopLeft,ellipse.Size);
dc.DrawRectangle(bluePen,rectangleArea);
dc.DrawEllipse(redPen,ellipseArea);
}
}
注意這里還把Pen、Size、Point對象變成成員字段---這比每次需要繪圖時都創建一個新Pen的效率高。
這里有一個問題,圖形在300*300像素的繪圖區域中放不下。
一般情況下,如果文檔太大,不能完全顯示,應用程序就會添加滾動條,以便用戶滾動窗口,查看其中選中的部分。這是另一個區域,在該區域中如果使用標准控件建立Windows窗體,就讓.NET運行環境和基類處理程序。如果窗體上有各種控件,Form實例一般知道這些控件在哪里,如果其窗體可能比較小,Form實例就知道需要添加滾動條。Form實例還會自動添加滾動條,不僅如此,它還可以正確繪制用戶滾動到的部分屏幕。此時,用戶不需要在代碼中做什么工作。但在本章中,我們要在屏幕上繪制圖形,所以要幫助Form實例確認何時能滾動。
添加滾動條是很簡單的。Form仍會處理所有的操作---因為它不知道繪圖區域有多大。在上面的BigShapse示例中沒有滾動條的原因是,Windows不知道它們需要滾動條。我們需要確認的是,矩形的大小從文檔的左上角(或者是在進行任何滾動前的客戶區域左上角)開始向下延伸,其大小應足以包含整個文檔。本章把這個區域稱為文檔區域。在下圖可以看出,本例的文檔區域應是(250,350)像素。
使用相關的屬性Form.AutoScrollMinSize即可確定文檔的大小。因此給InitializeComponent()方法或Form1構造函數添加下述代碼:
private void InitializeComponent()
{
this.AutoScaleBaseSize = new System.Drawing.Size(5,13);
this.ClientSize = new System.Drawing.Size(292,266);
this.Name = "From1";
this.Text = "BigShapes";
this.BackColor = Color.White;
this.AutoScrollMinSize = new Size(250,350);
}
另外,AutoScrollSize屬性還可以用VS2005屬性窗口設置。
在應用程序啟動時設置最小尺寸,並保持不變,在這個應用程序中是必要的,因為我們知道屏幕區域一般是有多大。在運行該應用程序時,這個“文檔”是不會改變大小的。但要記住,如果應用程序執行顯示文件內容的操作,或者執行某些改變屏幕區域的操作,就需要在其他時間設置這個屬性(此時,必須手工調整代碼,VS2005屬性窗口只能在構建窗體時設置屬性的初始值)。
設置MinScrollSize只是一個開始,僅有它是不夠的。下圖為示例應用程序目前的外觀。
注意,不僅窗體正確設置了滾動條,而且他們的大小也正確設置了,以指定文檔正確顯示的比例。可以試着在運行示例重新設置窗口的大小,這樣就會發現滾動條會正確響應,甚至如果窗口變得足夠大,不再需要滾動條時,他會消失。
但是,如果使用一個滾動條,並向下滾動它,會發生什么情況?如下圖,顯然,出錯了。
出錯的原因是我們沒有在OnPaint()重寫方法的代碼中考慮滾動條的位置。如果最小化窗口,再恢復它,重新繪制一遍窗口,就可以很清楚地看出這一點。結果如圖所示。
圖形像以前一樣進行了繪制,矩形的左上角嵌套在客戶區域的左上角,就好像根本沒有移動過滾動條一樣。
在更正這個問題前,先介紹一下在這些屏幕圖上發生了什么。
首先從BigShapes示例開始,如圖---所示。在這個例子中,整個窗口剛剛重新進行了繪制。看看前面的代碼,該代碼的作用是使graphics實例用左上角坐標(0,0)(相對於窗口客戶區域的左上角)繪制一個矩形---它是已經繪制過的。問題是,graphics實例在默認情況下把坐標解釋為是相對於客戶窗口的,它不知道滾動條的存在。代碼還沒有嘗試為滾動條的位置調整坐標。橢圓也是這樣。
下面處理圖---的問題。在滾動后,注意窗口上半部分顯示正確,這是因為它們是在應用程序第一次啟動時繪制的。在滾動窗口時,Windows沒有要求應用程序重新繪制已經顯示在屏幕中的內容。Windows只指出屏幕上目前顯示的內容可以平滑移動,以匹配滾動條的位置。這是一個非常高效的過程,因為它也能使用某些硬件加速來完成。在這個屏幕圖中,有錯的是窗口下部的1/3部分。在應用程序第一次顯示時,沒有繪制這部分窗口,因為在滾動窗口前,在部分在客戶區域的外部。這表示Windows要求BigShapes應用程序繪制這個區域。它引發Paint事件,把這個區域作為剪切的矩形。這也是OnPaint() 重載方法完成的任務。
問題的另一種表達方式是我們把坐標表示為相對於文檔開頭的左上角---需要轉換它們,使之相對於客戶區域的左上角。圖---說明了這一點。
為了使該圖更清晰,我們向下向右擴展了該文檔,超出了屏幕的邊界,但這不會改變我們的推論,我們還假定其上有一個水平滾動條和一個垂直滾動條。
在該圖中,細矩形標記了屏幕區域的邊框和整個文檔的邊框。粗線條標記試圖要繪制的矩形和橢圓。P標記要繪制的某個隨意點,這個點在后面會作為一個示例。在調用繪圖方法時,提供graphics實例和從B點到P點的矢量,這個矢量表示為一個Point實例。我們實際上需要給出從點A到點B的矢量。
不知道A點到P點的矢量,而知道B點到P點的矢量,這是P相對於文檔左上角的坐標---要在文檔的P點繪圖.還知道從B點到A點的矢量,這是滾動的距離,它存儲在Form類的一個屬性AutoScrollPosition中.但是不知道從A點到P點的矢量. 現在只需進行矢量相減即可.為了使之更簡便,Graphics類執行了一個方法來進行這些計算---TranlateTransform.提供水平和垂直坐標,表示客戶區域的左上角相對於文檔的左上角,然后Graphics設備考慮客戶區域相對於文檔區域的位置,計算這些坐標.
dc.TranslateTranform(this.AutoScrollPosition.X,this.AutoScrollPosition.Y);
在本例還要測試剪切區域,看看是否需要進行繪制工作.這個測試需要調整,把滾動的位置也考慮在內.完成后,該實例的整個繪圖代碼如下所示:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics dc = e.Graphics;
Size scrollOffset = new Size(this.AutoScrollPosition);
if (e.ClipRectangle.Top + scrollOffset.Width < 350 || e.ClipRectangle.Left + scrollOffset.Height < 250)
{
Rectangle rectangleArea = new Rectangle(rectangleTopLeft + scrollOffset, rectanlgeSize);
Rectangle ellipseArea = new Rectangle(ellipseTopLeft + scrollOffset,ellipseSize);
dc.DrawRectangle(bluePen ,rectangleArea);
dc.DrawEllipse(redPen,ellipseArea);
}
得到正確的滾動屏幕.
世界\頁面\設備坐標
測量相對於文檔區域左上角的位置和測量相對於屏幕(桌面)左上角的位置之間的區別非常重要,GDI+為它們指定了不同的名稱.
* 世界坐標(Word Coordinate):要測量的點距離文檔區域左上角的位置(以像素為單位).
* 頁面坐標(Page Coordiante):要測量的點距離客戶區域左上角的位置(以像素為單位).
注意: 熟悉GDI開發的人員要注意,世界坐標對應於GDI中的邏輯坐標.頁面坐標對應於設備坐標.編寫邏輯坐標和設備坐標之間的轉換代碼在GDI+中有了變化.在GDI中,轉化是使用Widows API函數LPtoDP()和DPtoLP()通過設備環境進行的,而在GDI+中,由Control類來維護轉化過程中的所需要的信息,Form和各種Windows窗體控件設備派生於Control類.
GDI+還有第3種坐標,即設備坐標(Device Coordinate).設備坐標類似於頁面坐標,但其測量單位不是像素,而是用戶通過調用Graphics.PageUnit屬性指定的單位.它可以使用的單位除了默認的像素外,還包括英寸和毫米.它可以用作獲取設備的不同像素密度方式.如:在監視器上,100像素約是1英寸.但激光打印機可以達到1200dpi(點/英寸)---這表示一個100像素寬的圖形在該激光打印機上打印時會比較小.把單位設置為英寸,指定圖形為1英寸寬,就可以確保圖形在不同的設備上有相同的大小.
顏色
在GDI+中,顏色用System.Drawing.Color結構的實例來表示。一般情況下,初始化這個結構后,就不能使用對應的Color實例對該結構進行一些操作了----只能把它傳送給其他的需要Color的調用方法。前面遇到這種結構,在前面的每個示例中都設置了窗口客戶區域的背景色,還設置了要顯示的各種圖形的顏色。Form.BackColor屬性返回一個Color實例。本節將詳細介紹這個結構,特別是要介紹構建Color的幾種不同方式。
(1) 紅綠藍(RGB)值
監視器可以顯示的顏色總數非常大---超過160億。其確切的數字是2的24方式,即16,777,216。顯示,需要對這些顏色進行索引,才能指定在給定的某個像素上要顯示什么顏色。
給顏色進行索引的最常見方式是把它們分為紅綠藍成份,這種方式基於以下原則:人眼可以分辨的任何顏色都是由一定量的紅色光、綠色光和藍色光組成的。這些光稱為成份(component)。實際上,如果每種成份的光分為256種不同的強度,它們提供了足夠平滑的過渡,可以把人眼能分辨出來的圖像顯示為具有照片質量。因此,指定顏色時,可以給出這些成份的量,其值在0~255之間,其中0表示沒有這種成份,255表示這種成份的光達到最大的強度。
這些出了向GDI+說明顏色的第一種方式。可以調用靜態函數Color.FromArgb()指定該顏色的紅綠藍值。微軟沒有為此提供構造函數,原因是除了一般的RGB成份外,還有其它方式表示顏色。因此,微軟認為i給定以的構造函數傳遞會引起誤解:
Color redColor = Color.FromArgb(255,0,0);
Color funnyOrangyBrownColor = Color.FromArgb(255,155,100);
Color blackColor = Color.FromArgb(0,0,0);
Color whiteColor = Color.FromArgb(255,255,255);
3個參數分別是紅綠藍指。這個函數有許多重載方法,其中一些也允許指定Alpha混合指(這是A在方法FromArgb()中的名稱)。Aplha混合超出了本章的范圍,但把它與屏幕上已有的顏色混合起來,可以描繪出半透明的顏色。這可以得到一些漂亮的效果,常用於游戲。
(2)命名顏色
使用FromArgb()構造顏色是一種非常靈活的技巧,因為它表示可以指定人眼睛辨識出的任何顏色。但是,如果要得到一些標准、眾所周知的純色,例如紅色或藍色,命名想要的顏色是比較簡單的。因此微軟還在Color中提供了許多靜態屬性,每個屬性返回一種命名顏色。在下面的實例中,把窗口的背景設置為白色時,就使用了其中一種屬性:
this.BackColor = Color.White;
// has the same effect as;
// this.BackColor = Color.FromArgb(255,255,255);
有幾百種這樣的顏色。完整的列表參見SDK文檔。包括所有的純色:紅、白、藍、綠和黑,還包括MediumAquamarine、LightCoral、DarkOrchid等顏色。還有一個KnownColor枚舉,列出了命名的顏色。
(3)圖形顯示模式和安全的調色板
原則上監視器可以顯示超出160億種RGB顏色,實際上這種取決於如何在計算機上這置顯示屬性。在Windows中,傳統上有3個主要的顏色選項:真彩色(24位)、增強色(16位)、256色。(在目前的一些圖形卡上,真彩色是32位的,因為硬件進行了優化,但此時32位中只有24位用於該顏色)。
只有真彩色模式允許同時顯示所有的RGB顏色。這聽起來是最佳選擇,但它是有代價的:完整的RGB值需要用3個字節來保存,這表示要顯示的每個像素都需要用圖形卡內存中的3個字節來保存。如果圖形卡內存需要額外的費用,就可以選擇其他模式。增強顏色模式用兩個字節表示以像素。每個RGB成份用5位就足夠了。所以紅色只有32種不同的強度,而不是256種。藍色和綠色也是這樣,總共有65535種顏色。這對於需要偶爾察看照片質量級的圖像來說是足夠了,但比較微妙的陰影區域會被破壞。
256色模式給出的顏色更少。但是在這種模式下,可以選擇任何顏色,系統會建立一個調色板,這是一個從160億RGB顏色中選擇出來的256種顏色列表。在調色板中指定了顏色后,圖形設備就只顯示所指定的這些顏色。當獲得高性能和視頻內存需要額外的費用時,才使用256色模式。大多數計算機游戲都使用 這種模式----它們仍能得到相當好的圖形,因為調色板經過了非常仔細的選擇。
一般情況下,如果顯示設備使用增強色或256色模式,並要顯示某種RGB顏色,它就會從能顯示的顏色池中選擇一種在數學上最接近的匹配顏色。因此知道顏色模式是非常重要的。如果要繪制某些涉及微妙陰影區域或照片質量級的圖像,而用戶沒有選擇24位顏色模式,就看不到期望的效果。如果要使用GDI+進行繪制,就應該用不同的顏色模式測試應用程序。
(4)安全調色板
這是一種非常常見的默認調色板。它工作的方式是為每種顏色成分這置6個間隔相等的值,這些值分別是0,51,102,153,204,255。換言之,紅色成分可以是這些值中的任一個。綠色成分和藍色成分也一樣。所以安全調色板中的顏色就包括(0,0,0)(黑色)、(153,0,0)(暗紅色)、(0,255,102)(藍綠色)等,這樣就得到了6的立方=216種顏色。這是一種讓調色板包含色譜中顏色和所有亮度的簡單方式,但實際上這是不可行的,因為數學上登間隔的顏色成分並不表示這些顏色的區別在人眼看來也是相等的。但安全調色板使用非常廣泛,相當多的應用程序和圖像仍然使用安全調色板上的顏色。
如果把Windows設置為256色模式,默認的調色板就是安全調色板,其中添加了20種標准的Windows顏色和20種備用顏色。
畫筆和鋼筆
本節介紹兩個輔助類,在繪制圖形時需要使用它們。前面已經見過了Pen類,它用於告訴工人graphics實例如何繪制線條。相關的類是System.Drawing.Brush,告訴graphics實例如何填充區域。例如,Pen用於繪制前面示例中的矩形和橢圓的邊框。如果需要把這些圖形繪制為實心的,就要使用畫筆指定如何填充它們。這兩個類有一個共同點:很難對他們調用任何方法。用需要的顏色和其他屬性構造一個Pen或Brush實例,再把它傳送給需要Pen或Brush的繪圖方法即可。
<注>:
如果使用以前的GDI編程,可能會注意到在前兩個示例中,在GDI+中使用Pen的方式是不同的.在GDI中,一般是調用一個WindowsAPI函數SelectObject(),它把鋼筆關聯到設備環境上.這個鋼筆用於所有需要鋼筆的繪圖操作中,直到再次調用SelectObject()通知設備環境停止使用它時為止.這個規則也適用於畫筆或其它對象,例如字體和位圖,而使用GDI+,微軟使用一種無狀態的模式,其中沒有默認的鋼筆或其它幫助對象.只需給每個方法調用指定合適的幫助對象即可.
<1>畫筆
GDI+有幾種不同類型的畫筆,這里只解釋幾個比較簡單的畫筆,每種畫筆都由一個派生自抽象類System.Drawing.Brush的類實例來表示.最簡單的畫筆System.Drawing.SolidBrush僅指定了區域用純色來填充:
Brush solidBeigeBrush = new SolidBrush(Color.Beige);
Brush solidFunnyOrangeyBrownBrush = new SolidBrush(Color.FromArgb(25,155,100));
另外,如果畫筆是一種Web安全顏色,就可以用另一個類System.Drawing.Brushes構造出畫筆.Brushes是永遠不能實例化的一個類(它有一個私有構造函數,禁止實例化).它有許多靜態屬性,每個屬性都返回指定顏色的畫筆.如下:
Brush solidAzureBrush = Burshes.Azure;
Brush solidChoolateBrush = BrushesChoolate;
比較復雜的一種畫筆是影線畫筆(hatch brush),它通過繪制一種模式填充區域,這種類型的畫筆比較高級,所以Drawing2D命名空間中,用System.Drawing.Drawing2D.HatchBrush類表示.Brushes類不能幫助我們使用影線畫筆,而需通過提供一個影線型式和兩種顏色(前景色和背景色,背景色可以忽略,此時將使用默認的黑色),來顯示構造一個影線畫筆.影線型式可以取自於枚舉Sysytem.Drawing.Drawing2D.HatchStyle,其中有許多HatchStyle值,其完整列表參閱SDK.
一般型式包括: ForwardDiagonal,Cross,DiagonalCross,SmallConfetti,ZigZag.示例如下:
Brush crossBrush = new HatchBrush(HatchStyle.Cross,Color.Azure);
// background color of CrossBrush is black
Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick,Color.DarkGoldenrod,Color.Cyan);
GDI只能使用實踐和影線畫筆,GDI+添加了兩種新畫筆:
* System.Drawing.Drawing2D.LinearGradientBrush用一種在屏幕上可變的顏色填充區域.
* System.Drawing.Drawing2D.PathGradientBrush與此類似,但其顏色沿着要填充的區域的路徑而變化.
<2>鋼筆
鋼筆只使用一個類System.Drawing.Pen來表示.但鋼筆比畫筆復雜一些,因為它需要指定線條應有多寬(像素),對於一條比較寬的線段,還要確定如何填充該線條中的區域.鋼筆還可以指定其他許多屬性,本章不討論它們,其中包括前面提到的Alignment屬性,該屬性表示相對於圖形的邊框,線條該如何繪制,以及在線條的末尾繪制什么圖形(是否使圖形光滑過度).
粗線條中的區域可以用純色填充,或者使用畫筆來填充.因此Pen實例可以包括Brush實例的引用.這是非常強大的,因為這表示可以繪制有影線填充或線性陰影的線條.構造Pen實例有四中不同的方式.可以通過傳送一種顏色,或者傳送一種畫筆.這兩個構造函數都會生成一個像素寬的鋼筆.另外,還可以傳送一種顏色或畫筆,以及一個表示鋼筆寬度的float類型的值.(該寬度必須是一個float類型的值,以防執行繪圖操作的Graphics對象使用非默認的單位,例如毫米或英寸,例如可以指定寬度是英寸的某個分數).例如可以構造如下的鋼筆:
Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick,Color.DarkGoldenrod,Color.Cyan);
Pen solidBluePen = new Pen(Color.FromArgb(0,0,255));
Pen solidWideBluePen = new Pen(Color.Blue,4);
Pen brickPen = new Pen(brickBrush);
Pen brickWidePen = new Pen(brickBrush,10);
另外,為了快速構造鋼筆,還可以使用類System.Drawing.Pens,它與Brushes類一樣.包括許多存儲好的鋼筆.這些鋼筆的寬度都是一個像素,使用通常的Web安全顏色,這樣就可以用下述方式構建一個鋼筆:
Pen solidYellowPen = Pens.Yellow;
繪制圖形和線條System.Drawing.Graphics有很多方法,利用這些方法可以繪制各種線條、空心圖形和實心圖型。下圖給出了只要方法。
在結束繪制簡單對象的主題前,用一個簡單示例來說明使用畫筆可以得到的各種可視效果。該實例是ScrollMoreShapes,它是ScrollShapes的修正版本。除了矩形和橢圓外,我們還添加了一條粗線,用各種定制的畫筆填充圖形。前面解釋了繪圖的規則,所以這里只給出代碼,而不作多的注釋。首先,因為添加了新畫筆,所以需要指定使用命名空間System.Drawing.Drawing2D:
using System;
using System.Collection.Generic;
using System.ComponentModel;
usgin System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Text;
using System.Windows.Forms;
接着是Form1類中的一些額外字段,其中包含了要繪制圖形的位置信息,以為要使用的各種鋼筆和畫筆:
private Rectangle rectangleBounds = new Rectangle(new Point(0,0),new Size(200,200));
private Rectangle ellipseBounds = new Rectangle(new Point(50,200),new Size(200,150));
private Pen bluePen = new Pen(Color.Blue,3);
private Pen redPen = new Pen(Color.Red,2);
private Brush solidAzureBrush = Brushes.Azure;
private Brush solidYellowBrush = new SolidBrush(Color.Yellow);
static private Brush brickBrush = new HatchBrush(HatchStyle.DiagonalBrick,Color.DarkGoldenrod,Color.Cyan);
private Pen brickWidePen = new Pen(brickBrush,10);
把BrickBrush字段聲明為靜態,就可以使用該字段的值初始化BrickWidePen字段了。C#不允許使用一個實例字段初始化另一個實例字段,因為還沒有定義要先初始化哪個實例字段,如果把字段聲明為靜態字段就可以解決這個問題,因為只實例化了Form1類的實例,字段是靜態字段還是實例字段就不重要了。