程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#發現之旅第七講 C#圖形開發高級篇

C#發現之旅第七講 C#圖形開發高級篇

編輯:關於C#

為了讓大家更深入的了解和使用C#,我們將開始這一系列的主題為“C#發現之旅 ”的技術講座。考慮到各位大多是進行WEB數據庫開發的,而所謂發現就是發現我們所 不熟悉的領域,因此本系列講座內容將是C#在WEB數據庫開發以外的應用。目前規劃的主要內 容是圖形開發和XML開發,並計劃編排了多個課程。在未來的C#發現之旅中,我們按照由淺入 深,循序漸進的步驟,一起探索和發現C#的其他未知的領域,更深入的理解和掌握使用C#進 行軟件開發,拓寬我們的視野,增強我們的軟件開發綜合能力。

本系列課程配套的演示代碼,其中的 PenMarkLib.zip 就是本課程的演示代碼。

課程說明

經過以前幾次課程,相信 大家對圖形編程有所了解了,並能自己動手開發一些簡單的圖形軟件。今天我們就在以前圖 形開發課程的基礎上演示使用C#開發一個能保存簽名軌跡的圖形軟件。

這個軟件的用 戶界面如圖

功能需求

本軟件的功能需求如下

用戶能操作來開始一次簽名和結束 簽名。

正在簽名時,用戶按下鼠標按鍵開始繪制一條線條,松開鼠標按鍵結束繪制一 個線條。

一個簽名可以包含多個不相連的線條。

可保存簽名信息到XML文檔, 也可以從XML文檔加載簽名信息。

可以生成包含簽名圖形的圖片文件。

軟件設 計

實現能實用的簽名功能是很復雜的,則此簡化了一些功能,目標軟件僅操作簽名信 息,不涉及簽名時的文檔。於是本軟件的設計如下

文檔對象

實現復雜的圖形 軟件首先是設計文檔對象模型,使得內存中的一個個對象能包含要顯示的數據,此處需要設 計一套對象模型來包含簽名信息。

經過分析,可以知道,一個文檔中可以包含若干個 簽名,一個簽名包含若干個線條,而一個線條包含若干個點,線條中的點相互連接來形成線 條,而同一個簽名中的線條是不相連的,但可以相交。

因此我們可以設計出如下的文 檔對象模型

點坐標數據列表PointArrayList ,該對象用於存放多個點坐標數據,在 這裡表示一條任意線段,用戶繪制線條時程序可以使用該對象的Add方法增加點數據。

PenMarkInfo 對象表示一個簽名,該對象定義了簽名的時間,線條的顏色,線條寬度 ,還包含了若干個PointArrayList對象來保存簽名軌跡線條定位信息。

PenMarkInfoDocument 對象表示整體的簽名信息對象,該對象定義了多個簽名對象, 還定義了加載和保存文檔數據的方法。

視圖控件

設計的簽名信息文檔對象模 型後,我們還需要設計一個控件來顯示和操作簽名信息文檔。這個控件是從UserControl派生 的,它重寫了OnPaint方法來顯示簽名圖形,重寫了鼠標事件來添加新的簽名信息。還有一些 控件狀態控制模塊。

程序代碼說明

現根據軟件設計,使用VS.NET2003開發出 這個簽名軟件,現對該軟件的代碼進行說明,首先說明一下文檔對象模型相關的代碼。

PointArrayList

本類型用於維護一個可變長度的專門用於存儲點坐標數據的 列表。該對象實現了ICollection接口,還實現了自定義的枚舉器。本類型提供了Add, RemoveAt,Clear方法來維護點數據列表,還使用Offset方法來移動對象。內部還定義了一個 MyPointEnumerator對象實現了自定義的枚舉器。這裡還定義了一個Bounds 屬性來獲得包含 所有點的最小外切矩形區域。

在這裡我們順便研究一下C#中的枚舉器結構。我們都知 道,在VB.NET或C#中可以使用foreach 語法結構來遍歷枚舉一個數組中的所有的元素,使用 foreach比使用for要簡單不少。從本質上說能應用到foreach語法結構的對象都是實現了 System.Collections.IEnumerable接口,該接口只有一個方法 GetEnumerator(),任何類 型只要實現了IEnumerable接口即可用於foreach語句中。函數GetEnumerator()返回一個實 現了System.Collection.Ienumerator的對象。

