2.2.1 電子海圖系統(tǒng)解析及開發(fā) SkiaSharp介紹

雖然S-57海圖的解析工作已完成,但數(shù)字化的海圖數(shù)據(jù),并不能給我們帶來直觀的感覺。離電子海圖系統(tǒng)雛形還差最重要的一環(huán):海圖顯示,這需要使用圖形庫將電子海圖中的基本點(diǎn)、線、面物標(biāo)繪制出來。關(guān)于C#平臺下的圖形庫有很多:

  • GDI+:其主要任務(wù)是負(fù)責(zé)系統(tǒng)與繪圖程序之間的信息交換,處理所有Windows程序的圖形輸出,是.Net平臺自帶的圖形庫;
  • SkiaSharp:是一個(gè)基于Google的Skia圖形庫(https://skia.org/)打造的供.Net平臺使用的跨平臺的2D繪圖API類庫。它提供一個(gè)全面的2D繪圖API,能用在移動端、服務(wù)端和桌面端呈現(xiàn)圖像;
  • OpenTK 是對OpenGL,OpenGL ES和OpenAL的C#封裝。 它(https://github.com/opentk/opentk)可以在所有主要平臺上運(yùn)行,并為數(shù)百種應(yīng)用程序,游戲和科學(xué)研究提供支持。

電子海圖系統(tǒng)軟件的好壞主要體現(xiàn)在動態(tài)繪制海圖的效率上,理論上任何一個(gè)圖形庫都能完成海圖顯示的工作,在效率沒太大差別的前提下,挑選一個(gè)能熟練使用的圖形庫就行。各圖形庫之間的調(diào)用函數(shù)、及參數(shù)類型都大同小異,因此,在感覺系統(tǒng)顯示海圖出現(xiàn)卡頓時(shí),可考慮使用一些更偏底級的圖形庫。本項(xiàng)目最早使用GDI+進(jìn)行海圖繪制,但當(dāng)海圖數(shù)據(jù)里過大的,性能下降較大,因此切換到SkiaSharp。

在開始海圖顯示之前,先需要完成一些基礎(chǔ)工作。

  1. 在解決方案中,新建一個(gè)Windows窗體應(yīng)用程序,命名為S57Viewer,當(dāng)窗體命名為EncViewer;
  2. 添加項(xiàng)目引用S57Parser;
  3. 利用NuGet包控制臺運(yùn)行 Install-Package SkiaSharp.Views.WindowsForms -Version 1.68.3;
  4. 窗體添加狀態(tài)欄StatusStrip用于輔助信息的顯示;
  5. 添加如下代碼:
     public partial class EncViewer : Form
     {
         private SKControl skiaView;
    
         public EncViewer()
         {
             InitializeComponent();
    
             //開戶雙緩沖
             this.SetStyle(
                 ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer |
                 ControlStyles.ResizeRedraw, true);
             this.SetStyle(ControlStyles.StandardDoubleClick, false);
         }
    
         private void EncViewer_Load(object sender, EventArgs e)
         {
             skiaView = new SKControl { Dock = DockStyle.Fill };
             this.Controls.Add(skiaView);
             skiaView.PaintSurface += this.skiaView_PaintSurface;
         }
    
         private void skiaView_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
         {
             //畫布
             var canvas = e.Surface.Canvas;
             ClearCanvas(canvas);
    
             //添加繪圖代碼
         }
    
         private void ClearCanvas(SKCanvas ca)
         {
             ca.Clear(new SKColor(150, 150, 150));
         }
     }
    

SkiaSharp常用操作

1. 屏幕坐標(biāo)系

屏幕的坐標(biāo)系原點(diǎn)在屏幕的左上角,水平往右、豎直往下為正。屏幕的坐標(biāo)橫坐標(biāo)用“x”表示,縱坐標(biāo)用“y”表示,坐標(biāo)的單位為像素。坐標(biāo)(4, 2)用表示當(dāng)前點(diǎn)在原點(diǎn)右方4個(gè)像素處,在原點(diǎn)下方2個(gè)像素處,

屏幕坐標(biāo)系

2. 顏色 SKColor

顏色的構(gòu)造方法有很多種,但最常見的是利用RGB三原色來構(gòu)造,此外還可以加入透明度:

    var color = new SKColor(180, 180, 180, 128);    //四個(gè)參數(shù)表示red, green, blue, alpha

3. 畫刷 SKPaint

Skia是用畫刷來完成各種繪制工作的。畫刷的構(gòu)造函數(shù)并無參數(shù)輸入,各種參數(shù)是以屬性的方式傳入的。常見的屬性有:顏色、字體、填充類型、畫筆寬度等。

    var paint = new SKPaint()
    {
        Color = new SKColor(180, 180, 180, 128), //顏色
        StrokeWidth = 2, //畫筆寬度
        Typeface = SKTypeface.FromFamilyName("宋體", SKFontStyle.Normal), //字體
        TextSize = 32,  //字體大小
        Style = SKPaintStyle.Stroke, //類型:填充 或 畫邊界 或全部
        PathEffect = SKPathEffect.CreateDash(LongDash, 0),   //繪制虛線
    };

4. 畫布 SKCanvas

Skia所有的繪制是基于畫布的。畫布來自于SKSurface,SKSurface一般從圖像從獲取。畫布繪制通常直接調(diào)用其DrawXXX方法,其函數(shù)意義及所需參數(shù)大都可通過其名稱輕易判斷。而本項(xiàng)目中海圖直接顯示窗體中的SKControl控件上,該控件的的PaintSurface事件中存在畫布。

    SKImageInfo imageInfo = new SKImageInfo(300, 250);
    using (SKSurface surface = SKSurface.Create(imageInfo))
    {
        SKCanvas canvas = surface.Canvas;
        canvas.DrawColor(SKColors.Red);  //填充顏色
    }

5. 繪制直線 DrawLine

最簡單的繪制函數(shù),輸入?yún)?shù)為起點(diǎn)、終點(diǎn)的坐標(biāo)和畫刷。

    canvas.DrawLine(3, 5, 500, 100, paint);   //用paint畫直線,起點(diǎn)(3, 5),終點(diǎn)(500, 100)

