Silverlight 的图形描述语言 (GDL)






4.47/5 (7投票s)
一个用于在 Silverlight 中进行简单图形绘制的迷你语言。


引言
我正在做一个项目,需要提供一种绘制楼层平面图的方法(类似于 AutoCAD,但没那么复杂),我尝试了很多方法,但都不理想。我偶然读到 Nicolas Dorier : 的这篇文章:
https://codeproject.org.cn/Articles/637818/WPF-Geometries-the-Processing-way
这篇文章是针对 WPF 的,所以我做了一些修改,它就能很好地工作了。
GDL 是 Graphic Description Language 的缩写,是我自己发明的,我不知道是否已经存在这样的东西。
背景
没什么特别难的,你应该对 Path.Data 有些了解,因为 GDL 与它类似,但有所改动。坐标系在所有命令中保持不变。改变的是目标点或图形本身。例如,如果你使用 ROT 命令旋转 30 度,只有目标点会被旋转,而坐标系不会。
命令
所有命令均不区分大小写,分隔符:空格、制表符、换行符、逗号均可接受。命令和参数之间必须提供分隔符。
有 5 种命令。
- 移动命令:M MT MH MV MC ROT
这些命令会将当前点移动到计算出的点。如果下一个命令是绘制命令,它将从该点开始或以该点为中心。 - 线-曲线命令:L LT H V LC A AT CB CBT QB QBT
这些命令将从当前点绘制一条线或曲线到计算出的终点。除了 LC 命令,当前点将被设置为终点。 - 形状命令:R E
这些命令将绘制一个以当前点为中心的形状。当前点将保持不变。 - 设置命令:Z ZF CNT
这些命令没有参数。它们用于配合其他命令执行某些操作。 - 预处理命令:LOOP LOOPEND #
与其他命令不同,这些命令是预先处理的。
以下是命令的说明,在那些图像中,网格的方块是 20x20,左上角是 0,0。
M offsetX, offsetY [,rotateAngle]
将当前点移动到相对于当前点的偏移量为 offsetX, offsetY 的目标点。如果提供了 rotateAngle,则旋转点是当前点。MT X, Y
将当前点移动到 X,Y。MH offsetX [,rotateAngle]
等于 M offsetX, 0 [, rotateAngle]。MV offsetY [,rotateAngle]
等于 M 0, offsetY [,rotateAngle]。MC
将当前点移动到由 CNT 命令设置的旋转中心。ROT rotateAngle
将当前点移动到由 CNT 设置的旋转中心的目标点。
例如:
#移动到 100,100,然后将其设置为旋转中心,然后水平移动 100 个单位(到 200,100)。
#然后旋转 90 度(到 100,200)。
MT 100,100 CNT MH 100 ROT 90L offsetX, offsetY [,rotateAngle]
从当前点绘制一条线到相对于当前点的偏移量为 offsetX,offsetY 的目标点。
如果提供了 rotateAngle,则旋转点是当前点。
例如:
#从 100,100 画一条线到 130。
MT 100,100 L 100 100
LT X, Y
从当前点绘制一条线到 X,Y。
H length [,rotateAngle]
等于 L length, 0 [, rotateAngle] 。V length [,rotateAngle]
等于 L 0, length [, rotateAngle]。LC
从当前点绘制一条线到由 CNT 命令设置的旋转中心。A offsetX, offsetY, radiusX, radiusY [, sweepDirection, isLargeArc, angle]
从当前点绘制一条椭圆弧线到相对于当前点的偏移量为 offsetX,offsetY 的目标点,椭圆的半径为 radiusX, radiusY。
如果 sweepDirection 设置为 1,则为逆时针,否则为顺时针。
如果 isLargeArc 设置为 1,则为真,否则为假。
参数 angle 是用于椭圆的旋转,而不是目标点的旋转。
例如:
MT 100,60 A 80 0 80 40 1 1AT X, Y, radiusX, radiusY [, sweepDirection, isLargeArc, angle]
从当前点绘制一条椭圆弧线到 X,Y,椭圆的半径为 radiusX, radiusY。
如果 sweepDirection 设置为 1,则为逆时针,否则为顺时针。
如果 isLargeArc 设置为 1,则为真,否则为假。
参数 angle 是用于椭圆的旋转,而不是目标点的旋转。CB offsetX, offsetY, controlOffsetX1, controlOffsetY1, controlOffsetX2, controlOffsetY2
从当前点绘制一条三次贝塞尔曲线到目标点。
从目标点到当前点的偏移量是 offsetX, offsetY。
从第一个控制点到当前点的偏移量是 controlOffsetX1, controlOffsetY1
从第二个控制点到当前点的偏移量是 controlOffsetX2, controlOffsetY2
例如:
#从 0,140 画一条三次贝塞尔曲线到 300,140。第一个控制点是 100,300。第二个控制点是 200,0。
MT 0 140 CB 300 0 100 150 200 -150![]()
CBT X, Y, controlX1, controlY1 , controlX2, controlY2
从当前点绘制一条三次贝塞尔曲线到 X,Y。
第一个控制点是 controlX1, controlY1
第二个控制点是 controlX2, controlY2
例如:
#与 CB 命令中给出的示例相同。
MT 0,140 CBT 300,140 100,300 200,0QB offsetX, offsetY, controlOffsetX, controlOffsetY
从当前点绘制一条二次贝塞尔曲线到目标点。
从目标点到当前点的偏移量是 offsetX, offsetY。
从控制点到当前点的偏移量是 controlOffsetX, controlOffsetY
例如:
#从 50,140 画一条二次贝塞尔曲线到 250,140,控制点是 50,290。
MT 50 140 QB 200 0 0 150QBT X, Y, controlX, controlY
从当前点绘制一条二次贝塞尔曲线到 X,Y。
控制点是 controlX, controlY
例如:
#与 QB 命令中给出的示例相同。
MT 50,140 QBT 250,140 50,290R width [, height, rotateAngle]
绘制一个以当前点为中心的矩形。如果提供了 rotateAngle,旋转中心也是当前点。
例如:
#绘制一个以 150,150 为中心,尺寸为 80x150,旋转 30 度的矩形。
MT 150,150 R 80 150 30
#绘制一个以 140,120 为中心,尺寸为 100x100 的正方形。
MT 140,120 R 100
E radiusX [, radiusY, rotateAngle]
绘制一个以当前点为中心的椭圆。如果提供了 rotateAngle,旋转中心也是当前点。
例如:
#绘制一个以 150,150 为中心,尺寸为 80x150,旋转 45 度的椭圆。
MT 150,150 E 80 150 45
#绘制一个半径为 100,中心为 120,140 的圆。
MT 120,140 E 100
Z
关闭当前图形。起点由最新的 M 命令(M、MC、MH、MV、MT、ROT)设置,终点是当前点。当前点保持不变。ZF
与 Z 命令相同,但使用预定义的(硬编码的)颜色填充区域。CNT
将当前点设置为 ROT 命令的旋转中心。LOOP [count] .... LOOPEND
预处理命令。类似于宏,会被展开。如果未提供 count,循环将重复一次。
LOOP 命令将尝试找到第一个 LOOPEND 命令作为循环的结束。如果未使用 LOOPEND,它将被视为整个 GDL 的结束。
不支持嵌套循环。
例如:
MT 100,100 CNT MH 100 LOOP 3 ROT 30 LC LOOPEND MC E 50
等于
MT 100,100 CNT MH 100 ROT 30 LC ROT 30 LC ROT 30 LC MC E 50
使用代码
Proj. GDL 在下载的解决方案中是核心 Silverlight DLL 项目。它包含了所有的处理代码和一个 UserControl。
Proj. GDLEditor 是一个测试 Silverlight 项目。它的运行效果如文章的第一张图所示。
我使用的处理方式的根本思想来源于这篇文章。
https://codeproject.org.cn/Articles/637818/WPF-Geometries-the-Processing-way
我只是做了一些修改以适应我的需求。
如果您下载了 WPF 版本,以下是主要在 Silverlight 中无法工作的几个问题:
- 代码位置:ProcessingContext.FindOperations(string)
MethodInfo 没有 IsDefined(Type) 方法,而是 IsDefined(Type,bool)。
MethodInfo 没有 GetCustomAttribute<T>() 扩展方法(sl 无法使用正确的 dll 来实现),而是 GetCustomAttributes(Type,bool)。
这是我为 FindOperations(string name) 编写的代码。
private List<CandidateMethod> FindOperations(string name) { return this.GetType() .GetMethods() .Where(m => m.IsDefined(typeof(GDLCommandAttribute), false)) .Where(m => m.GetCustomAttributes(typeof(GDLCommandAttribute), false) .OfType<GDLCommandAttribute>() .First() .MatchCommand(name)) .Select(m => new CandidateMethod(this) { Method = m, ParameterValues = new List<object>() }) .OrderByDescending(o => o.Method.GetParameters().Length) .ToList(); ; }
- BezierSegment、TranslateTransform 和其他一些类只有默认构造函数,需要进行修改,这很容易。
- Geometry 类没有 GetOutlinedPathGeometry 方法,非常麻烦。因此,ExtractPath 方法无效。替代调用 ExtractPath 方法,Rectangle(x,x,x,x) 方法本身就完成了四条线的绘制(在一个新的 PathFigure 中绘制四条线构成一个矩形),而 Ellipse 使用 2 个 ArcSegments 来构成一个椭圆。
- Transform 没有 Identity 属性,所以我编写了一个 GetIdentityTransform() 辅助方法来直接生成矩阵。
- Transform 没有 Value 属性来访问矩阵,我搜索了一下如何提取矩阵。
我将 transform 对象放入一个 TransformGroup 对象中,然后获得了矩阵的 Value 属性。将其封装为 Tranform 类的扩展方法。
public static Matrix AppendMatrix(Matrix m1, Matrix m2)
{
var mt1 = new MatrixTransform() { Matrix = m1 };
var mt2 = new MatrixTransform() { Matrix = m2 };
var tg = new TransformGroup();
tg.Children.Add(mt1);
tg.Children.Add(mt2);
return tg.Value;
}
所有命令上方的 GDLCommandAttribute 可以为同一个命令函数提供多个字符串。
例如:
[GDLCommand("M","MOV","MOVE")]
public void Move()
{
....
}
处理流程:
- 使用 GDLGeometry.CleanGDL 方法删除所有注释、多余的分隔符并展开循环。
- 将 GDL 分解成一个字符串队列。
- 处理队列中的每个元素,尝试找到目标方法或将其放入一个方法。
- 调用该方法。
- 回到第 3 步,直到队列为空。
关注点
感谢 Nicolas Dorier ,我学会了如何创建一个具有“设计模式”的东西,以及一种通过字符串查找方法重载的方法。