為了實現自定義的枚舉器,我們需要 定義兩個類型,一個類型是實現了IEnumerable接口,另外一個實現了IEnumerator接口,其 具體內部實現毫無限制,因此我們可以根據需要開發出各種各樣的枚舉器來應用到foreach語 句中。

PointArryList對象沒有明確的定義實現IEnumerable接口,但它實現了 ICollection接口,而ICollection接口是從IEnumerable接口派生的,因此PointArrayList對 象是間接實現了IEnumerable接口。

關於枚舉器的詳細情況可參考MSDN中的相關說明 。

PenMarkInfo

本類型用於維護一個簽名信息對象。這個對象保存了簽名人的 姓名,簽名時間,簽名線條顏色,寬度。此外還有一個Lines屬性用於存放若干個 PointArrayList對象,這個Lines屬性就保存了多條簽名書寫軌跡信息。

PenMarkInfo 對象定義了一個Bounds屬性,用於獲得包含所有簽名軌跡的最小外切矩形。

PenMarkInfo對象定義了Lines屬性,該屬性返回一個列表,該列表的元素是 PointArryList類型,用於保存多個簽名線條的軌跡信息。這裡使用了一個類型為 XmlArrayItem的特性,這個特性影響對象的XML序列化。該特性說明該列表的元素類型是 PointArrayList,而且保存數組元素的XML元素名稱為Line。

該對象還定義了Draw函 數來繪制簽名圖形,在Draw函數中使用了Graphics對象的DrawLines函數,該函數是根據N個 點來繪制首尾相連的N減1個線段。Draw函數的代碼如下

/// <summary>
/// 繪制簽名圖形
/// </summary>
/// <param name="g">圖形繪制對象</param>
/// <param name="ClipRectangle">前切矩形</param>
public void Draw( Graphics g , Rectangle ClipRectangle )
{
  using( Pen pen = new Pen( this.Color , this.LineWidth ))
  {
    foreach( PointArrayList line in myLines )
    {
      if( ClipRectangle.IntersectsWith( line.Bounds ))
      {
         g.DrawLines( pen , line.ToArray());
      }
    }
   }
  }

這裡說明一下,一次調用DrawLines函數和多次調用DrawLine函數 是有差別的。由於線段的兩端是可以設置不同的樣式,DrawLines能一次性繪制多個線段,而 且相鄰線段的端點經過了連接處理;而使用DrawLine是一條條繪制線段的,相鄰線段的端點 沒有連接處理,當線條寬度很大或者圖形進行的放大處理則會暴露出問題,繪制的圖形不大 美觀。

為了向其他程序提供簽名信息,本對象還定義了CreateBitmap函數,該函數能 創建一個保存簽名圖形的位圖對象。該函數演示裡如何在內存中創建圖片的過程。

在 本函數中,首先是創建一個Bitmap對象,該圖片對象的大小等於簽名對象的大小,然後使用 Graphics的FromImage函數在這個圖片的基礎上創建一個圖形繪制對象,使用這個Graphics對 象進行繪圖操作都會在這個Bitmap上面留下痕跡,此時圖形輸出目標不是顯示器或者打印機 ,而是內存中的一個圖片。進行坐標轉化後調用對象的Draw函數來繪制圖形,繪制圖形完畢 後就提供這個bitmap對象給其他軟件使用了。

/// <summary>
/// 創建包含簽名圖形的圖片對象
/// </summary>
/// <returns>創建 的圖片對象</returns>
public Bitmap CreateBitmap()
{
   Rectangle bounds = this.Bounds ;
  Bitmap bmp = new Bitmap( bounds.Width , bounds.Height );
  using( Graphics g = Graphics.FromImage( bmp ))
   {
    g.TranslateTransform( -bounds.Left , -bounds.Top );
     Draw( g , bounds );
  }
  return bmp ;
}

