跳转至

本节代码片段需要导入以下模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from qgis.core import (
  QgsApplication,
  QgsDataSourceUri,
  QgsCategorizedSymbolRenderer,
  QgsClassificationRange,
  QgsPointXY,
  QgsProject,
  QgsExpression,
  QgsField,
  QgsFields,
  QgsFeature,
  QgsFeatureRequest,
  QgsFeatureRenderer,
  QgsGeometry,
  QgsGraduatedSymbolRenderer,
  QgsMarkerSymbol,
  QgsMessageLog,
  QgsRectangle,
  QgsRendererCategory,
  QgsRendererRange,
  QgsSymbol,
  QgsVectorDataProvider,
  QgsVectorLayer,
  QgsVectorFileWriter,
  QgsWkbTypes,
  QgsSpatialIndex,
  QgsVectorLayerUtils
)

from qgis.core.additions.edit import edit

from qgis.PyQt.QtGui import (
    QColor,
)

6 使用矢量图层⚓︎

本节总结了使用矢量图层执行各种操作。

这里的大部分工作都是基于QgsVectorLayer类的方法。

6.1 检索相关属性信息⚓︎

你可以通过调用QgsVectorLayer对象的fields()方法检索一个矢量图层相关字段的信息:

1
2
3
4
5
6
7
8
9
vlayer = QgsVectorLayer("testdata/airports.shp", "airports", "ogr")
for field in vlayer.fields():
    print(field.name(), field.typeName())

# ID Integer64
# fk_region Integer64
# ELEV Real
# NAME String
# USE String

displayField()mapTipTemplate() 方法提供有关显示属性选项卡中使用的字段和模板的信息。

加载矢量层时,QGIS始终选择一个字段作为显示名称,而HTML地图提示默认情况下为空。使用这些方法,可以轻松地同时获得:

1
2
3
vlayer = QgsVectorLayer("testdata/airports.shp", "airports", "ogr")
print(vlayer.displayField())
# NAME

提示

如果将显示名称从字段更改为表达式,则必须使用 displayExpression() 而不是 displayField()

6.2 遍历矢量图层⚓︎

遍历矢量图层要素是最常见的任务之一。下面是执行此任务的简单基本代码示例,并显示有关每个要素的一些信息。layer变量被假定为一个QgsVectorLayer对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
layer = iface.activeLayer()
features = layer.getFeatures()

for feature in features:
    # 检索每一个要素的几何和属性
    print("Feature ID: ", feature.id())
    # 获取几何
    geom = feature.geometry()
    geomSingleType = QgsWkbTypes.isSingleType(geom.wkbType())
    if geom.type() == QgsWkbTypes.PointGeometry:
        # 几何类型可以是单个或多个类型,
        if geomSingleType:
            x = geom.asPoint()
            print("Point: ", x)
        else:
            x = geom.asMultiPoint()
            print("MultiPoint: ", x)
    elif geom.type() == QgsWkbTypes.LineGeometry:
        if geomSingleType:
            x = geom.asPolyline()
            print("Line: ", x, "length: ", geom.length())
        else:
            x = geom.asMultiPolyline()
            print("MultiLine: ", x, "length: ", geom.length())
    elif geom.type() == QgsWkbTypes.PolygonGeometry:
        if geomSingleType:
            x = geom.asPolygon()
            print("Polygon: ", x, "Area: ", geom.area())
        else:
            x = geom.asMultiPolygon()
            print("MultiPolygon: ", x, "Area: ", geom.area())
    else:
        print("Unknown or invalid geometry")
    # 获取属性
    attrs = feature.attributes()
    # attrs是一个列表。它包含要素的所有属性值
    print(attrs)
    # 本测试只打印第一个要素
    break


    # Feature ID:  1
    # Point:  <QgsPointXY: POINT(7 45)>
    # [1, 'First feature']

6.3 选择要素⚓︎

在QGIS桌面中,可以通过不同方式选择要素:用户可以单击要素、在地图画布上绘制矩形或使用表达式过滤器。所选要素通常以不同颜色突出显示(默认为黄色),以引起用户对已选要素的注意。

有时,以编程方式选择要素或更改默认颜色会很有用。

选择所有要素,可以使用selectAll()方法:

1
2
3
# 获取当前图层(必须是矢量图层)
layer = iface.activeLayer()
layer.selectAll()

使用表达式进行选择,使用selectByExpression()方法:

1
2
3
4
# 假设当前图层是来自QGIS测试套件的points.shp文件
#(Class(字符串)和Heading(数字)是points.shp中的属性)
layer = iface.activeLayer()
layer.selectByExpression('"Class"=\'B52\' and "Heading" > 10 and "Heading" <70', QgsVectorLayer.SetSelection)

更改选择颜色,可以使用QgsMapCanvassetSelectionColor()方法 :

1
iface.mapCanvas().setSelectionColor(QColor("red"))

给图层的所选要素列表添加要素,你可以调用select()添加到要素ID列表:

1
2
3
4
5
6
7
8
selected_fid  =  []

# 获取图层的第一个要素ID 
feature = next(layer.getFeatures())
if feature:
    selected_fid.append(feature.id())
# 将这些要素添加到选定的列表
layer.select(selected_fid)

清除选择:

1
layer.removeSelection()

6.3.1 访问属性⚓︎

属性可以通过名称来获得:

1
2
3
print(feature['name'])

# First feature

或者,可以通过索引引用属性。这比使用名称快一点。例如,获取第二个属性:

1
2
3
print(feature[1])

# Second feature

6.3.2 遍历选中的要素⚓︎

如果你只需要选中的要素,则可以使用矢量图层的selectedFeatures()方法:

1
2
3
4
5
selection = layer.selectedFeatures()
print(len(selection))
for feature in selection:
    # 使用要素执行任何操作
    pass

6.3.3 遍历一部分要素⚓︎

如果要遍历图层中特定的要素子集(例如给定区域内的要素),则必须添加QgsFeatureRequest对象到getFeatures()方法。下面是一个例子:

