Objective-C和C语言经常需要使用到指针。Swift中的数据类型由于良好的设计,使其可以和基于指针的C语言API无缝混用。同时Swift也可以自动处理大多数将指针作为参数的情况。在这篇文章里,我们可以看到在Swift语言中如何将变量、数组、字符串当做C语言中的指针参数来使用。

将输入输出参数作为指针参数

C和Objective-C不支持多类型的返回值。所以Cocoa API就使用指针作为函数的输入输出参数,以用来传递多类型的数据。Swift允许使用指针参数进行类似inout参数的处理,所以你可以使用&语法将一个var变量的引用作为指针参数进行传递。比如说,UIColorgetRed(_:green:blue:alpha:)方法,使用4个CGFloat*指针用来接收颜色的组成元素。我们可以使用&将这几个颜色组成部分装配在本地变量中。

var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)

另外一个常见的情况出现在Cocoa NSError类的使用中。很多方法都使用一个NSError**参数来保存异常信息。比如说,我们可以通过NSFileManager类的contentsOfDirectoryAtPath(_:error:)方法,挪列出指定目录中的信息,一旦出现疑似异常信息,就将其保存在NSError?类型的变量中。

var maybeError: NSError?
if let contents = NSFileManager.defaultManager()
    .contentsOfDirectoryAtPath("/usr/bin", error: &maybeError) {
    // Work with the directory contents
} else if let error = maybeError {
    // Handle the error
}

为安全起见,Swift要求在使用&传值时,变量必须是已经被初始化的。这是因为Swift无法知道也无法判断在操作指针之前,该指针是否确实在内存有指向的地址。

将数组作为指针参数

在C语言中,指针与数组是水乳交融,纠缠不清的。那么为了在Swift中能无缝的使用C语言中基于数组的一些API,Swift允许将Array作为指针参数。一个不可变数组的值可以作为一个const指针参数直接传递,可变数组可以使用&作为一个非const指针参数进行传递,就inout参数一样。比如,我们使用Accelerate框架中的vDSP_vadd函数对数组a和数组b进行相加,将结果写入result数组:

import Accelerate

let a: [Float] = [1, 2, 3, 4]
let b: [Float] = [0.5, 0.25, 0.125, 0.0625]
var result: [Float] = [0, 0, 0, 0]

vDSP_vadd(a, 1, b, 1, &result, 1, 4)

// result now contains [1.5, 2.25, 3.125, 4.0625]

将字符串作为指针参数

C语言中,传递字符串的主要方式是通过const char*指针。在Swift中,String也可以被用作const char*指针,用它可以向函数传递空字符串或UTF-8编码的字符串。比如,我们可以在标准的C语言和POSIX的库函数中直接使用字符串作为参数传递:

puts("Hello from libc")
let fd = open("/tmp/scratch.txt", O_WRONLY|O_CREAT, 0o666)

if fd < 0 {
    perror("could not open /tmp/scratch.txt")
} else {
    let text = "Hello World"
    write(fd, text, strlen(text))
    close(fd)
}

指针参数转换的安全性

Swift一直在努力让我们可以方便的、无缝的使用C语言中的指针,因为在Cocoa中已经使用的非常普遍了。虽然Swift是一个类型安全的语言,对指针参数的转换的安全性也有保障,但是相比Swift原生的其他代码来说,还是存在着一定的不安全性。所以我们在使用时要格外小心。比如说:

  • 如果调用者在指针返回之后保存了指针指向的对象,那么再去使用这个对象时是不安全的。这些被转换的指针参数只能在调用过程中或者发送消息过程中保证其有效性。即时你使用相同的变量、数组或者字符串作为多指针参数进行传递,你每次接收到的指针都是不同的。除非是全局或者静态变量。你可以安全的使用全局或静态变量的指针的参数,比如KVO上下文参数。
  • 当将数组或字符串作为指针参数传递时,Swift不会检查其边界值。在C语言中,数组和字符串的大小是不能增长的,所以当你将数组或字符串作为指针参数传递时,要确保它们有足够的大小,或者适合当前场景的大小。

如果你使用的基于指针的API不在这篇指导内,或者你需要重写接收指针参数的Cocoa方法,那么你可以直接使用Swift原始内存中的不安全的指针。我们会在以后的文章中介绍更多Swift的特性。

本文翻译自Swift官方博客,原文地址:Interacting with C Pointers

苹果在发布了Xcode 6 Bate 4后为Swift添加了新的特性–访问控制(Access Control),并且更新了The Swift Programming Language文档,我抽空把这篇文档翻译了一下,下面让我们来详细了解一下Access Control。

访问控制

访问控制可以限定你在源文件或模块中访问代码的级别,也就是说可以控制哪些代码你可以访问,哪些代码你不能访问。这个特性可以让我们隐藏功能实现的一些细节,并且可以明确的指定我们提供给其他人的接口中哪些部分是他们可以使用的,哪些是他们看不到的。

你可以明确的给类、结构体、枚举、设置访问级别,也可以给属性、函数、初始化方法、基本类型、下标索引等设置访问级别。协议也可以被限定在一定的范围内使用,包括协议里的全局常量、变量和函数。