這種在 內存中創建圖片的方法可用於任何類型的.NET程序中,我們可以在ASP.NET,命令行程序或者 Windows服務中使用這種方式來創建圖形,如此我們知道圖形編程不限於桌面軟件開發,任何 類型的軟件中都可以使用圖形編程。

PenMarkInfoDocument

本對象用於描述一 個完整的簽名文檔信息對象,它可以包含若干個簽名對象。並定義了加載和保存XML文件的功 能。

本對象使用XML序列化來保存數據到XML文檔,使用XML反序列化來從XML文檔來加 載對象數據。我們使用XmlSerializer對象來實現XML序列化和反序列化,該類型在名稱空間 System.Xml.Serialization下面。在XmlSerializer的幫助下,我們可以很方便的實現XML序 列化和反序列化。

這段代碼就是將對象序列化到XML文檔中。只要創建一個 XmlSerializer對象,指定要序列化的類型,指定XML書寫器,然後調用它的Serialize方法即 可完成序列化操作。

/// <summary>
/// 將對象序列化到XML文檔 中
/// </summary>
/// <param name="writer">XML文 檔書寫器</param>
public void Save( System.Xml.XmlWriter writer )
{
  XmlSerializer ser = new XmlSerializer( this.GetType());
   ser.Serialize( writer , this );
}

XML反序列化不能將加載的數據設 置到一個現有對象,而是需要重新創建一個對象,在這個代碼中定義了靜態函數能從XML文檔 反序利化生成一個新的簽名信息文檔對象。其代碼為

/// <summary>
/// 根據XML文檔反序列化生成簽名信息文檔對象
/// </summary>
/// <param name="strFileName">XML文件名</param>
/// <returns>生成的簽名信息對象列表</returns>
public static PenMarkInfoDocument Load( string strFileName )
{
   System.Xml.XmlTextReader reader = new System.Xml.XmlTextReader( strFileName );
  XmlSerializer ser = new XmlSerializer( typeof( PenMarkInfoDocument ));
  PenMarkInfoDocument list = ( PenMarkInfoDocument ) ser.Deserialize( reader );
  reader.Close();
  return list ;
}

在這個代碼 中,我們首先根據指定的XML文件名創建XML文檔讀取器,創建一個XmlSerializer對象,指定 要反序列化的對象類型,然後調用 Deserialize函數就可獲得一個反序列化所得的對象。

對WEB系統,XML序列化和反序列化是WebService的基礎,服務器端發送的數據首先序 列化為XML文檔然後使用HTTP協議發送出去,而客戶端獲得XML文檔使用XML反序列化來獲得對 象數據。

關於XML序列化和反序列化可參考MSDN文檔 Visual Studio.NET/.NET Frameword/使用.NET Framework編程/序列化對象/XML和SOAP序列化。

PenMarkControl

本類型從UseControl上派生的,用於在用戶界面上顯示和操 作簽名信息的。該類型是本演示程序中最復雜的部分。

我們首先看看這個控件是如何 繪制用戶界面的,我們找到該控件重寫的OnPaint函數,其代碼如下

/// <summary>
/// 繪制用戶界面
/// </summary>
/// <param name="e">參數</param>
protected override void OnPaint(PaintEventArgs e)
{
  base.OnPaint (e);
   e.Graphics.TranslateTransform( this.AutoScrollPosition.X , this.AutoScrollPosition.Y );
  System.Drawing.Rectangle ClipRect = e.ClipRectangle ;
  ClipRect.Offset( - this.AutoScrollPosition.X , - this.AutoScrollPosition.Y );
  System.Collections.ArrayList list = new ArrayList();
  list.AddRange( this.myDocument );
  if( this.Marking )
  {
    list.Add( this.myCurrentInfo );
  }
   foreach( PenMarkInfo info in list )
  {
    info.Draw( e.Graphics , ClipRect );
  }
  if( myCurrentInfo != null )
  {
     System.Drawing.Rectangle rect = myCurrentInfo.Bounds ;
     System.Windows.Forms.ControlPaint.DrawFocusRectangle( e.Graphics , rect );
  }
}