6. 繪制文本 DrawText

在指定的坐標(biāo)處,用畫筆來繪制指定的文本。指定的坐標(biāo)可被近似的認(rèn)為位于文本的左下角。

    canvas.DrawText("文本", 50, 50, paint);

7. 繪制矩形 DrawRect

矩形由四個(gè)參數(shù)來表示:左上角橫坐標(biāo)。左上角縱坐標(biāo),矩形寬度,矩形高度。

    canvas.DrawRect(10, 10, 100, 100, paint);

8. 繪制多點(diǎn) DrawPoints

多個(gè)點(diǎn)可以代表孤立的點(diǎn),可代表線段,也可代表多邊形區(qū)域。因此,繪制多點(diǎn)時(shí),最重要的是傳入多點(diǎn)的繪制模式[SKPointMode],SKPointMode是一個(gè)枚舉,其中0=點(diǎn),1=線段,2=多邊形。

    public void DrawPoints(SKPointMode mode, SKPoint[] points, SKPaint paint);

9. 路徑及其繪制 SKPath / DrawPath

繪制多點(diǎn)的方式可以繪制多邊形區(qū)域的,但如果多邊形內(nèi)部存在空洞,繪制多點(diǎn)則無能為力了。而路徑功能則強(qiáng)大得多,路徑有兩個(gè)最常用的方法:MoveTo 添加起點(diǎn)LineTo 添加拐點(diǎn)。路徑默認(rèn)的填充方式為Winding,此外還有EvenOdd、InverseWindingInverseEvenOdd。通過填充方式來判斷某一封閉區(qū)域是屬于整個(gè)區(qū)域內(nèi)部還是外部。纏繞算法和奇偶算法都基于從該區(qū)域繪制到無限遠(yuǎn)的假設(shè)線來確定是否填充了任何封閉區(qū)域。 該線與構(gòu)成路徑的一條或多條邊界線交叉。 在纏繞模式下,如果在一個(gè)方向上繪制的邊界線數(shù)量與在另一方向上繪制的邊界線數(shù)量平衡,則不會填充該區(qū)域(外部);否則,該區(qū)域?qū)⒈惶畛洌▋?nèi)部)。 如果邊界線的數(shù)量為奇數(shù),則奇偶算法將填充一個(gè)區(qū)域。直觀感受為,外圈順時(shí)針將點(diǎn)添加進(jìn)路徑,內(nèi)圈逆時(shí)針將點(diǎn)添加進(jìn)路徑,就可在內(nèi)部形成一個(gè)空洞,這與海圖空間記錄編碼標(biāo)準(zhǔn)一致。

    var path = new SKPath();
    //外圈 順時(shí)針
    path.MoveTo(50, 50);    //起點(diǎn)
    path.LineTo(50, 350);
    path.LineTo(350, 350);
    path.LineTo(350, 50);
    //內(nèi)圈 逆時(shí)針
    path.MoveTo(100, 100);  //起點(diǎn)
    path.LineTo(200, 100);
    path.LineTo(200, 200);
    path.LineTo(100, 200);

    //繪制路徑
    canvas.DrawPath(path, new SKPaint());
