2.2.1 电子海图系统解析及开发 SkiaSharp介绍

虽然S-57海图的解析工作已完成,但数字化的海图数据,并不能给我们带来直观的感觉。离电子海图系统雏形还差最重要的一环:海图显示,这需要使用图形库将电子海图中的基本点、线、面物标绘制出来。关于C#平台下的图形库有很多:

  • GDI+:其主要任务是负责系统与绘图程序之间的信息交换,处理所有Windows程序的图形输出,是.Net平台自带的图形库;
  • SkiaSharp:是一个基于Google的Skia图形库(https://skia.org/)打造的供.Net平台使用的跨平台的2D绘图API类库。它提供一个全面的2D绘图API,能用在移动端、服务端和桌面端呈现图像;
  • OpenTK 是对OpenGL,OpenGL ES和OpenAL的C#封装。 它(https://github.com/opentk/opentk)可以在所有主要平台上运行,并为数百种应用程序,游戏和科学研究提供支持。

电子海图系统软件的好坏主要体现在动态绘制海图的效率上,理论上任何一个图形库都能完成海图显示的工作,在效率没太大差别的前提下,挑选一个能熟练使用的图形库就行。各图形库之间的调用函数、及参数类型都大同小异,因此,在感觉系统显示海图出现卡顿时,可考虑使用一些更偏底级的图形库。本项目最早使用GDI+进行海图绘制,但当海图数据里过大的,性能下降较大,因此切换到SkiaSharp。

在开始海图显示之前,先需要完成一些基础工作。

  1. 在解决方案中,新建一个Windows窗体应用程序,命名为S57Viewer,当窗体命名为EncViewer
  2. 添加项目引用S57Parser
  3. 利用NuGet包控制台运行 Install-Package SkiaSharp.Views.WindowsForms -Version 1.68.3
  4. 窗体添加状态栏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. 屏幕坐标系

屏幕的坐标系原点在屏幕的左上角,水平往右、竖直往下为正。屏幕的坐标横坐标用“x”表示,纵坐标用“y”表示,坐标的单位为像素。坐标(4, 2)用表示当前点在原点右方4个像素处,在原点下方2个像素处,

屏幕坐标系

2. 颜色 SKColor

颜色的构造方法有很多种,但最常见的是利用RGB三原色来构造,此外还可以加入透明度:

    var color = new SKColor(180, 180, 180, 128);    //四个参数表示red, green, blue, alpha

3. 画刷 SKPaint

Skia是用画刷来完成各种绘制工作的。画刷的构造函数并无参数输入,各种参数是以属性的方式传入的。常见的属性有:颜色、字体、填充类型、画笔宽度等。

    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一般从图像从获取。画布绘制通常直接调用其DrawXXX方法,其函数意义及所需参数大都可通过其名称轻易判断。而本项目中海图直接显示窗体中的SKControl控件上,该控件的的PaintSurface事件中存在画布。

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

5. 绘制直线 DrawLine

最简单的绘制函数,输入参数为起点、终点的坐标和画刷。

    canvas.DrawLine(3, 5, 500, 100, paint);   //用paint画直线,起点(3, 5),终点(500, 100)

6. 绘制文本 DrawText

在指定的坐标处,用画笔来绘制指定的文本。指定的坐标可被近似的认为位于文本的左下角

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

7. 绘制矩形 DrawRect

矩形由四个参数来表示:左上角横坐标。左上角纵坐标,矩形宽度,矩形高度。

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

8. 绘制多点 DrawPoints

多个点可以代表孤立的点,可代表线段,也可代表多边形区域。因此,绘制多点时,最重要的是传入多点的绘制模式[SKPointMode],SKPointMode是一个枚举,其中0=点,1=线段,2=多边形。

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

9. 路径及其绘制 SKPath / DrawPath

绘制多点的方式可以绘制多边形区域的,但如果多边形内部存在空洞,绘制多点则无能为力了。而路径功能则强大得多,路径有两个最常用的方法:MoveTo 添加起点LineTo 添加拐点。路径默认的填充方式为Winding,此外还有EvenOddInverseWindingInverseEvenOdd。通过填充方式来判断某一封闭区域是属于整个区域内部还是外部。缠绕算法和奇偶算法都基于从该区域绘制到无限远的假设线来确定是否填充了任何封闭区域。 该线与构成路径的一条或多条边界线交叉。 在缠绕模式下,如果在一个方向上绘制的边界线数量与在另一方向上绘制的边界线数量平衡,则不会填充该区域(外部);否则,该区域将被填充(内部)。 如果边界线的数量为奇数,则奇偶算法将填充一个区域。直观感受为,外圈顺时针将点添加进路径,内圈逆时针将点添加进路径,就可在内部形成一个空洞,这与海图空间记录编码标准一致。

    var path = new SKPath();
    //外圈 顺时针
    path.MoveTo(50, 50);    //起点
    path.LineTo(50, 350);
    path.LineTo(350, 350);
    path.LineTo(350, 50);
    //内圈 逆时针
    path.MoveTo(100, 100);  //起点
    path.LineTo(200, 100);
    path.LineTo(200, 200);
    path.LineTo(100, 200);

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

10. 截图

在Skia中截图非常简单,直接调用SKSurface的Snapshot()方法即可。

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

11. 坐标变换

有时绘制某一物标时,需要缩放一定比例、旋转一定角度或偏移一定的位置,这都涉及到坐标变换。任何平面坐标之间的转换关系可以直接用三维矩阵表示,也可以分步进行。分步变换时,每后一步的变换均在前一步变换基础之上的。

  • 旋转(绕指定中心点旋转) public void RotateDegrees(float degrees, float px, float py);
  • 缩放(绕指定中心点,分横轴与纵轴方向缩放)public void Scale(float sx, float sy, float px, float py);
  • 平移 public void Translate(float dx, float dy);

如对一个路径,分别进行三次变换:

    var path = new SKPath();
    path.MoveTo(50, 50);    //起点
    path.LineTo(50, 150);
    path.LineTo(150, 150);
    path.LineTo(150, 50);
    path.LineTo(50, 50);

    //原图像 默认黑色
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke });

    //绕点(100,100)旋转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,绘制成蓝色
    canvas.Translate(150, 150);
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke, Color = SKColors.Blue });

得到如下效果:

分步坐标变换效果

如图所示,最后一步平移的实际结果,与初步设想(向右下方向平移150像素)不一样,是因为最后的平移需考虑前两步的旋转与缩放变换。

12. 坐标系保存与还原

坐标变换可知,每一步变换都是全局的,都对之后的绘制的坐标系产生影响。当绘制电子海图物标需要执行不同变换时,为避免不同坐标系之间相互干扰,绘制流程一般如下:1. 记住标准坐标系;2. 根据物标需要变换坐标;3. 绘制物标;4. 还原坐标系(执行坐标变换的逆运算)。
而Skia中就提供了当前坐标保存Save()与还原Restore()的方法。

    //原图像 默认黑色
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke });

    canvas.Save();
    //绕点(100,100)旋转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,绘制成蓝色
    canvas.Translate(150, 150);
    canvas.DrawPath(path, new SKPaint() { Style = SKPaintStyle.Stroke, Color = SKColors.Blue });
    canvas.Restore();

得到如下效果:

坐标保存与还原

如图所示,每变换一步之前都执行了坐标保存,变换之后立即执行了坐标还原,因此每一步变换只对当前路径作用一次。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容