在本函數中首先是對圖形繪制對象和剪切矩形進行坐標轉化。 創建一個名為list的列表,列表中放置文檔中已經有的簽名對象和正在新建中的簽名對象, 然後遍歷所有的簽名對象,調用它們的Draw函數來繪制簽名圖形,最後根據當前簽名信息來 繪制焦點矩形,這裡的myCurrentInfo就是當前簽名信息對象。

這裡使用了類型 ControlPaint來繪制焦點矩形。在Windows用戶界面中,表示一個控件獲得輸入焦點,可以在 其界面上繪制焦點矩形。比如按鈕,當按鈕獲得焦點時,按紐裡就會繪制一個虛線的矩形邊 框,這個就是焦點矩形。類型ControlPaint中定義了一些靜態方法,用以模擬繪制一些 Windows標准控件的用戶界面,比如細邊框和3D的凸起或下陷邊框,模擬繪制菜單,單選框, 復選框等等。這個類型是一些Win32API函數的封裝,這些API函數有DrawEdge, DrawFrameControl等等,ControlPaint還提供一些方法能反轉屏幕上的像素,從而能實現橡 皮筋技術,而標准的Graphics對象是沒有像素反轉功能的。

PenMarkControl還重寫了 鼠標處理方法來實現新增簽名的功能。首先控件有兩種狀態,正在簽名狀態和普通狀態,當 控件處於正在簽名狀態,則用戶的鼠標拖拽操作就能增加新的簽名筆跡;否則用戶的鼠標拖 拽操作不會新增簽名筆跡。控件定義了一個名為Marking的屬性來表示控件是否處於新增簽名 狀態。其代碼如下

/// <summary>
/// 正在簽名中
/// </summary>
/// <remarks>若當前簽名對象存在而且還不屬於文檔則控件 處於新增簽名狀態</remarks>
public bool Marking
{
  get{ return myCurrentInfo != null && myDocument.Contains( myCurrentInfo ) == false ;}
}

控件定義了BeginMark和EndMark方法來開始和結束新增簽名 操作。其代碼為

/// <summary>
/// 開始進行新增簽名
/// </summary>
/// <param name="UserName">簽名者 </param>
/// <param name="LineWidth">簽名線條寬度 </param>
public void BeginMark( string UserName , int LineWidth )
{
  if( myCurrentInfo != null )
  {
     System.Drawing.Rectangle rect = myCurrentInfo.Bounds ;
    rect.Offset( this.AutoScrollPosition.X , this.AutoScrollPosition.Y );
     this.Invalidate( rect );
  }
  myCurrentInfo = new PenMarkInfo ();
  myCurrentInfo.Creator = UserName ;
   myCurrentInfo.CreationTime = DateTime.Now ;
  myCurrentInfo.LineWidth = LineWidth ;
}
/// <summary>
/// 結束新增簽名操作
/// </summary>
public void EndMark()
{
  if( this.Marking )
  {
    if( myCurrentInfo.Lines.Count > 0 )
    {
      myDocument.Add( myCurrentInfo );
    }
    else
    {
      myCurrentInfo = null;
    }
  }
}

在BeginMark中,程序重新設置了當前簽名信息對象為新對象,而且新對 象還未加入到文檔中,此時Marking 屬性返回true。

在EndMark中,若正在新增的簽 名信息包含了至少一條簽名筆跡則將對象添加到文檔中,否則刪除當前簽名信息對象,此時 Marking屬性返回false。

這個控件重寫了鼠標按鍵按下事件處理來開始新增簽名軌跡 ,其代碼為