繪制中空的路徑

10. 截圖

在Skia中截圖非常簡單,直接調(diào)用SKSurface的Snapshot()方法即可。

    using (SKImage image = e.Surface.Snapshot())
    using (SKData data = image.Encode(SKEncodedImageFormat.Png, 100))  //指定圖片格式及質(zhì)量
    using (var mStream = new MemoryStream(data.ToArray()))
    {
        Bitmap bm = new Bitmap(mStream, false);
        pictureBox1.Image = bm;
    }

11. 坐標(biāo)變換

有時(shí)繪制某一物標(biāo)時(shí),需要縮放一定比例、旋轉(zhuǎn)一定角度或偏移一定的位置,這都涉及到坐標(biāo)變換。任何平面坐標(biāo)之間的轉(zhuǎn)換關(guān)系可以直接用三維矩陣表示,也可以分步進(jìn)行。分步變換時(shí),每后一步的變換均在前一步變換基礎(chǔ)之上的。

  • 旋轉(zhuǎn)(繞指定中心點(diǎn)旋轉(zhuǎn)) public void RotateDegrees(float degrees, float px, float py);
  • 縮放(繞指定中心點(diǎn),分橫軸與縱軸方向縮放)public void Scale(float sx, float sy, float px, float py);
  • 平移 public void Translate(float dx, float dy);

如對一個(gè)路徑,分別進(jìn)行三次變換:

    var path = new SKPath();
    path.MoveTo(50, 50);    //起點(diǎn)
    path.LineTo(50, 150);
    path.LineTo(150, 150);
    path.LineTo(150, 50);
    path.LineTo(50, 50);

    //原圖像 默認(rèn)黑色
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke });

    //繞點(diǎn)(100,100)旋轉(zhuǎn)45度,繪制成紅色
    canvas.RotateDegrees(45, 100, 100);
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke, Color = SKColors.Red });

    //縮放 橫軸與縱軸方向縮小一倍,縮放中心為(100, 100), 繪制成綠色
    canvas.Scale(0.5f, 0.5f, 100, 100);
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke, Color = SKColors.Green });

    //平移 向右平移150,向下平移150,繪制成藍(lán)色
    canvas.Translate(150, 150);
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke, Color = SKColors.Blue });

得到如下效果:

分步坐標(biāo)變換效果

如圖所示,最后一步平移的實(shí)際結(jié)果,與初步設(shè)想(向右下方向平移150像素)不一樣,是因?yàn)樽詈蟮钠揭菩杩紤]前兩步的旋轉(zhuǎn)與縮放變換。

12. 坐標(biāo)系保存與還原

坐標(biāo)變換可知,每一步變換都是全局的,都對之后的繪制的坐標(biāo)系產(chǎn)生影響。當(dāng)繪制電子海圖物標(biāo)需要執(zhí)行不同變換時(shí),為避免不同坐標(biāo)系之間相互干擾,繪制流程一般如下:1. 記住標(biāo)準(zhǔn)坐標(biāo)系;2. 根據(jù)物標(biāo)需要變換坐標(biāo);3. 繪制物標(biāo);4. 還原坐標(biāo)系(執(zhí)行坐標(biāo)變換的逆運(yùn)算)。
而Skia中就提供了當(dāng)前坐標(biāo)保存Save()與還原Restore()的方法。

    //原圖像 默認(rèn)黑色
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke });

    canvas.Save();
    //繞點(diǎn)(100,100)旋轉(zhuǎn)45度,繪制成紅色
    canvas.RotateDegrees(45, 100, 100);
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke, Color = SKColors.Red });
    canvas.Restore();

    canvas.Save();
    //縮放 橫軸與縱軸方向縮小一倍,縮放中心為(100, 100), 繪制成綠色
    canvas.Scale(0.5f, 0.5f, 100, 100);
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke, Color = SKColors.Green });
    canvas.Restore();

    canvas.Save();
    //平移 向右平移150,向下平移150,繪制成藍(lán)色
    canvas.Translate(150, 150);
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke, Color = SKColors.Blue });
    canvas.Restore();

得到如下效果:

坐標(biāo)保存與還原

如圖所示,每變換一步之前都執(zhí)行了坐標(biāo)保存,變換之后立即執(zhí)行了坐標(biāo)還原,因此每一步變換只對當(dāng)前路徑作用一次。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容