在提供了不同访问级别的同时,Swift并没有规定我们要在任何时候都要在代码中明确指定访问级别。其实,如果我们作为独立开发者在开发我们自己的app,而不是在开发一些Framework的时候,我们完全可以不用明确的指定代码的访问级别。

注意:为方便起见,在代码中可以设置访问级别的它们(属性、基本类型、函数等)在下面的章节中我们称之为“实体”。

模块和源文件

Swift中的访问控制模型基于模块和源文件这两个概念。

模块指的是FrameworkApp bundle。在Swift中,可以用import关键字引入自己的工程。

在Swift中,FramewordkApp bundle被作为模块处理。如果你是为了实现某个通用的功能,或者是为了封装一些常用方法而将代码打包成Framework,这个Framework在Swift中就被称为模块。不论它被引入到某个App工程或者其他的Framework,它里面的一切(属性、函数等)都属于这个模块。

源文件指的是Swift中的Swift File,就是编写Swift代码的文件,它通常属于一个模块。通常一个源文件包含一个,在中又包含函数属性等类型。

访问级别

Swift提供了三种不同的访问级别。这些访问级别相对于源文件中定义的实体,同时也相对于这些源文件所属的模块。

  • Public:可以访问自己模块或应用中源文件里的任何实体,别人也可以访问引入该模块中源文件里的所有实体。通常情况下,某个接口或Framework是可以被任何人使用时,你可以将其设置为public级别。
  • Internal:可以访问自己模块或应用中源文件里的任何实体,但是别人不能访问该模块中源文件里的实体。通常情况下,某个接口或Framework作为内部结构使用时,你可以将其设置为internal级别。
  • Private:只能在当前源文件中使用的实体,称为私有实体。使用private级别,可以用作隐藏某些功能的实现细节。

Public为最高级访问级别,Private为最低级访问级别。

访问级别的使用原则

在Swift中,访问级别有如下使用原则:访问级别统一性。
比如说:

  • 一个public访问级别的变量,不能将它的类型定义为internalprivate的类型。因为变量可以被任何人访问,但是定义它的类型不可以,所以这样就会出现错误。
  • 函数的访问级别不能高于它的参数、返回类型的访问级别。因为如果函数定义为public而参数或者返回类型定义为internalprivate,就会出现函数可以被任何人访问,但是它的参数和返回类型不可以,同样会出现错误。

默认访问级别

代码中的所有实体,如果你不明确的定义其访问级别,那么它们默认为internal级别。在大多数情况下,我们不需要明确的设置实体的访问级别,因为我们大多数时候都是在开发一个App bundle。

单目标应用程序的访问级别

当你编写一个单目标应用程序时,该应用的所有功能都是为该应用服务,不需要提供给其他应用或者模块使用,所以我们不需要明确设置访问级别,使用默认的访问级别internal即可。但是如果你愿意,你也可以使用private级别,用于隐藏一些功能的实现细节。

Framework的访问级别

当你开发Framework时,就需要把一些实体定义为public级别,以便其他人导入该Framework后可以正常使用其功能。这些被你定义为public的实体,就是这个Framework的API。

注意:Framework的内部实现细节依然可以使用默认的internal级别,或者也可以定义为private级别。只有你想将它作为API的实体,才将其定义为public级别。

访问控制语法

通过修饰符publicinternalprivate来声明实体的访问级别:

public class SomePublicClass {}
internal class SomeInternalClass {}
private class SomePrivateClass {}

public var somePublicVariable = 0
internal let someInternalConstant = 0
private func somePrivateFunction() {}

除非有特殊的说明,否则实体都使用默认的访问级别internal,可以查阅默认访问级别这一节。这意味着SomeInternalClasssomeInternalConstant不用明确的使用修饰符声明访问级别,但是他们任然拥有隐式的访问级别internal

class SomeInternalClass {}              // 隐式访问级别internal
var someInternalConstant = 0            // 隐式访问级别 internal

自定义类型

如果你想为一个自定义类型指定一个明确的访问级别,那么你要明确一点。那就是你要确保新类型的访问级别和它实际的作用域相匹配。比如说,如果某个类里的属性、函数、返回值它们的作用域仅在当前的源文件中,那么你就可以将这个类申明为private类,而不需要申明为public或者internal类。

类的访问级别也可以影响到类成员(属性、函数、初始化方法等)的默认访问级别。如果你将类申明为private类,那么该类的所有成员的默认访问级别也会成为private。如果你将类申明为public或者internal类(或者不明确的指定访问级别,而使用默认的internal访问级别),那么该类的所有成员的访问级别是internal

注意:上面提到,一个public类的所有成员的访问级别默认为internal级别,而不是public级别。如果你想将某个成员申明为public级别,那么你必须使用修饰符明确的申明该成员。这样做的好处是,在你定义公共接口API的时候,可以明确的选择哪些属性或方法是需要公开的,哪些是内部使用的,可以避免将内部使用的属性方法公开成公共API的错误。

public class SomePublicClass {          // 显示的 public 类
    public var somePublicProperty = 0    // 显示的 public 类成员 
    var someInternalProperty = 0         // 隐式的 internal 类成员 
    private func somePrivateMethod() {}  // 显示的 private 类成员 
}

class SomeInternalClass {               // 隐式的 internal 类
    var someInternalProperty = 0         // 隐式的 internal 类成员 
    private func somePrivateMethod() {}  // 显示的 private 类成员 
}