/// <summary>
/// 當前處理的線條點集合
/// </summary>
private PointArrayList myCurrentLine = null;
/// <summary>
/// 最後一次點坐標
/// </summary>
private System.Drawing.Point LastPoint = System.Drawing.Point.Empty ;
/// <summary>
/// 處理鼠標按鍵按下事件
/// </summary>
/// <param name="e"></param>
protected override void OnMouseDown(MouseEventArgs e)
{
  base.OnMouseDown (e);
  if( this.Marking )
  {
    // 正在簽名
    myCurrentLine = new PointArrayList();
    LastPoint = new Point( e.X , e.Y );
   }
  else
  {
    // 判斷鼠標光標是否命中某個簽名的線條
    int x = e.X - this.AutoScrollPosition.X ;
    int y = e.Y - this.AutoScrollPosition.Y ;
    foreach( PenMarkInfo info in myDocument )
    {
      // 判斷鼠標光標是否命中某個簽名的某 個線條
      foreach( PointArrayList line in info.Lines )
       {
        foreach( Point p in line )
        {
          double r = ( p.X - x ) * ( p.X - x ) + ( p.Y - y ) * ( p.Y - y );
          if( r < 13 )
          {
            System.Drawing.Rectangle rect = info.Bounds ;
             if( myCurrentInfo != null )
            {
              rect = System.Drawing.Rectangle.Union( rect , myCurrentInfo.Bounds );
              rect.Offset( this.AutoScrollPosition.X , this.AutoScrollPosition.Y );
             }
            myCurrentInfo = info ;
             this.Invalidate( rect );
            goto EndElse ;
          }
        }
      }
    }
  EndElse: ;
  }
}

當用戶按下鼠標按鍵時,若控件 處於新增簽名狀態則開始一條簽名軌跡操作,初始化一些全局變量。若控件不是新增簽名狀 態,則修正鼠標光標坐標,並查找鼠標光標下簽名對象,在這裡判斷鼠標光標和某個簽名軌 跡上的某點的距離的平方是否小於13,若找到這個簽名對象則設置該簽名對象為當前簽名對 象。然後聲明控件的部分界面無效,需要重新繪制。

聲明控件用戶界面無效是調用控 件的Invalidate方法,若帶參數則聲明用戶界面部分無效,參數是一個矩形,在將來要調用 的OnPaint方法的剪切矩形就是這個參數。若無參數的調用Invalidate方法,則是聲明整個控 件的用戶界面無效,全部需要重新繪制。

髒矩形

在圖形編程中,我們經常需 要主動的聲明用戶界面無效,此時為了提高效率需要盡量減少聲明無效的用戶界面的面積, 這樣能減少未來調用的OnPaint方法中的工作量。此時就會用到一種名為“髒矩形 ”的圖形編程技術。程序應當收集用戶界面中真正需要重新繪制的區域,然後獲得這些 區域的最小外切矩形,該矩形就表示用戶界面中被用戶操作“弄髒”的區域,需 要重新繪制,於是調用Invalidate方法,參數就是這個髒矩形。

在這裡我們切換當前 簽名區域,真正需要重新繪制的區域是舊的當前簽名外切矩形和新的當前簽名的外切矩形。 因為顯示在舊的當前簽名的焦點矩形需要檫掉,而新的當前簽名要顯示焦點矩形。我們使用 Rectangle的Union獲得這兩個簽名的最小外切矩形,也就是髒矩形,這個髒矩形采用的是文 檔視圖坐標,由於控件的I年nvalidate方法采用的是控件客戶區坐標,此時還需要針對折射 原理對髒矩形進行坐標轉化,生成控件客戶區的髒矩形,然後調用控件的Invalidate方法聲 明控件部分用戶界面無效。

這裡還用到 了比較少見的goto語句。學校裡面的老師告訴我們,goto語句是萬惡之源,但編程是注重實 踐的,不應當搞教條主義,根據我的個人經驗,在少數情況下goto也是有用的,在這裡有一 個三重的foreach循環語句,為了快速退出這個套嵌循環結構,goto是最好的選擇了。

有人會提出使用 return 語句代替goto語句,認為這裡goto完成後就是退出函數,還 不如直接用return語句。這裡我就說明一下我的編程風格。我認為一個函數應當建議使用單 入口單出口模式,函數的單入口是天生的,單出口則不一定了,單出口就是函數必然是在函 數結尾處退出去,也就是說只有一個地方能退出函數。若在一個函數的代碼中夾雜著return 語句,則就不是單出口。一般而言,單出口的函數比較好維護,比如斷點調試,修改函數的 返回值。不過這只是一個建議,不是規范,實際編程中要記得有這點就可以了。