1
2
3
4
5
6
7
areaOfInterest = QgsRectangle(450290,400520, 450750,400780)

request = QgsFeatureRequest().setFilterRect(areaOfInterest)

for feature in layer.getFeatures(request):
    # 使用要素执行任何操作
    pass

为了速度,相交通常仅使用要素的范围(bbox)来完成。但是有一个标志ExactIntersect可以确保只返回相交的要素:

1
request = QgsFeatureRequest().setFilterRect(areaOfInterest).setFlags(QgsFeatureRequest.ExactIntersect)

使用setLimit()你可以限制要素的数量。下面是一个例子:

1
2
3
4
5
request = QgsFeatureRequest()
request.setLimit(2)
for feature in layer.getFeatures(request):
    print(feature)
    # <qgis._core.QgsFeature object at 0x7f9b78590948>

如果你需要一个基于属性的过滤器来代替(或增加)一个空间过滤器,如上面的例子所示,你可以构建一个QgsExpression对象并将其传递给QgsFeatureRequest函数。下面是一个例子:

1
2
3
4
# 表达式将过滤字段“location_name” 
# 包含单词“Lake”(不区分大小写)
exp = QgsExpression("location_name ILIKE \'%Lake%\'")
request = QgsFeatureRequest(exp)

有关支持语法(QgsExpression)的详细信息,请参阅11-表达式,过滤和计算值

该请求可用于检索每个要素,因此迭代器返回所有要素,但返回每个要素的部分数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 仅返回选定的字段以增加请求速度
request.setSubsetOfAttributes([0,2])

# 更加友好的方式
request.setSubsetOfAttributes(['name','id'],layer.fields())

# 不返回几何对象以增加请求速度
request.setFlags(QgsFeatureRequest.NoGeometry)

# 仅获取ID为45的要素
request.setFilterFid(45)

# 这些选项可以是链式的
request.setFilterRect(areaOfInterest).setFlags(QgsFeatureRequest.NoGeometry).setFilterFid(45).setSubsetOfAttributes([0,2])

6.4 修改矢量图层⚓︎

大多数矢量数据提供者都支持编辑图层数据。有时它们仅支持编辑子集。使用capabilities()功能可以找出支持的功能集。

1
2
3
4
caps = layer.dataProvider().capabilities()
# 检查是否支持特定功能:
if caps & QgsVectorDataProvider.DeleteFeatures:
    print('图层支持删除要素')

有关所有可用功能的列表,请参阅 :QgsVectorDataProvider接口文档

打印图层功能的文本描述,结果是以逗号分隔的列表,你可以使用capabilitiesString() 方法,如下例所示:

1
2
3
4
5
6
caps_string = layer.dataProvider().capabilitiesString()
# Print:
# 'Add Features, Delete Features, Change Attribute Values, Add Attributes,
# Delete Attributes, Rename Attributes, Fast Access to Features at ID,
# Presimplify Geometries, Presimplify Geometries with Validity Check,
# Transactions, Curved Geometries'

通过使用以下任何方法进行矢量图层编辑,更改将直接提交到基础数据存储(文件,数据库等)。如果你只想进行临时修改,请跳到下一节6.4.4 使用编辑缓冲区修改矢量图层

提示

如果你在QGIS内部(从控制台或从插件中),可能需要强制重绘地图画布,以便查看你对几何、样式或属性所做的更改:

1
2
3
4
5
# 如果启用了缓存,简单的画布刷新可能不足以触发重绘,并且必须清除图层的缓存图像。
if iface.mapCanvas().isCachingEnabled():
    layer.triggerRepaint()
else:
    iface.mapCanvas().refresh()

6.4.1 添加要素⚓︎

创建一些QgsFeature实例并将它们的列表传递给提供者的 addFeatures()方法。它将返回两个值:result(true / false)和添加的要素列表(它们的ID由数据存储设置)。

设置要素的属性,可以通过传递QgsFields对象(可以从fields()矢量图层的方法获取 )或调用initAttributes()传递要添加的字段数来初始化要素。

1
2
3
4
5
6
7
8
if caps & QgsVectorDataProvider.AddFeatures:
    feat = QgsFeature(layer.fields())
    feat.setAttributes([0, 'hello'])
    # 或按key或index设置单个属性:
    feat.setAttribute('name', 'hello')
    feat.setAttribute(0, 'hello')
    feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(123, 456)))
    (res, outFeats) = layer.dataProvider().addFeatures([feat])

6.4.2 删除要素⚓︎

删除某些要素,只需提供其要素ID列表即可。

1
2
if caps & QgsVectorDataProvider.DeleteFeatures:
    res = layer.dataProvider().deleteFeatures([5, 10])

6.4.3 修改要素⚓︎

可以更改要素的几何图形或更改某些属性。以下示例首先更改索引为0和1的属性值,然后更改要素的几何。

1
2
3
4
5
6
7
8
9
fid = 100  # 我们将修改的要素ID

if caps & QgsVectorDataProvider.ChangeAttributeValues:
    attrs = { 0 : "hello", 1 : 123 }
    layer.dataProvider().changeAttributeValues({ fid : attrs })

if caps & QgsVectorDataProvider.ChangeGeometries:
    geom = QgsGeometry.fromPointXY(QgsPointXY(111,222))
    layer.dataProvider().changeGeometryValues({ fid : geom })

提示

QgsVectorLayerEditUtils类进行仅几何编辑

如果你只需要更改几何图形,可以考虑使用QgsVectorLayerEditUtils,它提供一些有用的方法来编辑几何图形(平移、插入或移动顶点等)。

6.4.4 使用编辑缓冲区修改矢量图层⚓︎

在QGIS应用程序中编辑矢量时,必须首先为特定图层设置开始编辑模式,然后进行一些修改,最后提交(或回滚)更改。你所做的所有更改在你提交之前都不会写入——它们保留在图层的内存编辑缓冲区中。也可以通过编程方式使用此功能——它仅仅是是矢量图层编辑的另一种方法,可以补充直接使用数据提供者。在为矢量图层编辑提供一些GUI工具时使用此选项,因为这将允许用户决定是否提交/回滚,并允许使用撤销/重做。提交更改后,编辑缓冲区中的所有更改都将保存到数据提供者中。

这些方法类似于我们在提供者中看到的方法,但它们在QgsVectorLayer 对象上调用。

使这些方法起作用,图层必须处于编辑模式。开始编辑模式,使用startEditing()方法。停止编辑,使用commitChanges()rollBack()方法。第一个方法将提交对数据源的所有更改,而第二个方法将丢弃它们,并且不会修改数据源。

确定图层是否处于编辑模式,使用isEditable()方法。

这里有一些示例演示,如何使用这些编辑方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from qgis.PyQt.QtCore import QMetaType

feat1 = feat2 = QgsFeature(layer.fields())
fid = 99
feat1.setId(fid)

# 添加两个要素(QgsFeature实例)
layer.addFeatures([feat1,feat2])
# 删除指定ID的要素 
layer.deleteFeature(fid)

# 为要素设置新几何(QgsGeometry实例)。
layer.changeGeometry(fid, geometry)
# 将给定字段索引(int)的属性更新为给定值
fieldIndex =1
value ='My new name'
layer.changeAttributeValue(fid, fieldIndex, value)

# 添加新的字段
layer.addAttribute(QgsField("mytext", QMetaType.Type.QString))
# 删除字段
layer.deleteAttribute(fieldIndex)

为了使撤消/重做正常工作,上述调用必须包含在撤消命令中。(如果你不关心撤消/重做并希望立即存储更改,那么通过6.4 修改矢量图层,你将可以更轻松地完成工作 。)

以下是使用撤消功能的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
layer.beginEditCommand("Feature triangulation")

# ...调用图层的编辑方法......

if problem_occurred:
  layer.destroyEditCommand()
  return

# ...更多编辑...

layer.endEditCommand()

beginEditCommand()方法将创建一个内部“活动”命令,并记录矢量图层中的后续更改。随着对endEditCommand() 命令的调用被推送到撤销栈,用户将能够从GUI撤消/重做它。如果在执行更改时出现问题, destroyEditCommand()方法将删除该命令并回滚此命令处于活动状态时所做的所有更改。

你还可以使用with edit(layer)——将提交和回滚包装在更具语义的代码块中,如下例所示:

1
2
3
4
with edit(layer):
  feat = next(layer.getFeatures())
  feat[0] = 5
  layer.updateFeature(feat)

结束后将自动调用commitChanges()。如果发生任何异常,它将进行rollBack()所有更改。如果commitChanges()(当该方法返回False时)遇到问题将引发QgsEditError异常。

6.4.5 添加和删除字段⚓︎

添加字段(属性),你需要指定字段定义列表。删除字段,只需提供字段索引列表。

1
2
3
4
5
6
7
8
9
from qgis.PyQt.QtCore import QMetaType

if caps & QgsVectorDataProvider.AddAttributes:
    res = layer.dataProvider().addAttributes(
        [QgsField("mytext", QMetaType.Type.QString),
        QgsField("myint", QMetaType.Type.Int)])

if caps & QgsVectorDataProvider.DeleteAttributes:
    res = layer.dataProvider().deleteAttributes([0])
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 删除字段的备用方法
# 首先创建要删除的临时字段 (f1-3)
layer.dataProvider().addAttributes([QgsField("f1", QMetaType.Type.Int),
                                    QgsField("f2", QMetaType.Type.Int),
                                    QgsField("f3", QMetaType.Type.Int)])
layer.updateFields()
count=layer.fields().count() # 图层字段个数
ind_list=list((count-3, count-2)) # 创建列表

# 使用索引删除字段
layer.dataProvider().deleteAttributes([count-1])

# 使用索引列表删除多个字段
layer.dataProvider().deleteAttributes(ind_list)

在数据提供程者中添加或删除字段后,需要更新图层的字段,因为更改不会自动传播。

1
layer.updateFields()

提示

使用with语句直接保存更改

使用with edit(layer): 更改将在结束后调用commitChanges()自动提交。如果发生任何异常,它将rollBack()所有更改。请参见6.4.4 使用编辑缓冲区修改矢量图层

6.5 使用空间索引⚓︎

如果需要对矢量图层进行频繁查询,空间索引可以显著提高代码的性能。例如,想象一下,你正在编写插值算法,并且对于给定位置你需要知道点图层中最近的10个点,以便使用这些点来计算插值。如果没有空间索引,QGIS找到这10个点的唯一方法是计算从每个点到指定位置的距离,然后比较这些距离。这可能是一项非常耗时的任务,特别是如果需要在多个位置重复这项任务。如果图层存在空间索引,则操作更有效。

可以将没有空间索引的图层视为电话簿,其中不对电话号码进行排序或索引。找到给定人员的电话号码的唯一方法是从头开始阅读,直到找到它为止。

默认情况下,QGIS矢量图层不会创建空间索引,但你可以轻松创建它们。这是你要做的:

1
index = QgsSpatialIndex()
  • 向索引添加要素——索引获取QgsFeature对象并将其添加到内部数据结构。你可以手动创建对象,也可以使用先前提供者的getFeatures()方法。
1
index.insertFeature(feat)
  • 或者,你可以批量加载图层的所有要素
1
index = QgsSpatialIndex(layer.getFeatures())
  • 一旦空间索引填充了一些值,你就可以进行一些查询
1
2
3
4
5
# 以数组形式返回五个最近要素的ID
nearest = index.nearestNeighbor(QgsPointXY(25.4, 12.7), 5)

# 以数组形式返回与矩形相交的要素
intersect = index.intersects(QgsRectangle(22.5, 15.3, 23.1, 17.2))

你可以使用QgsSpatialIndexKDBush空间索引。这个空间索引与标准空间库QgsSpatialIndex相似,但是有以下特性:

  • 支持支持单点要素
  • 静态 (构造后无法将其他要素添加到索引)
  • 更快
  • 允许直接检索原始要素的点,而无需其他要素请求
  • 支持基于真实距离的搜索,即返回搜索半径范围内的所有点

6.6 QgsVectorLayerUtils类⚓︎

QgsVectorLayerUtils 类包含一些非常有用的方法,你可以在矢量图层中使用。

例如, createFeature()方法准备将 QgsFeature添加到矢量层,并保留每个字段的所有最终约束和默认值

1
2
vlayer = QgsVectorLayer("testdata/airports.shp", "airports", "ogr")
feat = QgsVectorLayerUtils.createFeature(vlayer)

使用getValues()方法可以快速获取字段或表达式的值:

1
2
3
4
5
6
7
vlayer = QgsVectorLayer("testdata/airports.shp", "airports", "ogr")
# 只选择第一个要素
vlayer.selectByIds([1])
val = QgsVectorLayerUtils.getValues(vlayer, "NAME", selectedOnly=True)
print(val)

# (['AMBLER'], True)

6.7 创建矢量图层⚓︎

有几种方法可以生成矢量图层数据集:

  • QgsVectorFileWriter类:用于将矢量文件写入硬盘,通过静态调用writeAsVectorFormatV3()保存整个矢量图层,或创建该类的实例并调用addFeature()方法。该类支持 GDAL 支持的所有矢量格式(GeoPackage,Shapefile,GeoJSON,KML等)。
  • QgsVectorLayer类:实例化一个数据提供者,提供的数据源路径(url)以连接和访问数据。它可以用来创建临时的、基于内存存储的图层(memory),还可以连接到GDAL数据集( ogr),数据库(postgresspatialitemysqlmssql),更多(wfsgpxdelimitedtext...)。

6.7.1 从QgsVectorFileWriter实例创建⚓︎

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# SaveVectorOptions包含很多设置项
save_options = QgsVectorFileWriter.SaveVectorOptions()
transform_context = QgsProject.instance().transformContext()
# 写入GeoPackage (默认)
error = QgsVectorFileWriter.writeAsVectorFormatV3(layer,
                                                  "testdata/my_new_file.gpkg",
                                                  transform_context,
                                                  save_options)
if error[0] == QgsVectorFileWriter.NoError:
    print("success!")
else:
  print(error)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 使用utf-8编码写入ESRI Shapefile
save_options = QgsVectorFileWriter.SaveVectorOptions()
save_options.driverName = "ESRI Shapefile"
save_options.fileEncoding = "UTF-8"
transform_context = QgsProject.instance().transformContext()
error = QgsVectorFileWriter.writeAsVectorFormatV3(layer,
                                                  "testdata/my_new_shapefile",
                                                  transform_context,
                                                  save_options)
if error[0] == QgsVectorFileWriter.NoError:
    print("success again!")
else:
  print(error)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 写入ESRI GDB文件
save_options = QgsVectorFileWriter.SaveVectorOptions()
save_options.driverName = "FileGDB" # 译者注:默认情况下没有FileGDB驱动
# 如果没有几何
save_options.overrideGeometryType = QgsWkbTypes.Unknown
save_options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer
save_options.layerName = 'my_new_layer_name'
transform_context = QgsProject.instance().transformContext()
gdb_path = "testdata/my_example.gdb"
error = QgsVectorFileWriter.writeAsVectorFormatV3(layer,
                                                gdb_path,
                                                transform_context,
                                                save_options)
if error[0] == QgsVectorFileWriter.NoError:
  print("success!")
else:
  print(error)

你还可以使用 FieldValueConverter 转换字段使其与不同格式兼容。例如,要将数组变量类型(例如在Postgres中)转换为文本类型,可以执行以下操作 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
LIST_FIELD_NAME = 'xxxx'


class ESRIValueConverter(QgsVectorFileWriter.FieldValueConverter):

  def __init__(self, layer, list_field):
    QgsVectorFileWriter.FieldValueConverter.__init__(self)
    self.layer = layer
    self.list_field_idx = self.layer.fields().indexFromName(list_field)

  def convert(self, fieldIdxInLayer, value):
    if fieldIdxInLayer == self.list_field_idx:
      return QgsListFieldFormatter().representValue(layer=vlayer,
                                                    fieldIndex=self.list_field_idx,
                                                    config={},
                                                    cache=None,
                                                    value=value)
    else:
      return value

  def fieldDefinition(self, field):
    idx = self.layer.fields().indexFromName(field.name())
    if idx == self.list_field_idx:
      return QgsField(LIST_FIELD_NAME, QMetaType.Type.QString)
    else:
      return self.layer.fields()[idx]

converter = ESRIValueConverter(vlayer, LIST_FIELD_NAME)
opts = QgsVectorFileWriter.SaveVectorOptions()
opts.fieldValueConverter = converter

还可以指定目标CRS——如果将一个有效的QgsCoordinateReferenceSystem实例作为第四个参数,则将该图层转换为这个CRS。

有关有效的驱动程序的名称,请调用supportedFiltersAndFormats方法或查阅GDAL支持的格式——你应该将“Code”列中的值作为驱动程序名称传递。

(可选)你可以设置是仅导出选中的要素,传递更多驱动程序特定的选项进行创建,或者告诉数据写入类不要创建属性...还有许多其他(可选)参数; 请参阅QgsVectorFileWriter的详细信息

6.7.2 直接从要素创建⚓︎

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from qgis.PyQt.QtCore import QMetaType

# 为要素属性定义字段。需要QgsFields对象
fields = QgsFields()
fields.append(QgsField("first", QMetaType.Type.Int))
fields.append(QgsField("second", QMetaType.Type.QString))

"""
创建一个矢量文件编写器的实例,它将创建矢量文件
参数:
1. 新文件的路径(如果已存在则失败)
2. 字段映射
3. 几何类型 - WKBTYPE枚举
4. 图层的空间参考(QgsCoordinateReferenceSystem的实例)
5. 坐标转换上下文
6. 输出选项(驱动名称,编码等)
"""

crs = QgsProject.instance().crs()
transform_context = QgsProject.instance().transformContext()
save_options = QgsVectorFileWriter.SaveVectorOptions()
save_options.driverName = "ESRI Shapefile"
save_options.fileEncoding = "UTF-8"

writer = QgsVectorFileWriter.create(
  "testdata/my_new_shapefile.shp",
  fields,
  QgsWkbTypes.Point,
  crs,
  transform_context,
  save_options
)

if writer.hasError() != QgsVectorFileWriter.NoError:
    print("Error when creating shapefile: ",  writer.errorMessage())

# 添加一个要素
fet = QgsFeature()

fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10,10)))
fet.setAttributes([1, "text"])
writer.addFeature(fet)

# 删除writer写入到磁盘
del writer

6.7.3 从QgsVectorLayer实例创建⚓︎

QgsVectorLayer类支持的所有数据提供者中,让我们关注基于内存的图层。内存提供者主要供插件或第三方应用程序开发人员使用。它不会将数据存储在磁盘中,允许开发人员快速在后台使用临时图层。

提供者支持string,int和double字段。

内存提供者还支持空间索引,通过调用提供者的createSpatialIndex()方法来启用。创建空间索引后,你将能够更快地迭代较小区域内的要素(因为没有必要遍历所有要素,只遍历指定矩形内的要素)。

通过将"memory"作为QgsVectorLayer构造函数的参数来创建内存提供者。

这个构造函数也需要定义图层几何类型的URI,包括:"Point""LineString""Polygon""MultiPoint""MultiLineString""MultiPolygon" or "None"

URI还可以指定坐标参考系统、字段和索引。语法是:

  • crs=definition

指定坐标参考系统,其中定义可以是接受的任何形式 QgsCoordinateReferenceSystem.createFromString()

  • index=yes

指定提供者将使用空间索引

  • field=name:type(length,precision)

指定图层的属性。该属性具有名称,可选的类型(integer, double, or string),长度和精度。可能有多个字段定义。

以下URI示例包含所有这些选项

1
"Point?crs=epsg:4326&field=id:integer&field=name:string(20)&index=yes"

以下示例代码说明了如何创建和填充内存提供者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from qgis.PyQt.QtCore import QMetaType


# 创建图层
vl = QgsVectorLayer("Point", "temporary_points", "memory")
pr = vl.dataProvider()


# 添加字段
pr.addAttributes([QgsField("name", QMetaType.Type.QString),
                    QgsField("age",  QMetaType.Type.Int),
                    QgsField("size", QMetaType.Type.Double)])
vl.updateFields() # 告诉矢量图层从提供者获取更改

# 添加一个要素
fet = QgsFeature()
fet.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10,10)))
fet.setAttributes(["Johnny", 2, 0.3])
pr.addFeatures([fet])

# 在添加新要素时更新图层的范围,因为提供者中的范围更改不会传播到图层
vl.updateExtents()

最后,让我们检查一切是否顺利

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 显示一些统计
print("fields:", len(pr.fields()))
print("features:", pr.featureCount())
e = vl.extent()
print("extent:", e.xMinimum(), e.yMinimum(), e.xMaximum(), e.yMaximum())

# 遍历要素
features = vl.getFeatures()
for fet in features:
    print("F:", fet.id(), fet.attributes(), fet.geometry().asPoint())

# fields: 3
# features: 1
# extent: 10.0 10.0 10.0 10.0
# F: 1 ['Johny', 2, 0.3] <QgsPointXY: POINT(10 10)>

6.8 矢量图层的外观(符号系统)⚓︎

渲染矢量图层时,数据的外观由 渲染器 和与图层相关联的 符号 提供 。符号是一个类——负责绘制要素的可视化,而渲染器确定用于特定要素的符号。

获得图层的渲染器,如下所示:

1
renderer = layer.renderer()

有了这个参考,让我们来探讨一下

1
2
print("Type:", renderer.type())
# Type: singleSymbol

QGIS核心库中有几种已知的渲染器类型:

类型 描述
singleSymbol QgsSingleSymbolRenderer 使用相同的符号呈现所有要素
categorizedSymbol QgsCategorizedSymbolRenderer 使用每个类别的不同符号呈现要素
graduatedSymbol QgsGraduatedSymbolRenderer 为每个值范围使用不同的符号呈现要素

可能还有一些自定义渲染器类型,所以永远不要假设只有这些类型。你可以查询QgsApplicationQgsRendererRegistry方法查找当前可用的渲染器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
print(QgsApplication.rendererRegistry().renderersList())
# Print:
['nullSymbol',
'singleSymbol',
'categorizedSymbol',
'graduatedSymbol',
'RuleRenderer',
'pointDisplacement',
'pointCluster',
'invertedPolygonRenderer',
'heatmapRenderer',
'25dRenderer']

可以以文本形式获取渲染器的内容——可用于调试

1
2
print(renderer.dump())
# SINGLE: MARKER SYMBOL (1 layers) color 190,207,80,255

6.8.1 单一符号渲染器⚓︎

你可以通过调用symbol()方法获取用于渲染的符号,使用setSymbol()方法更改它(C ++开发人员注意:渲染器将获取符号的所有权。)

你可以通过调用setSymbol()并传递适当的符号实例来更改矢量图层的符号。线多边形图层的符号可以通过调用相应的类QgsMarkerSymbolQgsLineSymbolQgsFillSymbolcreateSimple()方法来创建。

给传递createSimple()的字典参数来设置符号的样式属性。

例如,你可以通过调用setSymbol()并传递QgsMarkerSymbol实例, 来替换特定 图层的符号,如下面的代码示例所示:

1
2
3
4
symbol = QgsMarkerSymbol.createSimple({'name': 'square', 'color': 'red'})
layer.renderer().setSymbol(symbol)
# 显示更改
layer.triggerRepaint()

name 表示符号的形状,可以是以下任何一种:

  • circle
  • square
  • cross
  • rectangle
  • diamond
  • pentagon
  • triangle
  • equilateral_triangle
  • star
  • regular_star
  • arrow
  • filled_arrowhead
  • x

获取符号实例的第一个符号图层的完整属性列表,可以按照以下示例代码进行操作:

1
2
3
print(layer.renderer().symbol().symbolLayers()[0].properties())
# Prints
{'angle': '0', 'cap_style': 'square', 'color': '255,0,0,255,rgb:1,0,0,1', 'horizontal_anchor_point': '1', 'joinstyle': 'bevel', 'name': 'square', 'offset': '0,0', 'offset_map_unit_scale': '3x:0,0,0,0,0,0', 'offset_unit': 'MM', 'outline_color': '35,35,35,255,rgb:0.13725490196078433,0.13725490196078433,0.13725490196078433,1', 'outline_style': 'solid', 'outline_width': '0', 'outline_width_map_unit_scale': '3x:0,0,0,0,0,0', 'outline_width_unit': 'MM', 'scale_method': 'diameter', 'size': '2', 'size_map_unit_scale': '3x:0,0,0,0,0,0', 'size_unit': 'MM', 'vertical_anchor_point': '1'}

如果要更改某些属性,这可能很有用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 你可以更改单个属性... 
layer.renderer().symbol().symbolLayer(0).setSize(3)
# ...但并非所有属性都可以从方法访问,
# 你也可以完全替换符号:
props = layer.renderer().symbol().symbolLayer(0).properties()
props['color'] = 'yellow'
props['name'] = 'square'
layer.renderer().setSymbol(QgsMarkerSymbol.createSimple(props))
# 显示更改
layer.triggerRepaint()

6.8.2 分类符号渲染器⚓︎

使用分类渲染器时,可以查询和设置用于分类的属性:使用 classAttribute()setClassAttribute()方法。

获取类别列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
categorized_renderer = QgsCategorizedSymbolRenderer()
# 添加一些类别
cat1 = QgsRendererCategory('1', QgsMarkerSymbol(), 'category 1')
cat2 = QgsRendererCategory('2', QgsMarkerSymbol(), 'category 2')
categorized_renderer.addCategory(cat1)
categorized_renderer.addCategory(cat2)

for cat in categorized_renderer.categories():
    print("{}: {} :: {}".format(cat.value(), cat.label(), cat.symbol()))

# 1: category 1 :: <qgis._core.QgsMarkerSymbol object at 0x7f378ffcd9d8>
# 2: category 2 :: <qgis._core.QgsMarkerSymbol object at 0x7f378ffcd9d8>

其中value()是类别之间用于区别的值, label()是用于类别描述的文本,symbol()方法返回所分配的符号。

渲染器通常还存储用于分类的原始符号和色带:sourceColorRamp()sourceSymbol()方法。

6.8.3 渐变符号渲染器⚓︎

此渲染器与上面描述的分类符号渲染器非常相似,但它不是每个类的一个属性值,而是使用值范围,因此只能用于数字属性。

了解有关渲染器中使用范围的更多信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
graduated_renderer = QgsGraduatedSymbolRenderer()
# 添加一些类别
graduated_renderer.addClassRange(QgsRendererRange(QgsClassificationRange('class 0-100', 0, 100), QgsMarkerSymbol()))
graduated_renderer.addClassRange(QgsRendererRange(QgsClassificationRange('class 101-200', 101, 200), QgsMarkerSymbol()))

for ran in renderer.ranges():
    print("{} - {}: {} {}".format(
        ran.lowerValue(),
        ran.upperValue(),
        ran.label(),
        ran.symbol()
      ))
# 0.0 - 100.0: class 0-100 <qgis._core.QgsMarkerSymbol object at 0x7f8bad281b88>
# 101.0 - 200.0: class 101-200 <qgis._core.QgsMarkerSymbol object at 0x7f8bad281b88>

你可以再次使用 classAttribute() (查找分类属性名称) sourceSymbol()sourceColorRamp()方法。此外,还有一种mode()方法可以确定范围的创建方式:使用等间隔,分位数或其他方法。

如果你希望创建自己的渐变符号渲染器,则可以执行此操作,如下面的示例代码段所示(这将创建一个简单的两个类别)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from qgis.PyQt import QtGui

myVectorLayer = QgsVectorLayer(myVectorPath, myName, 'ogr')
myTargetField = 'target_field'
myRangeList = []
myOpacity = 1
# 创建我们的第一符号和范围... 
myMin = 0.0
myMax = 50.0
myLabel = 'Group 1'
myColour = QtGui.QColor('#ffee00')
mySymbol1 = QgsSymbol.defaultSymbol(myVectorLayer.geometryType())
mySymbol1.setColor(myColour)
mySymbol1.setOpacity(myOpacity)
myRange1 = QgsRendererRange(myMin, myMax, mySymbol1, myLabel)
myRangeList.append(myRange1)
# 现在创建另一个符号和范围... 
myMin = 50.1
myMax = 100
myLabel = 'Group 2'
myColour = QtGui.QColor('#00eeff')
mySymbol2 = QgsSymbol.defaultSymbol(
     myVectorLayer.geometryType())
mySymbol2.setColor(myColour)
mySymbol2.setOpacity(myOpacity)
myRange2 = QgsRendererRange(myMin, myMax, mySymbol2, myLabel)
myRangeList.append(myRange2)
myRenderer = QgsGraduatedSymbolRenderer('', myRangeList)
myRenderer.setMode(QgsGraduatedSymbolRenderer.EqualInterval)
myRenderer.setClassAttribute(myTargetField)

myVectorLayer.setRenderer(myRenderer)
QgsProject.instance().addMapLayer(myVectorLayer)

6.8.4 使用符号⚓︎

对于符号的表示,QgsSymbol基类有三个派生类:

每个符号由一个或多个符号图层 (从QgsSymbolLayer派生的类)。符号图层执行实际渲染,符号类本身仅用作符号图层的容器。

拥有一个符号实例(例如来自渲染器),可以探索它:type()方法说明它是标记、线还是填充符号。dump() 方法可以返回符号的简短描述。获取符号图层列表:

1
2
3
4
5
6
marker_symbol = QgsMarkerSymbol()
for i in range(marker_symbol.symbolLayerCount()):
    lyr = marker_symbol.symbolLayer(i)
    print("{}: {}".format(i, lyr.layerType()))

# 0: SimpleMarker

找出符号的颜色使用color()方法,setColor()改变其颜色。使用标记符号,你还可以使用size()angle()方法查询符号大小和旋转。对于线符号,width()方法返回线宽。

默认情况下,大小和宽度以毫米为单位,角度以度为单位。

6.8.4.1 使用符号图层⚓︎

如前所述,符号层(QgsSymbolLayer的子类)决定要素的外观。有一些基本的符号图层类用于一般用途。可以实现新的符号图层类型,任意定制要素的呈现方式。layerType() 方法唯一标识符号图层类——基本类和默认类SimpleMarkerSimpleLine以及SimpleFill符号图层类型。

你可以使用以下代码获取可以为符号图层创建符号图层类型的完整列表:

1
2
3
4
5
from qgis.core import QgsSymbolLayerRegistry
myRegistry = QgsApplication.symbolLayerRegistry()
myMetadata = myRegistry.symbolLayerMetadata("SimpleFill")
for item in myRegistry.symbolLayersForType(QgsSymbol.Marker):
    print(item)

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
AnimatedMarker
EllipseMarker
FilledMarker
FontMarker
GeometryGenerator
MaskMarker
RasterMarker
SimpleMarker
SvgMarker
VectorField

QgsSymbolLayerRegistry类管理一个所有可用符号层类型的数据库。

访问符号图层数据,使用properties()方法返回属性的键值字典,该字典决定外观。每个符号图层类型都有一组特定的属性。此外,还有通用的方法color()size()angle()width()。当然,尺寸和角度仅适用于标记符号图层,宽度适用于线符号图层。

6.8.4.2 创建自定义符号图层类型⚓︎

想象一下,你想要自定义数据的呈现方式。你可以创建自己的符号图层类,完全按照你的意愿绘制要素。以下是绘制具有指定半径的红色圆圈的标记示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from qgis.core import QgsMarkerSymbolLayer
from qgis.PyQt.QtGui import QColor

class FooSymbolLayer(QgsMarkerSymbolLayer):

  def __init__(self, radius=4.0):
      QgsMarkerSymbolLayer.__init__(self)
      self.radius = radius
      self.color = QColor(255,0,0)

  def layerType(self):
     return "FooMarker"

  def properties(self):
      return { "radius" : str(self.radius) }

  def startRender(self, context):
    pass

  def stopRender(self, context):
      pass

  def renderPoint(self, point, context):
      # 渲染取决于是否选择了符号 (QGIS >= 1.5)
      color = context.selectionColor() if context.selected() else self.color
      p = context.renderContext().painter()
      p.setPen(color)
      p.drawEllipse(point, self.radius, self.radius)

  def clone(self):
      return FooSymbolLayer(self.radius)

layerType()方法确定符号图层的名称,它必须在所有符号层中是唯一的。properties()方法用于属性的持久化。clone() 方法必须返回符号图层的副本,其中所有属性完全相同。最后,渲染方法: startRender()在渲染第一个要素之前被调用,stopRender() 渲染完成时被调用,renderPoint()渲染时被调用。点的坐标已经转换为输出坐标。

对于线和多边形,唯一的区别在于渲染方法:你将使用 renderPolyline()接收线列表,renderPolygon()接收外环上的点列表作为第一个参数和内环列表(或None)作为第二个参数。

通常可以便利地添加用于设置符号图层类型属性的GUI,以允许用户自定义外观:在上面的示例中,我们可以让用户设置圆半径。以下代码实现了这样的控件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from qgis.gui import QgsSymbolLayerWidget

class FooSymbolLayerWidget(QgsSymbolLayerWidget):
    def __init__(self, parent=None):
        QgsSymbolLayerWidget.__init__(self, parent)

        self.layer = None

        # 设置简单的UI 
        self.label = QLabel("Radius:")
        self.spinRadius = QDoubleSpinBox()
        self.hbox = QHBoxLayout()
        self.hbox.addWidget(self.label)
        self.hbox.addWidget(self.spinRadius)
        self.setLayout(self.hbox)
        self.connect(self.spinRadius, SIGNAL("valueChanged(double)"), \
            self.radiusChanged)

    def setSymbolLayer(self, layer):
        if layer.layerType() != "FooMarker":
            return
        self.layer = layer
        self.spinRadius.setValue(layer.radius)

    def symbolLayer(self):
        return self.layer

    def radiusChanged(self, value):
        self.layer.radius = value
        self.emit(SIGNAL("changed()"))

此窗口控件可以嵌入到符号属性对话框中。在符号属性对话框中选择符号图层类型时,它会创建符号图层的实例和符号图窗口控件的实例。然后它调用setSymbolLayer()方法将符号图层分配给窗口控件。在该方法中,控件应该更新UI以反映符号层的属性。symbolLayer()方法用于通过属性对话框再次检索符号图层,将其用于符号。

在每次更改属性时,窗口控件都应发出changed()信号,让属性对话框更新符号预览。

现在我们只缺少最后的粘合剂:让QGIS了解这些新类。这是通过将符号图层添加到注册表来完成的。也可以在不将其添加到注册表的情况下使用符号图层,但某些功能不起作用:例如,使用自定义符号图层加载项目文件或无法在GUI中编辑图层的属性。

我们必须为符号图层创建元数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from qgis.core import QgsSymbol, QgsSymbolLayerAbstractMetadata, QgsSymbolLayerRegistry


class FooSymbolLayerMetadata(QgsSymbolLayerAbstractMetadata):

    def __init__(self):
        QgsSymbolLayerAbstractMetadata.__init__(self, "FooMarker", QgsSymbol.Marker)

    def createSymbolLayer(self, props):
        radius = float(props["radius"]) if "radius" in props else 4.0
        return FooSymbolLayer(radius)

    def createSymbolLayer(self, props):
        radius = float(props["radius"]) if "radius" in props else 4.0
        return FooSymbolLayer(radius)


fslmetadata = FooSymbolLayerMetadata()
QgsApplication.symbolLayerRegistry().addSymbolLayerType(fslmetadata)

你应该将图层类型(与图层返回的相同)和符号类型(marker/line/fill)传递给父类的构造函数。createSymbolLayer()方法负责使用props字典中指定的属性创建符号图层的实例。createSymbolLayerWidget()方法可以返回此符号图层类型的设置控件。

最后一步是将此符号图层添加到注册表中——我们完成了。

6.8.5 创建自定义渲染器⚓︎

如果要自定义如何选择符号呈现要素的规则,则创建新的渲染器可能很有用。你可能希望做一些用例:符号由字段组合确定,符号大小根据当前比例而变化等。

下面的代码显示了一个简单的自定义渲染器,它可以创建两个标记符号,并为每个要素随机选择其中一个

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import random
from qgis.core import QgsWkbTypes, QgsSymbol, QgsFeatureRenderer


class RandomRenderer(QgsFeatureRenderer):
  def __init__(self, syms=None):
    QgsFeatureRenderer.__init__(self, "RandomRenderer")
    self.syms = syms if syms else [QgsSymbol.defaultSymbol(QgsWkbTypes.geometryType(QgsWkbTypes.Point))]

  def symbolForFeature(self, feature):
    return random.choice(self.syms)

  def startRender(self, context, vlayer):
    for s in self.syms:
      s.startRender(context)

  def stopRender(self, context):
    for s in self.syms:
      s.stopRender(context)

  def usedAttributes(self):
    return []

  def clone(self):
    return RandomRenderer(self.syms)

父类QgsFeatureRenderer 的构造函数需要一个渲染器名称(在渲染器中必须是唯一的)。symbolForFeature()方法决定什么符号用于特定的要素。 startRender()stopRender()负责符号渲染的初始化/完成。usedAttributes() 方法返回渲染器的字段名称列表。最后,clone()方法应返回渲染器的副本。

与符号图层一样,可以附加GUI配置渲染器,它必须来源于QgsRendererWidget。以下示例代码创建一个允许用户设置第一个符号的按钮

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from qgis.gui import QgsRendererWidget, QgsColorButton


class RandomRendererWidget(QgsRendererWidget):
    def __init__(self, layer, style, renderer):
        QgsRendererWidget.__init__(self, layer, style)
        if renderer is None or renderer.type() != "RandomRenderer":
            self.r = RandomRenderer()
        else:
            self.r = renderer
        # setup UI
        self.btn1 = QgsColorButton()
        self.btn1.setColor(self.r.syms[0].color())
        self.vbox = QVBoxLayout()
        self.vbox.addWidget(self.btn1)
        self.setLayout(self.vbox)
        self.connect(self.btn1, SIGNAL("clicked()"), self.setColor1)

    def setColor1(self):
        color = QColorDialog.getColor(self.r.syms[0].color(), self)
        if not color.isValid(): return
        self.r.syms[0].setColor(color)
        self.btn1.setColor(self.r.syms[0].color())

    def renderer(self):
        return self.r

构造函数接收当前图层(QgsVectorLayer),全局样式(QgsStyle)和当前渲染器的实例。如果没有渲染器或渲染器具有不同的类型,它将被我们的新渲染器替换,否则我们将使用当前渲染器(已经是我们需要的类型)。应该更新窗口控件内容以显示渲染器的当前状态。当接受渲染器对话框时,将调用窗口控件的renderer()方法获取当前渲染器——它将被分配给该图层。

最后一个缺失的是渲染器元数据和注册表中的注册项,否则使用渲染器加载图层将不起作用,用户将无法从渲染器列表中选择它。RandomRenderer示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from qgis.core import QgsRendererAbstractMetadata, QgsRendererRegistry, QgsApplication


class RandomRendererMetadata(QgsRendererAbstractMetadata):
    def __init__(self):
        QgsRendererAbstractMetadata.__init__(self, "RandomRenderer", "Random renderer")

    def createRenderer(self, element):
        return RandomRenderer()

    def createRendererWidget(self, layer, style, renderer):
        return RandomRendererWidget(layer, style, renderer)


rrmetadata = RandomRendererMetadata()
QgsApplication.rendererRegistry().addRenderer(rrmetadata)

与符号图层类似,抽象元数据构造函数等待渲染器名称,对用户可见的名称以及渲染器图标的可选名称。createRenderer() 方法传递一个QDomElement实例,该实例可用于从DOM树恢复渲染器的状态。createRendererWidget() 方法创建配置控件。如果渲染器没有GUI,它不必存在或可以返回None

要将图标与渲染器关联,可以在QgsRendererAbstractMetadata 构造函数中将其指定为第三个(可选)参数

1
2
3
4
QgsRendererAbstractMetadata.__init__(self,
       "RandomRenderer",
       "Random renderer",
       QIcon(QPixmap("RandomRendererIcon.png", "png")))

也可以使用元数据类的setIcon方法关联该图标。图标可以从文件加载(如上所示),也可以从Qt资源加载 (PyQt5包含Python的.qrc编译文件)。

6.9 更多话题⚓︎

TODO:

  • 创建/修改符号
  • 使用样式(QgsStyle
  • 使用色带(QgsColorRamp
  • 探索符号图层和渲染器注册表