虽然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。
在开始海图显示之前,先需要完成一些基础工作。
- 在解决方案中,新建一个Windows窗体应用程序,命名为
S57Viewer
,当窗体命名为EncViewer
; - 添加项目引用
S57Parser
; - 利用NuGet包控制台运行
Install-Package SkiaSharp.Views.WindowsForms -Version 1.68.3
; - 窗体添加状态栏
StatusStrip
用于辅助信息的显示; - 添加如下代码:
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
,此外还有EvenOdd
、InverseWinding
、InverseEvenOdd
。通过填充方式来判断某一封闭区域是属于整个区域内部还是外部。缠绕算法和奇偶算法都基于从该区域绘制到无限远的假设线来确定是否填充了任何封闭区域。 该线与构成路径的一条或多条边界线交叉。 在缠绕模式下,如果在一个方向上绘制的边界线数量与在另一方向上绘制的边界线数量平衡,则不会填充该区域(外部);否则,该区域将被填充(内部)。 如果边界线的数量为奇数,则奇偶算法将填充一个区域。直观感受为,外圈顺时针将点添加进路径,内圈逆时针将点添加进路径,就可在内部形成一个空洞,这与海图空间记录编码标准一致。
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();
得到如下效果:
如图所示,每变换一步之前都执行了坐标保存,变换之后立即执行了坐标还原,因此每一步变换只对当前路径作用一次。