這個 控件還處理的鼠標移動事件,其代碼為

/// <summary>
/// 處理 鼠標移動事件
/// </summary>
/// <param name="e"> 事件參數</param>
protected override void OnMouseMove(MouseEventArgs e)
{
  base.OnMouseMove (e);
  if( this.Marking && myCurrentLine != null )
  {
    using( System.Drawing.Graphics g = this.CreateGraphics())
    {
      using( System.Drawing.Pen p = new Pen( myCurrentInfo.Color , myCurrentInfo.LineWidth ))
      {
        g.DrawLine( p , LastPoint , new Point( e.X , e.Y ));
      }
    }
    LastPoint = new Point( e.X , e.Y );
    Point point = new Point( e.X - this.AutoScrollPosition.X , e.Y - this.AutoScrollPosition.Y );
     myCurrentLine.Add( point );
  }
}

在這裡若控件處於新增簽 名軌跡的狀態,則將當前鼠標光標位置轉換為視圖坐標後添加到當前軌跡點坐標列表中。這 裡使用了另外一種用戶界面繪制過程。由於鼠標光標事件頻繁發生,一秒內可能發生幾十次 ,每發生一次需要在用戶界面上繪制一小段線條,此時采用髒矩形技術是不合適的。此時我 們可以調用控件的CreateGraphics對象,獲得該控件的圖像繪制對象,這有點類似Win32API 函數GetDC,我們可以直接使用這個圖形繪制對象來繪制用戶界面。這種方式跳過了控件重繪 事件處理機制,速度快,很適合頻繁的繪制用戶界面的操作。

控件還處理鼠標按鍵松 開事件,其代碼為

/// <summary>
/// 處理鼠標按鍵松開事件
/// </summary>
/// <param name="e">事件參數 </param>
protected override void OnMouseUp(MouseEventArgs e)
{
  base.OnMouseUp (e);
  if( this.Marking )
  {
    if( myCurrentLine != null )
    {
      System.Drawing.Rectangle rect = myCurrentLine.Bounds ;
      if( rect.Width > 5 || rect.Height >= 5 )
      {
         myCurrentInfo.Lines.Add( myCurrentLine);
      }
       myCurrentLine = null;
    }
  }
}

在這個代碼中 ,若當前處理的線條軌跡存在而且不是很小,則添加到當前簽名對象中去。此處判斷軌跡邊 界的大小是為了忽略用戶的誤操作,用戶可能不經意的點擊了鼠標按鍵,則程序會生成一個 軌跡信息,若這個軌跡太小,則程序就認為這個軌跡是誤操作,也就忽略掉該軌跡了。

其實在Windows操作系統判斷鼠標雙擊操作也采用類似的方法。用戶連續兩次快速按 下和松開鼠標按鍵,則用戶操作可能是雙擊操作,但也不一定是,此時Windows會判斷兩次鼠 標點擊操作的間隔時間和鼠標光標移動的距離,若間隔時間過長或者鼠標移動的距離過大, 則不是雙擊操作,而是兩個單擊操作,Windows這樣判斷也是為了減少用戶的誤操作。

測試控件

控件編寫好後我們就作了一個frmTest的窗體來測試這個用戶控件。 編譯程序,打開窗體設計器,在工具箱的我的用戶控件頁面中可以看到有一個 PenMakeControl的用戶控件,若沒有則鼠標右擊工具箱,選擇菜單項目“添加/移除項 目”。在對話框中點擊浏覽選擇剛剛編譯生成的EXE或DLL文件,然後選中 PenMarkControl即可在工具箱上新增PenMarkControl項目。我們在窗體上放置一個 PenMarkControl,再放置一些按鈕,添加一些代碼來測試這個控件的各種功能。

提交程序

設置程序的項目類型為類庫,重新編譯,生成一個DLL文件,這個DLL文件就是我們可 以提交給客戶的文件。

小結

在本課程中,我們一起研究了使用C#開發一個具 有一定復雜度的圖像軟件。在這個過程中我們了解了髒矩形技術,初步接觸了文檔對象模型 ,XML序列化。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved