C#發現之旅第六講 C#圖形開發中級篇
C#發現之旅第六講 C#圖形開發中級篇
袁永福 2008-5-15
系列課程說明
為了讓大家更深入的了解和使用C#,我們將開始這一系列的主題為“C#發現之旅”的技術講座。考慮到各位大多是進行WEB數據庫開發的,而所謂發現就是發現我們所不熟悉的領域,因此本系列講座內容將是C#在WEB數據庫開發以外的應用。目前規劃的主要內容是圖形開發和XML開發,并計劃編排了多個課程。在未來的C#發現之旅中,我們按照由淺入深,循序漸進的步驟,一起探索和發現C#的其他未知的領域,更深入的理解和掌握使用C#進行軟件開發,拓寬我們的視野,增強我們的軟件開發綜合能力。
本系列課程配套的演示代碼下載地址為 https://files.cnblogs.com/xdesigner/cs_discovery.zip 。其中的CellViewLib.zip 就是本課程的演示代碼。
本系列課程已發布的文章有
C#發現之旅第一講 C#-XML開發
C#發現之旅第二講 C#-XSLT開發
C#發現之旅第三講 使用C#開發基于XSLT的代碼生成器
C#發現之旅第四講 Windows圖形開發入門
C#發現之旅第五講 圖形開發基礎篇
C#發現之旅第六講 C#圖形開發中級篇
C#發現之旅第七講 C#圖形開發高級篇
C#發現之旅第八講 ASP.NET圖形開發帶超鏈接的餅圖
C#發現之旅第九講 ASP.NET驗證碼技術
C#發現之旅第十講 文檔對象模型
課程說明
在上一次課程中,我們一起研究了使用C#開發一個比較簡單的橢圓形按鈕的控件,初步接觸了C#圖形開發,在本次課程中我們將繼續深入研究C#圖形開發,嘗試使用C#開發一個稍微復雜點的數據網格控件。
功能需求
現客戶要求開發一個圖形軟件,其軟件功能需求是
- 用一個網格式界面顯示一個數據表的文本內容。
- 可以設置網格行的高度,單元格的寬度自動適應文本內容的大小。當顯示的內容比較多時顯示滾動條。
- 用戶可以使用鼠標點擊操作來選擇一個單元格,也可以鼠標拖拽選擇多個單元格。
- 可以復制選擇的單元格的文本。
最后開發的軟件其用戶界面如圖所示

軟件設計
根據軟件功能需求,其界面類似DataGrid控件,這是一種界面比較復雜的圖形軟件組件,需要采用文檔-視圖的軟件設計模式。
所謂文檔-視圖模式,就是將用戶界面中要顯示的數據并不直接放置到控件代碼中,而是按照邏輯層次關系建立一種文檔對象模型,使用一個個對象來影射到要顯示的數據中的某些部分。此時控件根據這個文檔對象模型來顯示用戶界面,并響應用戶界面事件來操作一個個文檔對象。這種軟件設計模式通常用于比較復雜的圖形用戶界面軟件的設計中。
文檔對象模型-單元格,表格行,表格
我個人的軟件開發風格是從底層到界面,當然也有人是喜歡從界面到底層的。要開發這個軟件,首先是設計文檔模型,此處文檔結構比較簡單,就是一個二維表格,因此很自然的設計出表格式的文檔對象模型,無非就是單元格,表格行和表格三種對象,這三種對象構成一個具有三個層次的對象樹狀結構,這有點類型HTML文檔中的表格對象結構。此處為了簡化設計就沒有定義表格列對象了。
視圖控件
本軟件的關鍵是建立一個自定義用戶控件,它能根據表格文檔對象模型來繪制網格,并實現一些操作特性。該控件開發主要實現的功能有
數據的加載,可以將一個二維表格數據結構設置到表格文檔對象模型中供控件顯示。此處測試時使用了一個DataTable對象填充表格文檔對象。為了開發方便,事先查詢數據庫獲得一個DataTable并序列化到一個文件中。測試程序將從這個序列化文件獲得一個DataTable然后填充到控件中。
控件成功加載數據后,還需要進行內容排版,計算表格中每一個單元格的位置和大小。由于客戶要求單元格的寬度自動適應文本內容的寬度,因此首先計算所有單元格文本的顯示所需的寬度,然后獲得某個表格列所有單元格的最大寬度,然后設置該網格列的寬度為這個最大寬度。網格內容排版后還要獲得整個網格的顯示大小,并根據需要設置控件的滾動條狀態。
我們可以使用Graphics對象的MeasureString 方法來計算字符串的顯示寬度。若文檔視圖比較大時用戶控件還要顯示滾動條來滾動顯示整個文檔視圖。
控件還需要鼠標點擊選擇一個單元格,或者鼠標拖拽選擇多個單元格,因此還需要處理鼠標事件。當用戶按下鼠標按鍵時,設置鼠標光標下面的單位格為選中狀態,當用戶鼠標拖拽時,將動態的形成一個從拖拽起點到當前鼠標光標位置的選擇區域矩形,若單元格和選擇矩形相交,則設置單元格為選中狀態。
軟件代碼說明
根據軟件設計,開發出了這個網格數據顯示軟件,現按照軟件的運行過程對代碼進行詳細說明。
使用VS.NET2003打開這個工程,按下F5運行,可以看到主窗體上有一個“顯示數據”按鈕,按下該按鈕,然后進行單步跟蹤狀態,可以看到程序首先從程序資源文件 DataTable.dat 中反序列化加載一個DataTable對象,這個DataTable.dat 文件是事先生成的,這樣做是為了簡化這個軟件的代碼,我們也可以連接數據庫查詢數據獲得一個DataTable對象,其效果是一樣的。
FillDataTable
獲得DataTable后程序調用控件的FillDataTable方法向控件填充數據。這個方法的代碼如下
| /// <summary> /// 根據一個DataTable 填充網格 /// </summary> /// <param name="table">數據表對象</param> public void FillDataTable( DataTable table ) { if( table == null ) throw new ArgumentNullException("table"); myDocument.Clear(); CellRow row = new CellRow(); myDocument.Add( row ); foreach( DataColumn col in table.Columns ) { row.Add( col.ColumnName ); } foreach( DataRow drow in table.Rows ) { row = new CellRow(); foreach( DataColumn col in table.Columns ) { object v = drow[ col ] ; string txt = ""; if( v == null || DBNull.Value.Equals( v )) txt = "[NULL]"; else txt = Convert.ToString( v ); row.Add( txt ); } myDocument.Add( row ); } using( System.Drawing.Graphics g = this.CreateGraphics()) { this.RefreshSize( g ); this.Invalidate(); } } |
進入FillDataTable方法可以看到程序是根據DataTable填充網格文檔對象 myDocument 。程序中實現了由Cell , CellRow 和 CellDocument 三種類型組成的網格文檔對象。
大家可以看看這三個類的代碼,它們是相當簡單的。Cell 類定義了網格單元格對象,包括單元格顯示的文本,位置和大小等信息。CellRow類定義了表格行對象,它本身也是單元格列表,可以添加單元格對象。CellDocument定義了表格文檔對象,它本身是表格行列表,可以添加表格行,還提供Cells 屬性返回文檔中所有的單元格對象組成的數組。
我們回到FillDataTable 函數,首先是清空文檔,然后遍歷DataTable的標題欄信息,生成網格文檔的第一行單元格,然后遍歷DataTable所有的數據行對象,對每一個數據行新增一個表格行對象,然后添加到 myDocument 中。
RefreshSize
程序使用一個DataTable填充網格文檔后,需要調用 RefreshSize 進行內容事先排版,為顯示文檔內容做準備。這個方法的代碼為
| /// <summary> /// 計算單元格大小,進行內容排版 /// </summary> /// <param name="g">計算文本大小使用的圖形繪制對象</param> public void RefreshSize( System.Drawing.Graphics g ) { ArrayList cells = new ArrayList(); System.Drawing.Size ViewSize = Size.Empty ; int LeftCount = 0 ; for( int iCount = 0 ; iCount < 1000 ; iCount ++ ) { // 遍歷所有的表格列,獲得指定的列的單元格對象 // 此處允許最大的表格列有1000列 cells.Clear(); for( int RowIndex = 0 ; RowIndex < myDocument.Count ; RowIndex ++ ) { CellRow row = myDocument[ RowIndex ] ; if( iCount < row.Count ) { Cell cell = row[ iCount ] ; // 設置單元格的位置 cell.intLeft = LeftCount ; cell.intTop = RowIndex * this.RowHeight ; cells.Add( cell ); } } if( cells.Count == 0 ) break; // 計算當前列的單元格的最大寬度 int MaxWidth = 40 ; foreach( Cell cell in cells ) { string txt = cell.Text ; if( txt != null && txt.Length > 0 ) { System.Drawing.SizeF size = g.MeasureString( txt , this.Font , 1000 , System.Drawing.StringFormat.GenericDefault ); if( MaxWidth < ( int ) size.Width ) MaxWidth = ( int ) size.Width ; } } MaxWidth += 10 ; // 設置單元格的大小 foreach( Cell cell in cells ) { cell.intWidth = MaxWidth ; cell.intHeight = this.RowHeight ; if( cell.Left + cell.Width > ViewSize.Width ) ViewSize.Width = cell.Left + cell.Width ; if( cell.Top + cell.Height > ViewSize.Height ) ViewSize.Height = cell.Top + cell.Height ; } LeftCount += MaxWidth ; } ViewSize.Width += 10 ; ViewSize.Height += 10 ; if( this.AutoScrollMinSize.Equals( ViewSize ) == false ) { this.AutoScrollMinSize = ViewSize ; this.Invalidate(); } }//public void RefreshSize( System.Drawing.Graphics g ) |
由于其中要計算單元格文本的顯示寬度,需要使用Graphics對象,因此這里使用用戶控件的 CreateGraphics 方法獲得一個 Graphics對象。Graphics對象不能使用new 語句直接實例化,必須使用某個控件的CreateGraphics方法或從一個圖片中創建Graphics 對象。現在我們隨著代碼的流程進入到RefreshSize 函數。
在這里我們定義了一個LeftCount變量,該變量保存了當前表格列的左邊緣位置。定義了ViewSize變量,用于保存整個文檔的顯示大小。首先我們需要計算各個表格列的寬度,由于我們沒有定義表格列對象,因此采用遍歷的手段來獲得所有屬于指定列號的單元格對象。并設置這些單元格的頂端位置和左端位置。
然后遍歷所有同一列的單元格,計算它們的文本顯示寬度,并獲得其最大值。則該最大值就是當前表格列的寬度,然后設置這些單元格的寬度為列寬。并修正整個文檔的顯示大小。
處理了所有的單元格后,文檔視圖排版完畢,可以顯示了。程序還計算了整個文檔視圖的大小,并根據需要設置控件的 AutoScrollMinSize 屬性用來設置滾動狀態。
UserControl支持自動設置滾動狀態。當設置用戶控件的AutoScroll屬性時,就啟用自動滾動設置。此時我們可以設置AutoScrollMinSize屬性來控制滾動狀態,當用戶控件的客戶區ClientSize的寬度或高度小于這個值時就會自動顯示橫向或縱向滾動條,若客戶區大小足夠容納這個AutoScrollMinSize時,就不會顯示滾動條,當用戶控件大小改變時會自動進行這樣的判斷。在此我們設置AutoScrollMinSize為文檔視圖的大小,因此程序也就自動維護滾動狀態。
當程序完成文檔內容排版后,我們就調用Invalidate函數來通知系統重新繪制控件的用戶界面。
OnPaint
數據加載了,文檔視圖也完成的排版,接下來就是繪制用戶界面了,我們就很自然的重寫控件的OnPaint函數來繪制網格了。這個方法代碼為
| /// <summary> /// 繪制控件內容 /// </summary> /// <param name="e">繪制圖形參數</param> protected override void OnPaint(PaintEventArgs e) { base.OnPaint (e); System.Drawing.Rectangle ClipRect = e.ClipRectangle ; ClipRect.Offset( - this.AutoScrollPosition.X , - this.AutoScrollPosition.Y ); // 進行坐標轉換 e.Graphics.TranslateTransform( this.AutoScrollPosition.X , this.AutoScrollPosition.Y ); // 繪制網格的畫筆對象 Pen GridPen = null; if( intGridColor.A != 0 ) { GridPen = new Pen( intGridColor ); } // 填充網格的畫刷對象 SolidBrush GridBrush = null; if( intGridBackColor.A != 0 ) { GridBrush = new SolidBrush( intGridBackColor ); } // 繪制文本的畫刷對象 SolidBrush TextBrush = new SolidBrush( this.ForeColor ); // 輸出文本使用的格式化對象 StringFormat TextFormat = new StringFormat(); TextFormat.Alignment = System.Drawing.StringAlignment.Near ; TextFormat.LineAlignment = System.Drawing.StringAlignment.Center ; TextFormat.FormatFlags = System.Drawing.StringFormatFlags.NoWrap ; try { foreach( CellRow row in myDocument ) { foreach( Cell cell in row ) { // 遍歷所有表格行的單元格對象,對單元格進行逐個繪制 Rectangle bounds = cell.Bounds ; // 若單元格和剪切矩形不相交,則單元格無需繪制,轉而處理下一個單元格. if( ClipRect.IntersectsWith( bounds ) == false ) { continue ; } if( cell.Selected ) { // 若單元格處于選擇狀態則顯示高亮度背景色 e.Graphics.FillRectangle( SystemBrushes.Highlight , bounds ); } else { // 繪制單元格背景 if( GridBrush != null ) { e.Graphics.FillRectangle( GridBrush , bounds ); } } if( GridPen != null ) { // 繪制單元格邊框 e.Graphics.DrawRectangle( GridPen , bounds ); } if( cell.Text != null ) { // 繪制單元格文本 e.Graphics.DrawString( cell.Text , this.Font , cell.Selected ? SystemBrushes.HighlightText : TextBrush , new RectangleF( cell.Left , cell.Top , cell.Width , cell.Height ) , TextFormat ); } } } } finally { if( GridPen != null ) GridPen.Dispose(); if( GridBrush != null ) GridBrush.Dispose(); TextBrush.Dispose(); TextFormat.Dispose(); } } |
由于我們已經計算了所有單元格的位置和大小,因此繪制網格的過程不復雜,就是遍歷所有的單元格,繪制一個矩形邊框和單元格文本而已。
由于這個用戶界面是可能發生滾動的,形成一種折射效應,繪制時需要進行坐標轉換,這就增加了一些復雜度。程序首先獲得剪切矩形ClipRect,并進行移位,然后設置圖形繪制對象e.Graphics進行坐標轉換,然后遍歷所有的單元格對象,針對每一個單元格,若剪切矩形和單元格的邊框相交,則繪制單元格,否則不繪制該單元格。
單元格對象有一個 Selected 屬性,表示單元格是否處于選擇狀態,若單元格處于選擇狀態則使用高亮度背景畫刷 SystemBrushes.Highlight繪制單元格背景,否則使用控件的網格背景色繪制單元格背景。然后使用控件的網格線顏色來繪制單元格的邊框。繪制控件的邊框后使用Graphics的DrawString成員來繪制單元格文本,而且當單元格處于選擇狀態則使用高系統定義的高亮度文本顏色,否則使用控件文本顏色。
這里創建繪制網格的畫筆對象和繪制背景的畫刷對象時進行了一些判斷,若顏色值的屬性A為0則不創建對象。在C#中使用類型System.Drawing.Color來表示顏色值,它有4個屬性來表示顏色特性,也就是A,R,G,B四個屬性值,其中屬性R,G,B分別表示顏色的紅綠藍的顏色分量,而屬性A表示顏色透明度,若A等于255則表示純色,不透明,若等于0則表示完全透明,此時繪制圖形也就無意義了,若A的值在1到254之間則表示半透明,這個值越小,顏色就越透明。
鼠標事件處理
用于客戶要求能用鼠標點擊或拖拽操作來選擇單元格,因此我們需要處理控件的鼠標事件來實現選擇單元格效果。其代碼為
| /// <summary> /// 上一次鼠標按鍵按下時鼠標光標位置 /// </summary> private Point LastMousePosition = new Point( -1 , -1); /// <summary> /// 處理鼠標按鍵按下事件 /// </summary> /// <param name="e"></param> protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown (e); // 鼠標光標位置坐標轉換 Point p = new Point( e.X , e.Y ); p.Offset( - this.AutoScrollPosition.X , - this.AutoScrollPosition.Y ); LastMousePosition = p ; Cell[] cells = myDocument.Cells ; foreach( Cell cell in cells ) { Rectangle bounds = cell.Bounds ; bool select = bounds.Contains( p ); if( cell.Selected != select ) { InvalidateCell( cell ); cell.Selected = select ; } } } /// <summary> /// 處理鼠標移動事件 /// </summary> /// <param name="e"></param> protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove (e); if( LastMousePosition.X >= 0 ) { // 鼠標光標位置坐標轉換 Point p = new Point( e.X , e.Y ); p.Offset( - this.AutoScrollPosition.X , - this.AutoScrollPosition.Y ); // 根據 p 和 LastMousePosition 兩點坐標獲得一個矩形選擇區域 Rectangle SelectRect = Rectangle.Empty ; if( p.X > LastMousePosition.X ) { SelectRect.X = LastMousePosition.X ; SelectRect.Width = p.X - LastMousePosition.X ; } else { SelectRect.X = p.X ; SelectRect.Width = LastMousePosition.X - p.X ; } if( p.Y > LastMousePosition.Y ) { SelectRect.Y = LastMousePosition.Y ; SelectRect.Height = p.Y - LastMousePosition.Y ; } else { SelectRect.Y = p.Y ; SelectRect.Height = LastMousePosition.Y - p.Y ; } foreach( Cell cell in myDocument.Cells ) { bool flag = SelectRect.IntersectsWith( cell.Bounds ); if( cell.Selected != flag ) { cell.Selected = flag ; this.InvalidateCell( cell ); } } } } /// <summary> /// 處理鼠標按鍵松開事件 /// </summary> /// <param name="e"></param> protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp (e); LastMousePosition = new Point( -1 , -1 ); } |
首先重寫OnMouseDown方法,由于剛剛說到的折射效果,需要將鼠標光標位置從控件客戶區坐標轉換為文檔視圖坐標。然后設置LastMousePosition變量,該變量服務于鼠標拖拽操作。遍歷表格文檔的所有的單元格對象,判斷單元格的邊框是否包含鼠標光標所在位置,然后根據需要設置單元格的選擇狀態,若單元格的選擇狀態發生改變,則調用InvalidateCell方法聲明該單元格樣式無效,準備重新繪制該單元格。
然后重寫OnMouseMove方法,首先進行鼠標光標位置坐標轉換,若LastMousePosition有效,則說明用戶正在拖拽鼠標,然后根據LastMousePosition坐標和當前鼠標光標坐標獲得一個選擇區域矩形,然后遍歷所有單元格,判斷選擇矩形和單元格邊框是否相交,并設置單元格的選擇狀態,若單元格的選擇狀態發生改變則聲明該單元格樣式無效,準備重新繪制界面。
重寫OnMouseUp方法,設置LastMousePosition變量無效,結束鼠標拖拽操作。
復制數據
程序主窗體上有一個復制按鈕,按下該按鈕可以進入到控件的Copy方法。代碼為
| /// <summary> /// 復制選擇的單元格的文本 /// </summary> public void Copy() { System.Text.StringBuilder myStr = new System.Text.StringBuilder(); foreach( CellRow row in myDocument ) { bool find = false; foreach( Cell cell in row ) { if( cell.Selected ) { myStr.Append( cell.Text ); myStr.Append( " " ); find = true ; } } if( find ) myStr.Append( System.Environment.NewLine ); } if( myStr.Length > 0 ) { System.Windows.Forms.DataObject data = new DataObject(); data.SetData( myStr.ToString()); System.Windows.Forms.Clipboard.SetDataObject( data , true ); } } |
該方法也比較簡單,遍歷所有的單元格,若該單元格處于選擇狀態,則獲得它的文本,然后將所有選擇單元格的文本拼湊起來,并設置到Windows系統剪切板中。
大量的程序使用Windows剪切板交流數據。在.NET中操作剪切板是比較方便的,當我們進行復制操作時,首先是創建一個DataObject對象,使用它的SetData方法設置數據,然后使用ClipBoard的SetDataObject方法來向Windows剪切板設置數據。我們可以同時向DataObject設置多種格式的數據,比如可以同時設置純文本數據,RTF文檔或圖片數據,其他應用程序會檢索剪切板中的數據格式,從而利用可處理的數據。
當我們進行粘貼操作時,使用System.Windows.Forms.ClipBoard的GetDataObject方法獲得一個System.Windows.Forms.IDataObject對象,然后使用IDataObject的GetFormats方法檢索可用的數據格式,類型System.Windows.Forms.DataFormats的靜態字段預定義了一些數據格式的名稱,然后可以使用IDataObject的GetData 方法獲得指定格式的數據,如此可以根據獲得的數據繼續進行操作。
系統預定義顏色
類型SystemBrushes定義了一些系統顏色的畫刷對象,系統顏色是指Windows操作系統預先定義的標準顏色,包括桌面背景色,窗體顏色,菜單控件文本顏色,3D邊框中的亮邊框顏色,暗邊框顏色,提示文本顏色和背景色,高亮度選擇狀態的文本顏色和背景色等等。打開操作系統桌面屬性,可以進入這些系統顏色定義對話框,該對話框樣式如圖所示。

在進行圖形開發時有時候需要使用這種系統預定義顏色,這樣使得應用系統的顏色風格和Windows操作系統的整體風格保持一致,這樣可以獲得和操作系統一致的用戶體驗。
在.NET中,類型SystemBrushes的一些靜態屬性提供了具有這種系統預定義顏色的畫刷對象,類似的SystemPens的靜態屬性提供了具有系統預定義顏色的畫筆對象,而SystemColors則提供了這些預定義顏色值。
折射效應
由于該控件可能存在滾動,這就造成一種折射效應。這加大了程序的復雜度。
在空氣中,光線是直線傳播的,因此手迎著光線直線移動必然能接觸到物體。但若一個物體在水中,由于折射作用,手迎著光線直線移動也不一定能接觸到物體,因此人的動作要根據折射的因素進行修正,才能準確的抓住物體。
當用戶界面發生滾動時也會有類似的折射效應。控件客戶區中顯示了一個圖形,由于發生了滾動,則該圖形在文檔視圖中的位置不等于在控件客戶區中的位置,兩者存在一個偏移量,這個偏移量就是控件的滾動量。
在繪圖圖形時,需要將圖形在文檔視圖中的坐標轉換為控件客戶區中的坐標來模擬這種折射效果,在OnPaint方法的開頭,就調用Graphics的TranslateTrnasform方法進行坐標轉換,這樣就整體實現了文檔視圖坐標向控件客戶區坐標的轉換。
在本控件的處理鼠標事件時,需要判斷鼠標光標下的單元格對象,事件參數提供的鼠標光標坐標是在控件客戶區中的坐標,若直接根據這個控件客戶區坐標位置查找單元格對象,當控件發生滾動時,這樣的操作過程是錯誤的。因此需要將鼠標的控件客戶區坐標轉換為視圖坐標,轉換后再搜索單元格才是正確的。
控件中定義了一個InvalidateCell方法,參數是Cell類型,該方法的功能是聲明某個單元格樣式無效,需要重新繪制。由于聲明控件部分界面無效的方法Invalidate的參數是采用控件客戶區坐標的,而單元格位置是采用文檔視圖坐標的,因此需要進行坐標轉換。
折射效果在圖形開發中是會經常遇到的,此處的折射效果是比較簡單的,只是簡單的整體移位。在一些復雜的圖形用戶界面中還可能發生圖形的縮放和旋轉,文檔視圖的不同的部分發生了不同的折射效應,此時程序處理折射效應是比較復雜的。

完成開發
為了開發方便,我們設置該程序為WinForm應用程序模式,編譯生成一個EXE文件,我們可以修改工程類型為類庫,編譯生成一個DLL文件,我們就可以把這個DLL提交給客戶使用了。
小結
在本課程中,我們一起研究了一個稍微復雜的C#開發的圖形軟件,相對于上一個演示軟件,這個軟件展示了更多的C#圖形編程技術,包括圖形文檔的排版,使用剪切矩形優化圖形繪制,理解了用戶界面的折射效應。相信大家認真學習后能身體力行,開始能編寫一些自己的圖形軟件了。
在下一個課程中,我們將探索更為復雜的C#圖形開發,開始學習高級圖形軟件所用到的一些開發技術。使得大家能在C#圖形開發的世界中更自在的探索研究。
posted on 2008-05-28 09:07 袁永福 電子病歷,醫療信息化 閱讀(10779) 評論(17) 收藏 舉報
浙公網安備 33010602011771號