private class SomePrivateClass {        // 显示的 private 类
    var somePrivateProperty = 0          // 隐式的 private 类成员 
    func somePrivateMethod() {}          // 隐式的 private 类成员 
}

元组类型

元组的访问级别使用是所有类型的访问级别使用中最为严谨的。比如说,如果你构建一个包含两种不同类型元素的元组,其中一个元素类型的访问级别为internal,另一个为private级别,那么这个元组的访问级别为private。也就是说元组的访问级别遵循它里面元组中最低级的访问级别。

注意:元组不同于类、结构体、枚举、函数那样有单独的定义。元组的访问级别是在它被使用时自动推导出的,而不是明确的申明。

函数类型

函数的访问级别需要根据该函数的参数类型访问级别、返回类型访问级别得出。如果根据参数类型和返回类型得出的函数访问级别不符合上下文,那么就需要明确的申明该函数的访问级别。

下面的例子中定义了一个全局函数名为someFunction,并且没有明确的申明其访问级别。你也许会认为该函数应该拥有默认的访问级别internal,但事实并非如此。事实上,如果按下面这种写法,编译器是无法编译通过的:

func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}

我们可以看到,这个函数的返回类型是一个元组,该元组中包含两个自定义的类(可查阅自定义类型)。其中一个类的访问级别是internal,另一个的访问级别是private,所以根据元组访问级别的原则,该元组的访问级别是private(元组的访问级别遵循它里面元组中最低级的访问级别)。

因为该函数返回类型的访问级别是private,所以你必须使用private修饰符,明确的申请该函数:

private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // function implementation goes here
}

将该函数申明为publicinternal,或者使用默认的访问级别internal都是错误的,因为如果把该函数当做publicinternal级别来使用的话,是无法得到private级别的返回值的。

枚举类型

枚举中成员的访问级别继承自该枚举,你不能为枚举中的成员指定访问级别。
比如下面的例子,枚举CompassPoint被明确的申明为public级别,那么它的成员NorthSouthEastWest的访问级别同样也是public

public enum CompassPoint {
    case North
    case South
    case East
    case West
}

原始值和关联值

用于枚举定义中的任何原始值,或关联的值类型必须有一个访问级别,至少要高于枚举的访问级别。比如说,你不能在一个internal访问级别的枚举中定义private级别的原始值类型。

嵌套类型

如果在private级别的类型中定义嵌套类型,那么该嵌套类型就自动拥有private访问级别。如果在public或者internal级别的类型中定义嵌套类型,那么该嵌套类型自动拥有internal访问级别。如果想让嵌套类型拥有public访问级别,那么需要对该嵌套类型进行明确的访问级别申明。

子类

子类的访问级别不得高于父类的访问级别。比如说,父类的访问级别是internal,子类的访问级别就不能申明为public
此外,在满足子类不高于父类访问级别以及遵循各访问级别作用域(即模块或源文件)的前提下,你可以重写任意类成员(方法、属性、初始化方法、下标索引等)。

如果我们无法直接访问某个类中的属性或函数等,那么可以继承该类,从而可以更容易的访问到该类的类成员。下面的例子中,类A的访问级别是public,它包含一个函数someMethod,访问级别为private。类B继承类A,并且访问级别申明为internal,但是在类B中重写了类A中访问级别为private的方法someMethod,并重新申明为internal级别。通过这种方式,我们就可以访问到某类中private级别的类成员,并且可以重新申明其访问级别,以便其他人使用:

public class A {
    private func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {}
}

只要满足子类不高于父类访问级别以及遵循各访问级别作用域的前提下(即private的作用域在同一个源文件中,internal的作用域在同一个模块下),我们甚至可以在子类中,用子类成员访问父类成员,哪怕父类成员的访问级别比子类成员的要低:

public class A {
    private func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {
        super.someMethod()
    }
}

因为父类A和子类B定义在同一个源文件中,所以在类B中可以在重写的someMethod方法中调用super.someMethod()

常量、变量、属性、下标

常量、变量、属性不能拥有比它们的类型更高的访问级别。比如说,你定义一个public级别的属性,但是它的类型是private级别的,这是编译器不允许的。同样,下标也不能拥有比索引类型或返回类型更高的访问级别。

如果常量、变量、属性、下标索引的定义类型是private级别的,那么它们必须要明确的申明访问级别为private

private var privateInstance = SomePrivateClass()

Getter和Setter

常量、变量、属性、下标索引的GettersSetters的访问级别继承自它们所属成员的访问级别。

Setter的访问级别可以低于对应的Getter的访问级别,这样就可以控制变量、属性或下标索引的读写权限。在varsubscript定义作用域之前,你可以通过private(set)internal(set)先为它门的写权限申明一个较低的访问级别。

注意:这个规定适用于用作存储的属性或用作计算的属性。即使你不明确的申明存储属性的GetterSetter,Swift也会隐式的为其创建GetterSetter,用于对该属性进行读取操作。使用private(set)internal(set)可以改变Swift隐式创建的Setter的访问级别。在计算属性中也是同样的。

下面的例子中定义了一个结构体名为TrackedString,它记录了value属性被修改的次数:

struct TrackedString {
    private(set) var numberOfEdits = 0
    var value: String = "" {
    didSet {
        numberOfEdits++
    }
    }
}

TrackedString结构体定义了一个用于存储的属性名为value,类型为String,并将初始化值设为""(即一个空字符串)。该结构体同时也定义了另一个用于存储的属性名为numberOfEdits,类型为Int,它用于记录属性value被修改的次数。这个功能的实现通过属性valuedidSet方法实现,每当给value赋新值时就会调用didSet方法,给numberOfEdits加一。

结构体TrackedString和它的属性value均没有明确的申明访问级别,所以它们都拥有默认的访问级别internal。但是该结构体的numberOfEdits属性使用private(set)修饰符进行申明,这意味着numberOfEdits属性只能在定义该结构体的源文件中赋值。numberOfEdits属性的Getter依然是默认的访问级别internal,但是Setter的访问级别是private,这表示该属性只有在当前的源文件中是可读可写的,在当前源文件所属的模块中它只是一个可读的属性。

如果你实例化TrackedString结构体,并且多次对value属性的值进行修改,你就会看到numberOfEdits的值会随着修改次数更改:

var stringToEdit = TrackedString()
stringToEdit.value = "This string will be tracked."
stringToEdit.value += " This edit will increment numberOfEdits."
stringToEdit.value += " So will this one."
println("The number of edits is \(stringToEdit.numberOfEdits)")
// prints "The number of edits is 3"

虽然你可以在其他的源文件中实例化该结构体并且获取到numberOfEdits属性的值,但是你不能对其进行赋值。这样就能很好的告诉使用者,你只管使用,而不需要知道其实现细节。

初始化

我们可以给自定义的初始化方法指定访问级别,但是必须要低于或等于它所属类的访问级别。但如果该初始化方法是必须要使用的话,那它的访问级别就必须和所属类的访问级别相同。

如同函数或方法参数,初始化方法参数的访问级别也不能低于初始化方法的访问级别。

默认初始化方法

Swift为结构体、类都提供了一个默认的无参初始化方法,用于给它们的所有属性提供赋值操作,但不会给出具体值。默认初始化方法可以参阅Default Initializers。默认初始化方法的访问级别与所属类型的访问级别相同。

注意:如果一个类型被申明为public级别,那么默认的初始化方法的访问级别为internal。如果你想让无参的初始化方法在其他模块中可以被使用,那么你必须提供一个具有public访问级别的无参初始化方法。

结构体的默认成员初始化方法

如果结构体中的任一存储属性的访问级别为private,那么它的默认成员初始化方法访问级别就是private。尽管如此,结构体的初始化方法的访问级别依然是internal

如果你想在其他模块中使用该结构体的默认成员初始化方法,那么你需要提供一个访问级别为public的默认成员初始化方法。

协议

如果你想为一个协议明确的申明访问级别,那么有一点需要注意,就是你要确保该协议只在你申明的访问级别作用域中使用。

协议中的每一个必须要实现的函数都具有和该协议相同的访问级别。这样才能确保该协议的使用者可以实现它所提供的函数。

注意:如果你定义了一个public访问级别的协议,那么实现该协议提供的必要函数也会是public的访问级别。这一点不同于其他类型,比如,public访问级别的其他类型,他们成员的访问级别为internal

协议继承

如果定义了一个新的协议,并且该协议继承了一个已知的协议,那么新协议拥有的访问级别最高也只和被继承协议的访问级别相同。比如说,你不能定义一个public的协议而去继承一个internal的协议。

协议一致性

类可以采用比自身访问级别低的协议。比如说,你可以定义一个public级别的类,可以让它在其他模块中使用,同时它也可以采用一个internal级别的协议,并且只能在定义了该协议的模块中使用。

采用了协议的类的访问级别遵循它本身和采用协议中最低的访问级别。也就是说如果一个类是public级别,采用的协议是internal级别,那个采用了这个协议后,该类的访问级别也是internal

如果你采用了协议,那么实现了协议必须的方法后,该方法的访问级别遵循协议的访问级别。比如说,一个public级别的类,采用了internal级别的协议,那么该类实现协议的方法至少也得是internal

注意:在Swift中和Objective-C中一样,协议的一致性保证了一个类不可能在同一个程序中用不同的方法采用同一个协议。

扩展

你可以在条件允许的情况下对类、结构体、枚举进行扩展。扩展成员应该具有和原始类成员一致的访问级别。比如你扩展了一个公共类型,那么你新加的成员应该具有和原始成员一样的默认的internal访问级别。

或者,你可以明确申明扩展的访问级别(比如使用private extension)给该扩展内所有成员指定一个新的默认访问级别。这个新的默认访问级别仍然可以被单独成员所指定的访问级别所覆盖。

协议的扩展

如果一个扩展采用了某个协议,那么你就不能对该扩展使用访问级别修饰符来申明了。该扩展中实现协议的方法都会遵循该协议的访问级别。

泛型

泛型类型或泛型函数的访问级别遵循泛型类型、函数本身、泛型类型参数三者中访问级别最低的级别。

类型别名

任何被你定义的类型别名都会认为是不同的类型进行访问控制。一个类型别名的访问级别低于或等于这个类型的访问级别。比如说,一个private级别的类型别名可以设定给一个publicinternalprivate的类型,但是一个public级别的类型别名只能设定给一个public级别的类型,不能设定给internalprivate的类类型。

注意:这条规则也适用于为满足协议一致性而给相关类型命名别名。

相关链接

你可以自定义节点的样式和行为。

常规节点

简单的框图节点

大多数情况下,我们的节点都是一个框包含着文本信息(TextBlock),并且将排列属性设置为Panel.Auto

diagram.nodeTemplate =
$(go.Node, "Auto",
  $(go.Shape, "Rectangle",
    new go.Binding("fill", "color")),
  $(go.TextBlock,
    { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.model.nodeDataArray = [
    { key: "Alpha", color: "lightblue" }
];

结果:
node

其他形状的框图节点

下面的例子中展示其他几种形状的框图:

diagram.nodeTemplate =
$(go.Node, "Auto",
  $(go.Shape,
    new go.Binding("figure", "fig"),
    new go.Binding("fill", "color")),
  $(go.TextBlock,
    { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.model.nodeDataArray = [
    { key: "Alpha", color: "lightblue", fig: "RoundedRectangle" },
    { key: "Beta", color: "lightblue", fig: "Ellipse" },
    { key: "Gamma", color: "lightblue", fig: "Hexagon" },
    { key: "Delta", color: "lightblue", fig: "FramedRectangle" },
    { key: "Epsilon", color: "lightblue", fig: "Cloud" },
    { key: "Zeta", color: "lightblue", fig: "Procedure" }
];

结果:
node

较复杂的框图节点

一个排列参数设置为Panel.Auto的节点中,你不必将TextBlock局限为一个,你可以根据情况自行组合Panel对象。下面的例子中就使用TablePanel包含了多个TextBlock

diagram.nodeTemplate =
$(go.Node, "Auto",
  $(go.Shape,
    { fill: $(go.Brush, go.Brush.Linear, { 0: "white", 1: "lightblue" }),
      stroke: "darkblue", strokeWidth: 2 }),
  $(go.Panel, "Table",
    { defaultAlignment: go.Spot.Left, margin: 4 },
    $(go.RowColumnDefinition, { column: 1, width: 4 }),
    $(go.TextBlock,
      { row: 0, column: 0, columnSpan: 3, alignment: go.Spot.Center },
      { font: "bold 12pt sans-serif" },
      new go.Binding("text", "key")),
    $(go.TextBlock, "First: ",
      { row: 1, column: 0 }),
    $(go.TextBlock,
      { row: 1, column: 2 },
      new go.Binding("text", "prop1")),
    $(go.TextBlock, "Second: ",
      { row: 2, column: 0 }),
    $(go.TextBlock,
      { row: 2, column: 2 },
      new go.Binding("text", "prop2"))
  )
);

diagram.model.nodeDataArray = [
    { key: "Alpha", prop1: "value of 'prop1'", prop2: "the other property" }
];

结果:
node

简单组合节点

图标节点

像下面例子这种的节点,一般元素的排列属性都设置为Panel.Vertical,也就是说上面是图,下面是文字信息。

diagram.nodeTemplate =
$(go.Node, "Vertical",
  $(go.Picture,
    { maxSize: new go.Size(50, 50) },
    new go.Binding("source", "img")),
  $(go.TextBlock,
    { margin: new go.Margin(3, 0, 0, 0),
      maxSize: new go.Size(100, 30),
      editable: true, isMultiline: false },
    new go.Binding("text", "text"))
);

diagram.model.nodeDataArray = [
    { text: "kitten", img: "images/50x40.png" }
];

结果:
node

小图标节点

一般这种节点元素的排列属性为Panel.Horizontal,水平排列,就是左边是图,右边是文字信息。

diagram.nodeTemplate =
$(go.Node, "Horizontal",
  $(go.Picture,
    { maxSize: new go.Size(16, 16) },
    new go.Binding("source", "img")),
  $(go.TextBlock,
    { margin: new go.Margin(0, 0, 0, 2) },
    new go.Binding("text", "text"))
);

diagram.model.nodeDataArray = [
    { text: "kitten", img: "images/50x40.png" }
];

结果:
node

复杂组合节点

有时候,你需要在节点中展示更多的信息。那么我们就需要将元素排列属性设置为Panel.Spot,这个属性的意思就是Node里的元素都排列在这个Node中的各个锚点上。

diagram.nodeTemplate =
$(go.Node, "Spot",
  // the main content:
  $(go.Panel, "Vertical",
    $(go.Picture,
      { maxSize: new go.Size(50, 50) },
      new go.Binding("source", "img")),
    $(go.TextBlock,
      { margin: new go.Margin(3, 0, 0, 0) },
      new go.Binding("text", "text"),
      new go.Binding("stroke", "error", function(err) { return err ? "red" : "black" }))
  ),
  // decorations:
  $(go.Shape, "TriangleUp",
    { alignment: go.Spot.TopLeft,
      fill: "yellow", width: 14, height: 14,
      visible: false },
    new go.Binding("visible", "info", function(i) { return i ? true : false; })),
  $(go.Shape, "StopSign",
    { alignment: go.Spot.TopRight,
      fill: "red", width: 14, height: 14,
      visible: false },
    new go.Binding("visible", "error")),
  {
    toolTip:
      $(go.Adornment, "Auto",
        $(go.Shape, { fill: "#FFFFCC" },
          new go.Binding("visible", "info", function(i) { return i ? true : false; })),
        $(go.TextBlock, { margin: 4 },
          new go.Binding("text", "info"))
      )
  }
);

diagram.model.nodeDataArray = [
    { text: "kitten", img: "images/50x40.png", info: "" },
    { text: "kitten", img: "images/50x40.png", error: true, info: "shredded curtains" }
];

结果:
node

Link对象顾名思义就是连线,它可以通过连线的方式表达Node之间的关系。

无方向连线

最简单的Link就是没有箭头的,无方向的连线。当Diagram中的Node之间的关系没有方向性或者它们之间存在隐性关系的时候,我们使用这种连线。

我们在构建Link时,先设置创建模板(linkTemplate),该模板指定了连线的样式。然后创建一个指定连线关系的数组linkDataArray,我们通过该数组指定Node之间的连线关系,最后通过GraphLinksModel方法构建连线。

diagram.nodeTemplate =
$(go.Node, "Auto",
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,       // the whole link panel
  $(go.Shape));  // the link shape, default black stroke

var nodeDataArray = [
{ key: "Alpha", loc: "0 0" },
{ key: "Beta", loc: "100 50" }
];
var linkDataArray = [
{ from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

带方向连线

一般情况下我们都需要使用带方向的连线,即带箭头的连线。我们只需要修改一下linkTemplate即可,我们在连线模板中加入一个箭头,通过在Link中再添加一个Shape,然后设置Shape.toArrow属性为OpenTrigangle

diagram.nodeTemplate =
$(go.Node, "Auto",
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,
  $(go.Shape),  // the link shape
  $(go.Shape,   // the arrowhead
    { toArrow: "OpenTriangle", fill: null })
);

var nodeDataArray = [
    { key: "Alpha", loc: "0 0" },
    { key: "Beta", loc: "100 50" }
];
var linkDataArray = [
    { from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

连线路径

如果你想改变连线的路径,那么需要设置Link.routing属性,下面的例子中展示了Link.NormalLink.Orthogonal两种连线路径。

diagram.nodeTemplate =
$(go.Node, "Auto",
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,
  new go.Binding("routing", "routing"),
  $(go.Shape),
  $(go.Shape, { toArrow: "Standard" })
);

var nodeDataArray = [
    { key: "Alpha", loc: "0 0" },
    { key: "Beta", loc: "50 50" },
    { key: "Gamma", loc: "100 25" }
];
var linkDataArray = [
    { from: "Alpha", to: "Beta", routing: go.Link.Normal },
    { from: "Alpha", to: "Gamma", routing: go.Link.Orthogonal }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

要注意的是,这里设置连线路径的是Shape里的属性,在GraphObject中也有设置路径的属性,GraphObject.fromSpotGraphObject.toSpot,前者是作用于Shape上的,后者是作用于容器上的。

你还可以设置Link.routing属性为Link.AvoidsNodes,可以让连线自动避开Node,也就是说可以避免LinkNode相交。但是如果有一个Node离起始Node或者终点Node非常近的时候,连线就没有办法避让了,因为它没有足够的空间去计算避让路线。

diagram.nodeTemplate =
$(go.Node, "Auto",
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,
  { routing: go.Link.AvoidsNodes },  // link route should avoid nodes
  $(go.Shape),
  $(go.Shape, { toArrow: "Standard" })
);

var nodeDataArray = [
    { key: "Alpha", loc: "0 0" },
    { key: "Beta", loc: "250 40" },
    { key: "Gamma", loc: "100 0" },
    { key: "Delta", loc: "75 50" },
    { key: "Epsilon", loc: "150 30" }
];
var linkDataArray = [
    { from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

连线的弯曲度和角度

在我们创建好连线关系以及路径后,就可以使用其他属性来控制连线的其他特性,比如说弯曲度。我们可以通过设置Link.curve属性设置连线的弯曲度,默认情况下该属性的值为Link.Node

下面的例子中,我们将Link.curve属性设置为Link.bezier,让连线产生弧度。

diagram.nodeTemplate =
$(go.Node, "Auto",
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,
  { curve: go.Link.Bezier },  // Bezier curve
  $(go.Shape),
  $(go.Shape, { toArrow: "Standard" })
);

var nodeDataArray = [
    { key: "Alpha", loc: "0 0" },
    { key: "Beta", loc: "100 50" }
];
var linkDataArray = [
    { from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

我们还可以通过Link.curviness属性设置连线的弯曲力度。

下面的例子中,两个Node之间有多条连线,并且我们没有设置连线的弯曲度,但是我们可以看到,每条连线都自动的产生了轻微的弯曲度的变化。

diagram.nodeTemplate =
$(go.Node, "Auto",
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,
  { curve: go.Link.Bezier },
  $(go.Shape),
  $(go.Shape, { toArrow: "Standard" })
);

var nodeDataArray = [
    { key: "Alpha", loc: "0 0" },
    { key: "Beta", loc: "100 50" }
];
var linkDataArray = [
    { from: "Alpha", to: "Beta" },  // multiple links between the same nodes
    { from: "Alpha", to: "Beta" },
    { from: "Alpha", to: "Beta" },
    { from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

上面第四个例子中,连线避让Node时折角都是直角,我们还可以通过Link.corner属性,设置连线折角的弧度。

diagram.nodeTemplate =
$(go.Node, "Auto",
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,
  { routing: go.Link.AvoidsNodes,
    corner: 10 },                  // rounded corners
  $(go.Shape),
  $(go.Shape, { toArrow: "Standard" })
);

var nodeDataArray = [
    { key: "Alpha", loc: "0 0" },
    { key: "Beta", loc: "250 40" },
    { key: "Gamma", loc: "100 0" },
    { key: "Delta", loc: "75 50" },
    { key: "Epsilon", loc: "150 30" }
];
var linkDataArray = [
    { from: "Alpha", to: "Beta" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

当两条连线相交时,我们希望在视觉上能有所表示。那么我们可以设置Link.curve属性为Link.JumpOver属性,展现出的效果就是一条线越过了交叉的另一条线。

diagram.nodeTemplate =
$(go.Node, "Auto",
  { locationSpot: go.Spot.Center },
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,
  { routing: go.Link.Orthogonal,  // may be either Orthogonal or AvoidsNodes
    curve: go.Link.JumpOver },
  $(go.Shape),
  $(go.Shape, { toArrow: "Standard" })
);

var nodeDataArray = [
    { key: "Alpha", loc: "0 50" },
    { key: "Beta", loc: "100 50" },
    { key: "Alpha2", loc: "50 0" },
    { key: "Beta2", loc: "50 100" }
];
var linkDataArray = [
    { from: "Alpha", to: "Beta" },  // these two links will cross
    { from: "Alpha2", to: "Beta2" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

或者我们也可以将Link.curve属性设置为Link.JumpGap,表现为当两条连线相交时,其中一条在相交处会断开。

diagram.nodeTemplate =
$(go.Node, "Auto",
  { locationSpot: go.Spot.Center },
  new go.Binding("location", "loc", go.Point.parse),
  $(go.Shape, "RoundedRectangle", { fill: "lightgray" }),
  $(go.TextBlock, { margin: 5 },
    new go.Binding("text", "key"))
);

diagram.linkTemplate =
$(go.Link,
  { routing: go.Link.Orthogonal,  // may be either Orthogonal or AvoidsNodes
    curve: go.Link.JumpGap },
  $(go.Shape),
  $(go.Shape, { toArrow: "Standard" })
);

var nodeDataArray = [
    { key: "Alpha", loc: "0 50" },
    { key: "Beta", loc: "100 50" },
    { key: "Alpha2", loc: "50 0" },
    { key: "Beta2", loc: "50 100" }
];
var linkDataArray = [
    { from: "Alpha", to: "Beta" },  // these two links will cross
    { from: "Alpha2", to: "Beta2" }
];
diagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);

结果:
Link

通过Shape你可以构建一个几何图形,并且可以控制它的形状和轮廓颜色以及填充色等。

图形

你可以通过Shape.figure属性设置它的形状。让你使用GraphObject.make方法构建时,你可以将形状参数以字符串形式作为第二个参数。你可能还需要通过GraphObject.desiredSizeGraphObject.widthGraphObject.height属性设置Shape的尺寸大小。

下面的例子中列出了一些常用的Shape形状,并且你可以看到它们的名字:

diagram.add(
$(go.Part, "Horizontal",
  $(go.Shape, "Rectangle",
              { width: 40, height: 60, margin: 4, fill: null }),
  $(go.Shape, "RoundedRectangle",
              { width: 40, height: 60, margin: 4, fill: null }),
  $(go.Shape, "Ellipse",
              { width: 40, height: 60, margin: 4, fill: null }),
  $(go.Shape, "Triangle",
              { width: 40, height: 60, margin: 4, fill: null }),
  $(go.Shape, "Diamond",
              { width: 40, height: 60, margin: 4, fill: null })
));

结果:
shape

填充色与轮廓

可以通过Shape.stroke属性设置Shape的轮廓线条颜色。通过Shape.strokeWidth设置轮廓线条的粗细度。通过Shape.fill属性设置填充颜色。

diagram.add(
$(go.Part, "Horizontal",
  $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4
                }),  // default fill and stroke are "black"
  $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                fill: "green" }),
  $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                fill: "green", stroke: null }),
  $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                fill: null, stroke: "green" }),
  $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                fill: null, stroke: "green", strokeWidth: 3 }),
  $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                fill: null, stroke: "green", strokeWidth: 6 }),
  $(go.Shape, { figure: "Club", width: 40, height: 40, margin: 4,
                fill: "green", background: "orange" })
));

结果:
shape

Shape.strokeShape.fill属性的值一般使用CSS设置,它们的默认颜色都是黑色。但是我们最常用的还是将它们设置为nulltransparent,前者意思是空,后者是透明,在表象上,这两者是没有差距的,但是在点击行为上有区别。如果设置为null,那么只能点击图形的轮廓才能选中,点击图形里面是无法选中的。而如设置为transparent,点击图形的任何地方都可以选中该图形。

diagram.add(
$(go.Part, "Table",
  $(go.Shape, { row: 0, column: 0, figure: "Club", width: 40, height: 40, margin: 4,
                fill: "green" }),
  $(go.TextBlock, "green", { row: 1, column: 0 }),
  $(go.Shape, { row: 0, column: 1, figure: "Club", width: 40, height: 40, margin: 4,
                fill: "white" }),
  $(go.TextBlock, "white", { row: 1, column: 1 }),
  $(go.Shape, { row: 0, column: 2, figure: "Club", width: 40, height: 40, margin: 4,
                fill: "transparent" }),
  $(go.TextBlock, "transparent", { row: 1, column: 2 }),
  $(go.Shape, { row: 0, column: 3, figure: "Club", width: 40, height: 40, margin: 4,
                fill: null }),
  $(go.TextBlock, "null", { row: 1, column: 3 })
));

结果:
shape

几何形状

形状其实就是若干个不同坐标的点,然后被线连起来形成的。上述的例子中,我们使用的都是gojs定义好的一些形成,然而gojs也支持用户自定义形状,可以使用Shape.geometry属性。可以通过Geometry.parse来创建一个几何图形,不过Geometry的表达式交为复杂,我们将在后期对其进行详细的介绍。

下面的例子中,我们展示了通过Geometry.parse方法创建自定义“W”几何图形的过程:

var W_geometry = go.Geometry.parse("M 0,0 L 10,50 20,10 30,50 40,0", false);
diagram.add(
$(go.Part, "Horizontal",
  $(go.Shape, { geometry: W_geometry, strokeWidth: 2 }),
  $(go.Shape, { geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                strokeJoin: "miter", strokeCap: "butt" }),
  $(go.Shape, { geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                strokeJoin: "miter", strokeCap: "round" }),
  $(go.Shape, { geometry: W_geometry, stroke: "blue", strokeWidth: 10,
                strokeJoin: "miter", strokeCap: "square" }),
  $(go.Shape, { geometry: W_geometry, stroke: "green", strokeWidth: 10,
                strokeJoin: "bevel", strokeCap: "butt" }),
  $(go.Shape, { geometry: W_geometry, stroke: "green", strokeWidth: 10,
                strokeJoin: "bevel", strokeCap: "round" }),
  $(go.Shape, { geometry: W_geometry, stroke: "green", strokeWidth: 10,
                strokeJoin: "bevel", strokeCap: "square" }),
  $(go.Shape, { geometry: W_geometry, stroke: "red", strokeWidth: 10,
                strokeJoin: "round", strokeCap: "butt" }),
  $(go.Shape, { geometry: W_geometry, stroke: "red", strokeWidth: 10,
                strokeJoin: "round", strokeCap: "round" }),
  $(go.Shape, { geometry: W_geometry, stroke: "red", strokeWidth: 10,
                strokeJoin: "round", strokeCap: "square" }),
  $(go.Shape, { geometry: W_geometry, stroke: "purple", strokeWidth: 2,
                strokeDashArray: [4, 2] }),
  $(go.Shape, { geometry: W_geometry, stroke: "purple", strokeWidth: 2,
                strokeDashArray: [6, 6, 2, 2] })
));

结果:
shape

角度和比例

除了通过GraphObject.desiredSizeGraphObject.widthGraphObject.height这几个属性设置Shape的尺寸大小外,还可以使用GraphObject.angleGraphObject.scale属性设置Shape的角度和比例。

diagram.add(
$(go.Part, "Table",
  $(go.Shape, { row: 0, column: 1,
                figure: "Club", fill: "green", width: 40, height: 40,
                }),  // default angle is zero; default scale is one
  $(go.Shape, { row: 0, column: 2,
                figure: "Club", fill: "green", width: 40, height: 40,
                angle: 30 }),
  $(go.Shape, { row: 0, column: 3,
                figure: "Club", fill: "green", width: 40, height: 40,
                scale: 1.5 }),
  $(go.Shape, { row: 0, column: 4,
                figure: "Club", fill: "green", width: 40, height: 40,
                angle: 30, scale: 1.5 })
));

结果:
shape

下面的例子中,将Shape.fill的属性设置为Brush画笔对象,并使用了线性渐变画笔给Shape填充颜色。

diagram.add(
$(go.Part, "Table",
  $(go.Shape, { row: 0, column: 0,
                figure: "Club", width: 40, height: 40, angle: 0, scale: 1.5,
                fill: $(go.Brush, go.Brush.Linear, { 0.0: "blue", 1.0: "red" }),
                background: $(go.Brush, go.Brush.Linear, { 0.0: "yellow", 1.0: "green" }),
                areaBackground: $(go.Brush, go.Brush.Linear, { 0.0: "gray", 1.0: "lightgray" }) }),
  $(go.Shape, { row: 0, column: 1, width: 10, fill: null, stroke: null }),
  $(go.Shape, { row: 0, column: 2,
                figure: "Club", width: 40, height: 40, angle: 45, scale: 1.5,
                fill: $(go.Brush, go.Brush.Linear, { 0.0: "blue", 1.0: "red" }),
                background: $(go.Brush, go.Brush.Linear, { 0.0: "yellow", 1.0: "green" }),
                areaBackground: $(go.Brush, go.Brush.Linear, { 0.0: "black", 1.0: "lightgray" }) })
));

结果:
shape