├── README.md ├── docs ├── assets │ ├── infobar.png │ ├── bar-button.png │ ├── errorbar.png │ ├── 1559181577361.png │ ├── dialog-with-bar.png │ ├── errorbar-timed.png │ ├── QgsAuthConfigSelect.png │ ├── QgsAuthEditorWidgets.png │ └── QgsAuthAuthoritiesEditor.png ├── index.md ├── css │ └── img.css ├── images │ ├── logo.svg │ └── logo-blue.svg ├── 18-使用插件图层.md ├── 17-编写处理插件.md ├── 12-读取和存储设置.md ├── 2-加载项目.md ├── 8-投影支持.md ├── 11-表达式,过滤和计算值.md ├── 4-访问图层目录树.md ├── 13-与用户通信.md ├── 5-使用栅格图层.md ├── 1-引言.md ├── 10-地图渲染和打印.md ├── 7-几何处理.md ├── 14-认证基础.md ├── 3-加载图层.md ├── 9-使用地图画布.md ├── 15-任务——在后台做繁重的工作.md ├── 19-网络分析库.md ├── 21-PyQGIS速查表.md ├── 16-开发Python插件.md └── 20-QGIS服务器和Python.md ├── .github ├── dependabot.yml └── workflows │ └── mkdocs.yml └── mkdocs.yml /README.md: -------------------------------------------------------------------------------- 1 | # PyQGIS开发者手册 2 | 3 | 本项目是PyQGIS开发者手册的中文翻译。 4 | -------------------------------------------------------------------------------- /docs/assets/infobar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/infobar.png -------------------------------------------------------------------------------- /docs/assets/bar-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/bar-button.png -------------------------------------------------------------------------------- /docs/assets/errorbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/errorbar.png -------------------------------------------------------------------------------- /docs/assets/1559181577361.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/1559181577361.png -------------------------------------------------------------------------------- /docs/assets/dialog-with-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/dialog-with-bar.png -------------------------------------------------------------------------------- /docs/assets/errorbar-timed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/errorbar-timed.png -------------------------------------------------------------------------------- /docs/assets/QgsAuthConfigSelect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/QgsAuthConfigSelect.png -------------------------------------------------------------------------------- /docs/assets/QgsAuthEditorWidgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/QgsAuthEditorWidgets.png -------------------------------------------------------------------------------- /docs/assets/QgsAuthAuthoritiesEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luolingchun/PyQGIS-Developer-Cookbook-cn/HEAD/docs/assets/QgsAuthAuthoritiesEditor.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # PyQGIS开发者手册 2 | 3 | 本项目是PyQGIS开发者手册的中文翻译。 4 | 5 | 如果您在阅读过程中发现任何问题,[欢迎指正](https://github.com/luolingchun/PyQGIS-Developer-Cookbook-cn/issues/new)。 6 | -------------------------------------------------------------------------------- /docs/css/img.css: -------------------------------------------------------------------------------- 1 | 2 | p img:not(#c) { 3 | box-shadow: 0 0 10px #333743f0; 4 | /* p标签img居中 */ 5 | display: block; 6 | margin: 0 auto; 7 | } 8 | 9 | .md-header { 10 | background: linear-gradient(to right, transparent, rgba(2, 80, 197, 0.9) 0%, rgba(212, 63, 141, 0.9) 100%); 11 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/logo-blue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: mkdocs 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v6 27 | - uses: actions/setup-python@v6 28 | with: 29 | python-version: 3.x 30 | - run: pip install mkdocs-material jieba 31 | - run: mkdocs gh-deploy --force 32 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: PyQGIS开发者手册 2 | site_url: https://luolingchun.github.io/PyQGIS-Developer-Cookbook-cn 3 | site_description: 本项目是PyQGIS开发者手册的中文翻译。 4 | site_author: llc 5 | 6 | repo_url: https://github.com/luolingchun/PyQGIS-Developer-Cookbook-cn 7 | repo_name: 'Github' 8 | 9 | markdown_extensions: 10 | - admonition 11 | - toc: 12 | permalink: ⚓︎ 13 | - md_in_html 14 | - pymdownx.magiclink 15 | - pymdownx.superfences 16 | - pymdownx.snippets 17 | - pymdownx.highlight: 18 | linenums: true 19 | 20 | 21 | theme: 22 | name: material 23 | language: 'zh' 24 | logo: images/logo.svg 25 | favicon: images/logo-blue.svg 26 | palette: 27 | - media: "(prefers-color-scheme: light)" 28 | scheme: default 29 | toggle: 30 | icon: material/weather-sunny 31 | name: 黑 32 | - media: "(prefers-color-scheme: dark)" 33 | scheme: slate 34 | toggle: 35 | icon: material/weather-night 36 | name: 亮 37 | features: 38 | - navigation.tracking 39 | - navigation.footer 40 | - navigation.top 41 | # - navigation.expand 42 | - search.suggest 43 | - search.highlight 44 | - search.share 45 | - content.code.copy 46 | - content.action.edit 47 | - content.action.view 48 | - toc.follow 49 | 50 | plugins: 51 | - search 52 | 53 | extra_css: 54 | - css/img.css 55 | 56 | nav: 57 | - 主页: index.md 58 | - 1-引言: 1-引言.md 59 | - 2-加载项目: 2-加载项目.md 60 | - 3-加载图层: 3-加载图层.md 61 | - 4-访问图层目录树: 4-访问图层目录树.md 62 | - 5-使用栅格图层: 5-使用栅格图层.md 63 | - 6-使用矢量图层: 6-使用矢量图层.md 64 | - 7-几何处理: 7-几何处理.md 65 | - 8-投影支持: 8-投影支持.md 66 | - 9-使用地图画布: 9-使用地图画布.md 67 | - 10-地图渲染和打印: 10-地图渲染和打印.md 68 | - 11-表达式,过滤和计算值: 11-表达式,过滤和计算值.md 69 | - 12-读取和存储设置: 12-读取和存储设置.md 70 | - 13-与用户通信: 13-与用户通信.md 71 | - 14-认证基础: 14-认证基础.md 72 | - 15-任务 - 在后台做繁重的工作: 15-任务——在后台做繁重的工作.md 73 | - 16-开发Python插件: 16-开发Python插件.md 74 | - 17-编写处理插件: 17-编写处理插件.md 75 | - 18-使用插件图层: 18-使用插件图层.md 76 | - 19-网络分析库: 19-网络分析库.md 77 | - 20-QGIS服务器和Python: 20-QGIS服务器和Python.md 78 | - 21-PyQGIS速查表: 21-PyQGIS速查表.md 79 | -------------------------------------------------------------------------------- /docs/18-使用插件图层.md: -------------------------------------------------------------------------------- 1 | 本节代码片段需导入以下模块: 2 | 3 | ```python 4 | from qgis.core import ( 5 | QgsPluginLayer, 6 | QgsPluginLayerType, 7 | QgsMapLayerRenderer, 8 | QgsApplication, 9 | QgsProject, 10 | ) 11 | 12 | from qgis.PyQt.QtGui import QImage 13 | ``` 14 | 15 | # 18 使用插件图层 16 | 17 | 如果你的插件使用自己的方法渲染图层,基于[`QgsPluginLayer`](https://qgis.org/pyqgis/master/core/QgsPluginLayer.html#qgis.core.QgsPluginLayer)编写你自己的图层类型是最有效的方式。 18 | 19 | ## 18.1 子类化QgsPluginLayer 20 | 21 | 下面例子是`QgsPluginLayer`的最小实现。它是基于[Watermark example plugin](https://github.com/sourcepole/qgis-watermark-plugin)的原版实现。 22 | 23 | 自定义渲染器是实现画布中实际图形工具的一部分。 24 | 25 | ```python 26 | class WatermarkLayerRenderer(QgsMapLayerRenderer): 27 | 28 | def __init__(self, layerId, rendererContext): 29 | super().__init__(layerId, rendererContext) 30 | 31 | def render(self): 32 | image = QImage("/usr/share/icons/hicolor/128x128/apps/qgis.png") 33 | painter = self.renderContext().painter() 34 | painter.save() 35 | painter.drawImage(10, 10, image) 36 | painter.restore() 37 | return True 38 | 39 | 40 | class WatermarkPluginLayer(QgsPluginLayer): 41 | LAYER_TYPE = "watermark" 42 | 43 | def __init__(self): 44 | super().__init__(WatermarkPluginLayer.LAYER_TYPE, "Watermark plugin layer") 45 | self.setValid(True) 46 | 47 | def createMapRenderer(self, rendererContext): 48 | return WatermarkLayerRenderer(self.id(), rendererContext) 49 | 50 | def setTransformContext(self, ct): 51 | pass 52 | 53 | # 可以添加读写项目特定信息的方法 54 | 55 | def readXml(self, node, context): 56 | pass 57 | 58 | def writeXml(self, node, doc, context): 59 | pass 60 | 61 | ``` 62 | 63 | 可以将插件图层添加到项目中并作为任何其它图层添加到画布中: 64 | 65 | ```python 66 | plugin_layer = WatermarkPluginLayer() 67 | QgsProject.instance().addMapLayer(plugin_layer) 68 | ``` 69 | 70 | 加载包含此类图层的项目时,需要工厂类: 71 | 72 | ```python 73 | class WatermarkPluginLayerType(QgsPluginLayerType): 74 | 75 | def __init__(self): 76 | super().__init__(WatermarkPluginLayer.LAYER_TYPE) 77 | 78 | def createLayer(self): 79 | return WatermarkPluginLayer() 80 | 81 | # 你可以添加GUI代码来自定义显示图层的属性信息 82 | def showLayerProperties(self, layer): 83 | pass 84 | 85 | 86 | # 保持引用该实例,防止被垃圾回收 87 | plt = WatermarkPluginLayerType() 88 | 89 | assert QgsApplication.pluginLayerRegistry().addPluginLayerType(plt) 90 | ``` 91 | 92 | -------------------------------------------------------------------------------- /docs/17-编写处理插件.md: -------------------------------------------------------------------------------- 1 | # 17 编写处理插件 2 | 3 | 取决于你将开发的插件类型,它可能是更好的选择—作为处理算法(或者算法集合)去增加功能。这将在QGIS中提供给更好的集成,额外的功能(它可以运行在处理组件中,如建模或批处理界面运行),和更快的开发时间(处理将花费的很大一部分工作)。 4 | 5 | 为了分发这些算法,你应该创建一个新的插件,并将它们添加到处理工具箱。该插件应该包含一个算法提供者,它在插件实例化时进行注册。 6 | 7 | ## 17.1 从头开始创建 8 | 9 | 从头开始创建一个插件,它包含一个算法提供者,你可以使用插件构造器,按照下列步骤操作: 10 | 11 | 1. 安装`Plugin Builder`插件 12 | 2. 使用`Plugin Builder`创建一个新的插件。当`Plugin Builder`要求你使用模板时,选择“Processing provider”。 13 | 3. 创建的插件包含一个算法提供者。无论是提供者文件和算法文件都被完全注释,并包含有关如何修改提供者,并添加额外的算法的信息。参考它们以获取更多信息。 14 | 15 | ## 17.2 更新插件 16 | 17 | 如果你想添加你现有的插件到“Processing”,你需要添加一些代码。 18 | 19 | 1. 在你的`metadata.txt`,你需要添加一个变量: 20 | 21 | ```bash 22 | hasProcessingProvider=yes 23 | ``` 24 | 25 | 2. 在Python文件中,插件使用`initGui`方法安装,你需要改写这样的例子: 26 | 27 | ```python 28 | from qgis.core import QgsApplication 29 | from .processing_provider import Provider 30 | 31 | class YourPluginName(): 32 | 33 | def __init__(self): 34 | self.provider = None 35 | 36 | def initProcessing(self): 37 | self.provider = Provider() 38 | QgsApplication.processingRegistry().addProvider(self.provider) 39 | 40 | def initGui(self): 41 | self.initProcessing() 42 | 43 | def unload(self): 44 | QgsApplication.processingRegistry().removeProvider(self.provider) 45 | ``` 46 | 47 | 3. 你可以创建一个`processing_provider`文件夹,其中包含三个文件 48 | 49 | - `__init__.py`没有任何东西。这是Python包所必需的。 50 | 51 | - `provider.py` 这将创建处理提供者并公开你的算法。 52 | 53 | ```python 54 | from qgis.core import QgsProcessingProvider 55 | from .example_processing_algorithm import ExampleProcessingAlgorithm 56 | 57 | class Provider(QgsProcessingProvider): 58 | 59 | def loadAlgorithms(self, *args, **kwargs): 60 | self.addAlgorithm(ExampleProcessingAlgorithm()) 61 | # 在这里添加其他算法 62 | # self.addAlgorithm(MyOtherAlgorithm()) 63 | 64 | def id(self, *args, **kwargs): 65 | """插件的ID,用于标识提供者。 66 | 67 | 该字符串应该是唯一的,短的,仅字符串,例如“qgis”或“gdal”。此字符串不应被翻译(localised)。 68 | """ 69 | return 'yourplugin' 70 | 71 | def name(self, *args, **kwargs): 72 | """插件的人性化名称。 73 | 74 | 该字符串应尽可能短(例如“Lastools”,而不是"Lastools version 1.0.1 64-bit"),并且需要翻译(localised)。 75 | """ 76 | return self.tr('Your plugin') 77 | 78 | def icon(self): 79 | """应该返回用于处理工具箱中提供者的QIcon。 80 | """ 81 | return QgsProcessingProvider.icon(self) 82 | ``` 83 | 84 | - `example_processing_algorithm.py`,其中包含示例算法文件。复制/粘贴[模板文件](https://github.com/qgis/QGIS/blob/master/python/plugins/processing/script/ScriptTemplate.py)中的内容,并根据需要更新它。 85 | 86 | 4. 现在,你可以在QGIS中重新加载插件,你应该可以在处理工具箱和模型中看到你的示例脚本。 87 | 88 | -------------------------------------------------------------------------------- /docs/12-读取和存储设置.md: -------------------------------------------------------------------------------- 1 | # 12 读取和存储设置 2 | 3 | 此页上的代码段需要导入以下模块: 4 | 5 | ```python 6 | from qgis.core import ( 7 | QgsProject, 8 | QgsSettings, 9 | QgsVectorLayer 10 | ) 11 | ``` 12 | 13 | 很多时候,插件保存一些变量非常有用,这样用户下次运行插件时就不必再输入或选择它们。 14 | 15 | 借助Qt和QGIS API可以保存和检索这些变量。对于每个变量,你应该选择一个用于访问变量的键——对于用户喜欢的颜色,你可以使用键“favourite_color”或任何其他有意义的字符串。建议为键的命名提供一些结构。 16 | 17 | 我们可以区分几种类型的设置: 18 | 19 | - **全局设置:** 它们绑定到特定计算机上的用户。QGIS本身存储了许多全局设置,例如,主窗口大小或默认捕捉容差。使用[`QgsSettings`](https://qgis.org/pyqgis/master/core/QgsSettings.html#qgis.core.QgsSettings)类处理设置,该类提供[`setValue()`](https://qgis.org/pyqgis/master/core/QgsSettings.html#qgis.core.QgsSettings.setValue)和[`value()`](https://qgis.org/pyqgis/master/core/QgsSettings.html#qgis.core.QgsSettings.value)方法 20 | 21 | 在这里,你可以看到如何使用这些方法的示例: 22 | 23 | ```python 24 | def store(): 25 | s = QgsSettings() 26 | s.setValue("myplugin/mytext", "hello world") 27 | s.setValue("myplugin/myint", 10) 28 | s.setValue("myplugin/myreal", 3.14) 29 | 30 | def read(): 31 | s = QgsSettings() 32 | mytext = s.value("myplugin/mytext", "default text") 33 | myint = s.value("myplugin/myint", 123) 34 | myreal = s.value("myplugin/myreal", 2.71) 35 | nonexistent = s.value("myplugin/nonexistent", None) 36 | print(mytext) 37 | print(myint) 38 | print(myreal) 39 | print(nonexistent) 40 | ``` 41 | 42 | [`value()`](https://qgis.org/pyqgis/master/core/QgsSettings.html#qgis.core.QgsSettings.value)方法的第二个参数是可选的,如果没有,则返回默认值。 43 | 44 | 通过`qgis_global_settings.ini`文件在全局设置中预配置默认值,更多详情查看[Deploying QGIS within an organization](https://docs.qgis.org/testing/en/docs/user_manual/introduction/qgis_configuration.html#deploying-organization) 45 | 46 | - **项目设置:** 在不同项目之间有所不同,因此它们与项目文件相关联。以地图画布背景颜色或目标坐标参考系统(CRS)作为例子——白色背景和WGS84可能适用于一个项目,而黄色背景和UTM投影适用于另一个项目。 47 | 48 | 以下是一个用法示例: 49 | 50 | ```python 51 | proj = QgsProject.instance() 52 | 53 | # 存储值 54 | proj.writeEntry("myplugin", "mytext", "hello world") 55 | proj.writeEntry("myplugin", "myint", 10) 56 | proj.writeEntry("myplugin", "mydouble", 0.01) 57 | proj.writeEntry("myplugin", "mybool", True) 58 | 59 | # 读取值(返回具有值的元组,和一个布尔状态,检索到的值是否可以转换到它的类型, string, an integer, a double and a boolean) 60 | mytext, type_conversion_ok = proj.readEntry("myplugin", "mytext", "default text") 61 | myint, type_conversion_ok = proj.readNumEntry("myplugin", "myint", 123) 62 | mydouble, type_conversion_ok = proj.readDoubleEntry("myplugin", "mydouble", 123) 63 | mybool, type_conversion_ok = proj.readBoolEntry("myplugin", "mybool", 123) 64 | ``` 65 | 如你所见,[`writeEntry()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.writeEntry)方法用于许多数据类型(integer, string, list),但有几种方法可以读取设置值,而且必须为每种数据类型选择相应的一个。 66 | 67 | - **地图图层设置**:这些设置与项目的图层实例相关。它们不与图层的基础数据源连接,因此如果你创建两个shapefile图层实例,则它们不会共享设置。设置存储在项目文件中,因此如果用户再次打开项目,则与图层相关的设置将再次存在。使用[`customProperty()`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.customProperty)方法检索给定设置的值,并可使用[`setCustomProperty()`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.setCustomProperty)进行设置。 68 | 69 | ```python 70 | vlayer = QgsVectorLayer() 71 | # 保存值 72 | vlayer.setCustomProperty("mytext", "hello world") 73 | 74 | # 再次读取(如果没有找到,则返回“default text”) 75 | mytext = vlayer.customProperty("mytext", "default text") 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /docs/2-加载项目.md: -------------------------------------------------------------------------------- 1 | 本节的代码片段需要导入以下模块: 2 | 3 | ```python 4 | from qgis.core import ( 5 | Qgis, 6 | QgsProject, 7 | QgsPathResolver 8 | ) 9 | 10 | from qgis.gui import ( 11 | QgsLayerTreeMapCanvasBridge, 12 | ) 13 | ``` 14 | 15 | # 2 加载项目 16 | 17 | 有时你需要从插件加载现有项目,或者(更常见)在开发独立的QGIS Python应用程序时加载(请参阅:[Python应用程序](1-引言.md#14-python))。 18 | 19 | 将项目加载到当前QGIS应用程序中,需要创建[`QgsProject`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)类的实例。这是一个单例类,因此你必须使用其[`instance()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.instance)方法来执行此操作。你可以调用[`read()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.read)方法,传递加载项目的路径: 20 | 21 | ```python 22 | # 如果你不在QGIS控制台内运行,首先需要导入qgis和PyQt类,如下所示: 23 | from qgis.core import QgsProject 24 | # 获取项目实例 25 | project = QgsProject.instance() 26 | # 打印当前项目的文件名(可能为空,因为没有项目加载) 27 | # print(project.fileName()) 28 | 29 | # 加载另一个项目 30 | project.read('testdata/01_project.qgs') 31 | print(project.fileName()) 32 | # testdata/01_project.qgs 33 | ``` 34 | 35 | 如果你需要对项目进行修改(例如添加或删除某些图层)并保存更改,调用[`write()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.write)方法。该方法还支持将项目保存到新的位置: 36 | 37 | ```python 38 | # 将项目保存到同一个文件 39 | project.write() 40 | # ...或新文件 41 | project.write('testdata/my_new_qgis_project.qgs') 42 | ``` 43 | 44 | `read()`和`write()`函数都返回一个布尔值,你可以使用它来检查操作是否成功。 45 | 46 | !!! 提示 47 | 48 | 如果你正在编写QGIS独立应用程序,为了将加载的项目与画布同步,你需要实例化[`QgsLayerTreeMapCanvasBridge`](https://qgis.org/pyqgis/master/gui/QgsLayerTreeMapCanvasBridge.html#qgis.gui.QgsLayerTreeMapCanvasBridge),如下所示: 49 | 50 | ```python 51 | bridge = QgsLayerTreeMapCanvasBridge(QgsProject.instance().layerTreeRoot(), canvas) 52 | # 现在你可以安全地加载项目,并在画布上看到它 53 | project.read('testdata/my_new_qgis_project.qgs') 54 | ``` 55 | 56 | ## 2.1 解决错误路径 57 | 58 | 在项目中加载的图层可能被移动到另一个位置。当项目再次加载时,所有的图层路径都被破坏。[`QgsPathResolver`](https://qgis.org/pyqgis/master/core/QgsPathResolver.html#qgis.core.QgsPathResolver)类帮助你在项目中重写图层路径。 59 | 60 | `setPathPreprocessor()`方法允许设置一个自定义的路径预处理函数,它允许在将路径和数据源解析为文件引用或图层源之前对它们进行操作。 61 | 62 | 该处理函数必须接受一个单一的字符串参数(代表原始文件路径或数据源),并返回该路径的处理版本。 63 | 64 | 路径预处理器函数在任何错误图层处理程序之前被调用。如果设置了多个预处理器,将根据最初设置的顺序依次调用它们。 65 | 66 | 一些应用案例: 67 | 68 | 1. 替换过时的路径: 69 | 70 | ```python 71 | def my_processor(path): 72 | return path.replace('c:/Users/ClintBarton/Documents/Projects', 'x:/Projects/') 73 | 74 | QgsPathResolver.setPathPreprocessor(my_processor) 75 | ``` 76 | 77 | 2. 用一个新的数据库主机地址来替换: 78 | 79 | ```python 80 | def my_processor(path): 81 | return path.replace('host=10.1.1.115', 'host=10.1.1.116') 82 | 83 | QgsPathResolver.setPathPreprocessor(my_processor) 84 | ``` 85 | 86 | 3. 替换新的数据库证书: 87 | 88 | ```python 89 | def my_processor(path): 90 | path= path.replace("user='gis_team'", "user='team_awesome'") 91 | path = path.replace("password='cats'", "password='g7as!m*'") 92 | return path 93 | 94 | QgsPathResolver.setPathPreprocessor(my_processor) 95 | ``` 96 | 97 | 98 | 同样,路径编写器函数也可以使用[`setPathWriter()`](https://qgis.org/pyqgis/master/core/QgsPathResolver.html#qgis.core.QgsPathResolver.setPathWriter)方法。 99 | 100 | 使用变量替换路径的示例: 101 | 102 | ```python 103 | def my_processor(path): 104 | return path.replace('c:/Users/ClintBarton/Documents/Projects', '$projectdir$') 105 | 106 | QgsPathResolver.setPathWriter(my_processor) 107 | ``` 108 | 109 | 这两种方法都返回一个`id`,可用于删除它们添加的预处理器或写入程序。查看[`removePathPreprocessor()`](https://qgis.org/pyqgis/master/core/QgsPathResolver.html#qgis.core.QgsPathResolver.removePathPreprocessor)和[`removePathWriter()`](https://qgis.org/pyqgis/master/core/QgsPathResolver.html#qgis.core.QgsPathResolver.removePathWriter)。 110 | 111 | ## 2.2 使用标识符 112 | 113 | 在某些情况下,你可能不需要使用功能齐全的项目,而只是出于特定原因想要访问它,标识可能会有所帮助。完整的标识列表可在[`ProjectReadFlag`](https://qgis.org/pyqgis/master/core/Qgis.html#qgis.core.Qgis.ProjectReadFlag)下找到。可以将多个标志添加在一起。 114 | 115 | 例如,如果我们不关心实际的图层和数据,而只是想访问项目(例如布局或3D视图设置),则可以使用`DontResolveLayers`标识绕过数据验证步骤并防止出现无效图层对话框。可以执行以下操作: 116 | 117 | ```python 118 | readflags = Qgis.ProjectReadFlags() 119 | readflags |= Qgis.ProjectReadFlag.DontResolveLayers 120 | project = QgsProject().instance() 121 | project.read('C:/Users/ClintBarton/Documents/Projects/mysweetproject.qgs', readflags) 122 | ``` 123 | 124 | 添加更多标志,必须使用 python 按位或运算符 (|)。 -------------------------------------------------------------------------------- /docs/8-投影支持.md: -------------------------------------------------------------------------------- 1 | # 8 投影支持 2 | 3 | 此页面上的代码片段需要导入以下模块: 4 | 5 | ```python 6 | from qgis.core import ( 7 | QgsCoordinateReferenceSystem, 8 | QgsCoordinateTransform, 9 | QgsProject, 10 | QgsPointXY, 11 | ) 12 | ``` 13 | 14 | ## 8.1 坐标参考系统 15 | 16 | 坐标参考系统(CRS)由[`QgsCoordinateReferenceSystem`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem) 类封装 。可以通过几种不同的方式创建此类的实例: 17 | 18 | - ID 19 | 20 | ```python 21 | # WGS84:4326 22 | crs = QgsCoordinateReferenceSystem("EPSG:4326") 23 | print(crs.isValid()) 24 | # True 25 | ``` 26 | 27 | QGIS支持不同的CRS识别符,支持以下格式: 28 | 29 | - `EPSG:`——EPSG组织分配的ID——使用 [`createFromOgcWms()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromOgcWmsCrs) 30 | - `POSTGIS:`——PostGIS数据库使用的ID——使用[`createFromSrid()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromSrid) 31 | - `INTERNAL:`——QGIS内部数据库中使用的ID,使用[`createFromSrsId()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromSrsId) 32 | - `PROJ:`——使用[`createFromProj()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromProj) 33 | - `WKT:`——使用[`createFromWkt()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.createFromWkt) 34 | 35 | 如果未指定前缀,则默认使用WKT定义。 36 | 37 | - 通过WKT指定CRS 38 | 39 | ```python 40 | wkt = 'GEOGCS["WGS84", DATUM["WGS84", SPHEROID["WGS84", 6378137.0, 298.257223563]],' \ 41 | 'PRIMEM["Greenwich", 0.0], UNIT["degree",0.017453292519943295],' \ 42 | 'AXIS["Longitude",EAST], AXIS["Latitude",NORTH]]' 43 | crs = QgsCoordinateReferenceSystem(wkt) 44 | print(crs.isValid()) 45 | # True 46 | ``` 47 | 48 | - 创建一个无效的CRS,然后使用其中一个`create*`函数进行初始化。在下面的示例中,我们使用Proj字符串初始化投影。 49 | 50 | ```python 51 | crs = QgsCoordinateReferenceSystem() 52 | crs.createFromProj("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs") 53 | print(crs.isValid()) 54 | # True 55 | ``` 56 | 57 | 检查CRS的创建(即在数据库中查找)是否成功是明智的:[`isValid()`](https://qgis.org/pyqgis/master/core/QgsCoordinateReferenceSystem.html#qgis.core.QgsCoordinateReferenceSystem.isValid)必须返回`True`。 58 | 59 | 请注意,对于空间参考系统的初始化,QGIS需要在其内部数据库`srs.db`中查找适当的值。因此,如果你创建一个独立的应用程序,你需要使用[`QgsApplication.setPrefixPath()`](https://qgis.org/pyqgis/master/core/QgsApplication.html#qgis.core.QgsApplication.setPrefixPath)正确设置路径 ,否则将无法找到数据库。如果你在QGIS Python控制台中运行命令或开发插件,则无需关注:一切都已经为你准备好了。 60 | 61 | 访问空间参考系统信息: 62 | 63 | ```python 64 | crs = QgsCoordinateReferenceSystem(4326) 65 | 66 | print("QGIS CRS ID:", crs.srsid()) 67 | print("PostGIS SRID:", crs.postgisSrid()) 68 | print("Description:", crs.description()) 69 | print("Projection Acronym:", crs.projectionAcronym()) 70 | print("Ellipsoid Acronym:", crs.ellipsoidAcronym()) 71 | print("Proj4 String:", crs.toProj4()) 72 | # 检查是地理坐标系统还是投影坐标系统 73 | print("Is geographic:", crs.isGeographic()) 74 | # 检查CRS的地图单位类型(在QGis::units枚举中定义) 75 | print("Map units:", crs.mapUnits()) 76 | ``` 77 | 78 | 输出: 79 | 80 | ```python 81 | QGIS CRS ID: 3452 82 | PostGIS SRID: 4326 83 | Description: WGS 84 84 | Projection Acronym: longlat 85 | Ellipsoid Acronym: WGS84 86 | Proj4 String: +proj=longlat +datum=WGS84 +no_defs 87 | Is geographic: True 88 | Map units: DistanceUnit.Degrees 89 | ``` 90 | 91 | ## 8.2 坐标参考系统转换 92 | 93 | 你可以使用[`QgsCoordinateTransform`](https://qgis.org/pyqgis/master/core/QgsCoordinateTransform.html#qgis.core.QgsCoordinateTransform)类在不同的空间参考系之间进行转换。使用它的最简单方法是创建原始和目标CRS,并在当前项目中构造[`QgsCoordinateTransform`](https://qgis.org/pyqgis/master/core/QgsCoordinateTransform.html#qgis.core.QgsCoordinateTransform)实例。然后只需反复调用 [`transform()`](https://qgis.org/pyqgis/master/core/QgsCoordinateTransform.html#qgis.core.QgsCoordinateTransform.transform)函数进行转换。默认情况下,它会正向转换,但也可以逆向转换。 94 | 95 | ```python 96 | crsSrc = QgsCoordinateReferenceSystem(4326) # WGS 84 97 | crsDest = QgsCoordinateReferenceSystem(32633) # WGS 84 / UTM zone 33N 98 | xform = QgsCoordinateTransform(crsSrc, crsDest, QgsProject.instance()) 99 | 100 | # 正向转换: src -> dest 101 | pt1 = xform.transform(QgsPointXY(18,5)) 102 | print("Transformed point:", pt1) 103 | 104 | # 逆向转换: dest -> src 105 | pt2 = xform.transform(pt1, QgsCoordinateTransform.ReverseTransform) 106 | print("Transformed back:", pt2) 107 | ``` 108 | 109 | 输出: 110 | 111 | ```python 112 | Transformed point: 113 | Transformed back: 114 | ``` 115 | -------------------------------------------------------------------------------- /docs/11-表达式,过滤和计算值.md: -------------------------------------------------------------------------------- 1 | # 11 表达式,过滤和计算值 2 | 3 | 此页面上的代码段需要导入以下模块: 4 | 5 | ```python 6 | from qgis.core import ( 7 | edit, 8 | QgsExpression, 9 | QgsExpressionContext, 10 | QgsFeature, 11 | QgsFeatureRequest, 12 | QgsField, 13 | QgsFields, 14 | QgsVectorLayer, 15 | QgsPointXY, 16 | QgsGeometry, 17 | QgsProject, 18 | QgsExpressionContextUtils 19 | ) 20 | ``` 21 | 22 | QGIS支持解析类似SQL的表达式。仅支持一小部分SQL语法。表达式可以作为布尔值(返回True或False)或作为函数(返回标量值)来计算。有关可用功能的完整列表,请参阅“用户手册”中的“ [表达式”](https://docs.qgis.org/latest/en/docs/user_manual/working_with_vector/expression.html#vector-expressions)。 23 | 24 | 支持三种基本类型: 25 | 26 | - 数字——整数和小数,例如`123`,`3.14` 27 | - 字符串——它们必须用单引号括起来: `'hello world'` 28 | - 列引用——在评估时,引用将替换为字段的实际值。名称不会被转义。 29 | 30 | 可以使用以下操作: 31 | 32 | - 算术运算符:`+`,`-`,`*`,`/`,`^` 33 | - 括号:用于强制运算符优先级: `(1 + 1) * 3` 34 | - 一元加减:`-12`,`+5` 35 | - 数学函数:`sqrt`,`sin`,`cos`,`tan`,`asin`, `acos`,`atan` 36 | - 转换函数:`to_int`,`to_real`,`to_string`,`to_date` 37 | - 几何函数:`$area`,`$length` 38 | - 几何处理函数:`$x`,`$y`,`$geometry`,`num_geometries`,`centroid` 39 | 40 | 支持以下运算: 41 | 42 | - 比较:`=`,`!=`,`>`,`>=`,`<`,`<=` 43 | - 模式匹配:`LIKE`(使用%和_),`~`(正则表达式) 44 | - 逻辑谓词:`AND`,`OR`,`NOT` 45 | - NULL值检查:,`IS NULL`,`IS NOT NULL` 46 | 47 | 示例: 48 | 49 | - `1 + 2 = 3` 50 | - `sin(angle) > 0` 51 | - `'Hello' LIKE 'He%'` 52 | - `(x > 10 AND y > 10) OR z = 0` 53 | 54 | 标量表达式的示例: 55 | 56 | - `2 ^ 10` 57 | - `sqrt(val)` 58 | - `$length + 1` 59 | 60 | ## 11.1 解析表达式 61 | 62 | 下面的例子展示了如何检查一个表达式是否能被正确解析: 63 | 64 | ```python 65 | exp = QgsExpression('1 + 1 = 2') 66 | assert(not exp.hasParserError()) 67 | 68 | exp = QgsExpression('1 + 1 = ') 69 | assert(exp.hasParserError()) 70 | 71 | assert(exp.parserErrorString() == '\nsyntax error, unexpected end of file') 72 | ``` 73 | 74 | ## 11.2 评估表达式 75 | 76 | 表达式可以在不同的情况下使用,例如过滤要素或计算新的字段值。在任何情况下,表达式都必须被评估。这意味着它的值是通过执行指定的计算步骤计算出来的,计算步骤可以从简单的算术到集合表达式。 77 | 78 | ### 11.2.1 基本表达式 79 | 80 | 此基本表达式代表一个简单的算术运算: 81 | 82 | ```python 83 | exp = QgsExpression('2 * 3') 84 | print(exp) 85 | print(exp.evaluate()) 86 | 87 | # 88 | # 6 89 | ``` 90 | 91 | 表达式也可用于比较,1为真,0为假 92 | 93 | ```python 94 | exp = QgsExpression('1 + 1 = 2') 95 | exp.evaluate() 96 | 97 | # 1 98 | ``` 99 | 100 | ### 11.2.2 要素表达式 101 | 102 | 要对一个要素进行表达式评估,必须创建一个[`QgsExpressionContext`](https://qgis.org/pyqgis/master/core/QgsExpressionContext.html#qgis.core.QgsExpressionContext)对象,并将其传递给`evaluation`函数,以允许表达式访问该要素的字段值。 103 | 104 | 下面的示例展示了如何创建一个名为 "Column "字段的要素,以及如何将该要素添加到表达式上下文中。 105 | 106 | ```python 107 | fields = QgsFields() 108 | field = QgsField('Column') 109 | fields.append(field) 110 | feature = QgsFeature() 111 | feature.setFields(fields) 112 | feature.setAttribute(0, 99) 113 | 114 | exp = QgsExpression('"Column"') 115 | context = QgsExpressionContext() 116 | context.setFeature(feature) 117 | exp.evaluate(context) 118 | # 99 119 | ``` 120 | 121 | 下面是一个比较完整的例子,说明如何在矢量图层的上下文中使用表达式,以计算新的字段值: 122 | 123 | ```python 124 | from qgis.PyQt.QtCore import QMetaType 125 | 126 | # 创建矢量图层 127 | vl = QgsVectorLayer("Point", "Companies", "memory") 128 | pr = vl.dataProvider() 129 | pr.addAttributes([QgsField("Name", QMetaType.Type.QString), 130 | QgsField("Employees", QMetaType.Type.Int), 131 | QgsField("Revenue", QMetaType.Type.Double), 132 | QgsField("Rev. per employee", QMetaType.Type.Double), 133 | QgsField("Sum", QMetaType.Type.Double), 134 | QgsField("Fun", QMetaType.Type.Double)]) 135 | vl.updateFields() 136 | 137 | # 将数据添加到前三个字段 138 | my_data = [ 139 | {'x': 0, 'y': 0, 'name': 'ABC', 'emp': 10, 'rev': 100.1}, 140 | {'x': 1, 'y': 1, 'name': 'DEF', 'emp': 2, 'rev': 50.5}, 141 | {'x': 5, 'y': 5, 'name': 'GHI', 'emp': 100, 'rev': 725.9}] 142 | 143 | for rec in my_data: 144 | f = QgsFeature() 145 | pt = QgsPointXY(rec['x'], rec['y']) 146 | f.setGeometry(QgsGeometry.fromPointXY(pt)) 147 | f.setAttributes([rec['name'], rec['emp'], rec['rev']]) 148 | pr.addFeature(f) 149 | 150 | vl.updateExtents() 151 | QgsProject.instance().addMapLayer(vl) 152 | 153 | # 第一个表达式计算每个员工的收入 154 | # 第二个计算图层中所有收入值的总和。 155 | # 第三个表达式并没有什么意义,但是说明了我们可以在表达式中使用各种表达式函数(例如area和buffer): 156 | expression1 = QgsExpression('"Revenue"/"Employees"') 157 | expression2 = QgsExpression('sum("Revenue")') 158 | expression3 = QgsExpression('area(buffer($geometry,"Employees"))') 159 | 160 | # QgsExpressionContextUtils.globalProjectLayerScopes()是一个方便函数,可一次添加全局,项目和图层范围。另外,这些范围也可以手动添加。无论如何,重要的是始终从“最通用”到“最具体”的范围,即从全局到项目再到图层 161 | context = QgsExpressionContext() 162 | context.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(vl)) 163 | 164 | with edit(vl): 165 | for f in vl.getFeatures(): 166 | context.setFeature(f) 167 | f['Rev. per employee'] = expression1.evaluate(context) 168 | f['Sum'] = expression2.evaluate(context) 169 | f['Fun'] = expression3.evaluate(context) 170 | vl.updateFeature(f) 171 | 172 | print( f['Sum']) 173 | 174 | # 876.5 175 | ``` 176 | 177 | ### 11.2.3 表达式过滤图层 178 | 179 | 以下示例可用于过滤图层并返回与谓词匹配的所有要素: 180 | 181 | ```python 182 | layer = QgsVectorLayer("Point?field=Test:integer", 183 | "addfeat", "memory") 184 | 185 | layer.startEditing() 186 | 187 | for i in range(10): 188 | feature = QgsFeature() 189 | feature.setAttributes([i]) 190 | assert(layer.addFeature(feature)) 191 | layer.commitChanges() 192 | 193 | expression = 'Test >= 3' 194 | request = QgsFeatureRequest().setFilterExpression(expression) 195 | 196 | matches = 0 197 | for f in layer.getFeatures(request): 198 | matches += 1 199 | 200 | print(matches) 201 | 202 | # 7 203 | ``` 204 | 205 | ## 11.3 处理异常错误 206 | 207 | 在表达式解析或评估期间,可能发生表达相关的错误: 208 | 209 | ```python 210 | exp = QgsExpression("1 + 1 = 2 ") 211 | if exp.hasParserError(): 212 | raise Exception(exp.parserErrorString()) 213 | 214 | value = exp.evaluate() 215 | if exp.hasEvalError(): 216 | raise ValueError(exp.evalErrorString()) 217 | ``` 218 | -------------------------------------------------------------------------------- /docs/4-访问图层目录树.md: -------------------------------------------------------------------------------- 1 | # 4 访问图层目录树 2 | 3 | 此页面上的代码片段需要导入以下模块: 4 | 5 | ```python 6 | from qgis.core import ( 7 | QgsProject, 8 | QgsVectorLayer, 9 | ) 10 | ``` 11 | 12 | ## 4.1 QgsProject类 13 | 14 | 可以使用[QgsProject](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)检索有关目录和所有已加载图层的信息。 15 | 16 | 必须创建[QgsProject](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)的实例`instance`,并使用其方法(函数)来获取已加载的图层。 17 | 18 | [mapLayers](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.mapLayers)方法返回已加载图层的字典: 19 | 20 | ```python 21 | layers = QgsProject.instance().mapLayers() 22 | print(layers) 23 | 24 | # {'countries_89ae1b0f_f41b_4f42_bca4_caf55ddbe4b6': } 25 | ``` 26 | 27 | 字典的`keys`是图层的唯一id,而`values`是图层的对象。 28 | 29 | 现在直接去获得有关图层的任何其他信息: 30 | 31 | ```python 32 | # 使用列表表达式获得图层名称 33 | l = [layer.name() for layer in QgsProject.instance().mapLayers().values()] 34 | # 键值对存储图层名称和图层对象 35 | layers_list = {} 36 | for l in QgsProject.instance().mapLayers().values(): 37 | layers_list[l.name()] = l 38 | 39 | print(layers_list) 40 | 41 | # {'countries': } 42 | ``` 43 | 44 | 还可以使用图层名称查询目录: 45 | 46 | ```python 47 | country_layer = QgsProject.instance().mapLayersByName("countries")[0] 48 | ``` 49 | 50 | !!! 提示 51 | 52 | 返回所有匹配图层的列表,因此我们使用索引[0]获取第一个图层。 53 | 54 | 55 | ## 4.2 QgsLayerTreeGroup类 56 | 57 | 图层树是由节点构建的经典树结构。当前有两种类型的节点:图层组节点([QgsLayerTreeGroup](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup))和图层节点([QgsLayerTreeLayer](https://qgis.org/pyqgis/master/core/QgsLayerTreeLayer.html#qgis.core.QgsLayerTreeLayer))。 58 | 59 | !!! 提示 60 | 61 | 更多信息请访问Martin Dobias的[博客](https://www.lutraconsulting.co.uk/blog/2014/07/06/qgis-layer-tree-api-part-1/)。 62 | 63 | 64 | 可以使用[QgsProject](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)类的[layerLayerRoot()](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.layerTreeRoot)方法轻松访问项目图层树: 65 | 66 | ```python 67 | root = QgsProject.instance().layerTreeRoot() 68 | ``` 69 | 70 | `root`是一个图层组节点,具有子节点: 71 | 72 | ```python 73 | root.children() 74 | ``` 75 | 76 | 返回直接子节点列表。子组节点可以从他们自己的直接父级访问。 77 | 78 | 我们可以检索其中一个子节点: 79 | 80 | ```python 81 | child0 = root.children()[0] 82 | print(child0) 83 | 84 | # 85 | ``` 86 | 87 | 可以使用它们的唯一`id`来检索: 88 | 89 | ```python 90 | ids = root.findLayerIds() 91 | # 访问第一个图层 92 | root.findLayer(ids[0]) 93 | ``` 94 | 95 | 可以使用图层组名称检索: 96 | 97 | ```python 98 | root.findGroup('Group Name') 99 | ``` 100 | 101 | [QgsLayerTreeGroup](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup)还有许多其他有用的方法,可用于获取有关目录树的更多信息: 102 | 103 | ```python 104 | # 获得所有已选图层 105 | checked_layers = root.checkedLayers() 106 | print(checked_layers) 107 | 108 | # [] 109 | ``` 110 | 111 | 现在,我们把一些图层添加到项目的图层树中,有两种方法可以做到: 112 | 113 | 1. **显式添加:** 使用[addLayer()](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup.addLayer)或者[insertLayer()](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup.insertLayer)方法: 114 | 115 | ```python 116 | # 创建临时图层 117 | layer1 = QgsVectorLayer("path_to_layer", "Layer 1", "memory") 118 | # 添加图层到图层树末尾 119 | root.addLayer(layer1) 120 | # 插入图层到指定位置 121 | root.insertLayer(5, layer1) 122 | ``` 123 | 124 | 2. **隐式添加:** 由于项目的图层树已连接到图层注册表,因此可以在图层注册表中添加图层: 125 | 126 | ```python 127 | QgsProject.instance().addMapLayer(layer1) 128 | ``` 129 | 130 | 你可以轻松地在[QgsVectorLayer](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer)和[QgsLayerTreeLayer](https://qgis.org/pyqgis/master/core/QgsLayerTreeLayer.html#qgis.core.QgsLayerTreeLayer)之间切换: 131 | 132 | ```python 133 | node_layer = root.findLayer(country_layer.id()) 134 | print("Layer node:", node_layer) 135 | print("Map layer:", node_layer.layer()) 136 | 137 | # Layer node: 138 | # Map layer: 139 | ``` 140 | 141 | 可以使用[addGroup()](https://qgis.org/pyqgis/master/core/QgsLayerTreeGroup.html#qgis.core.QgsLayerTreeGroup.addGroup)方法添加组。在下面的示例中,前者将在目录树的末尾添加一个组,而后者则可以在现有的组中添加另一个组: 142 | 143 | ```python 144 | node_group1 = root.addGroup('Simple Group') 145 | # 在图层组中添加子组 146 | node_subgroup1 = node_group1.addGroup("I'm a sub group") 147 | ``` 148 | 149 | 移动节点和组有许多有用的方法。 150 | 151 | 移动现有节点分三个步骤: 152 | 153 | 1. 克隆已存在的节点 154 | 2. 移动克隆的节点到想要的位置 155 | 3. 删除原始节点 156 | 157 | ```python 158 | # 克隆图层组 159 | cloned_group1 = node_group1.clone() 160 | # 移动图层组(包括子组和图层)到顶层 161 | root.insertChildNode(0, cloned_group1) 162 | # 删除原始图层组节点 163 | root.removeChildNode(node_group1) 164 | ``` 165 | 166 | 移动一个图层要稍微复杂一点: 167 | 168 | ```python 169 | # 获得一个图层 170 | vl = QgsProject.instance().mapLayersByName("countries")[0] 171 | # 从图层树中获取 172 | myvl = root.findLayer(vl.id()) 173 | # 克隆 174 | myvlclone = myvl.clone() 175 | # 获取父级,如果图层不在图层组中返回空字符串 176 | parent = myvl.parent() 177 | # 移动图层节点到顶层 178 | parent.insertChildNode(0, myvlclone) 179 | # 删除原有节点 180 | root.removeChildNode(myvl) 181 | ``` 182 | 183 | 或者移动到已存在的图层组中: 184 | 185 | ```python 186 | # 获得一个图层 187 | vl = QgsProject.instance().mapLayersByName("countries")[0] 188 | # 从图层树中获取 189 | myvl = root.findLayer(vl.id()) 190 | # 克隆 191 | myvlclone = myvl.clone() 192 | # 创建一个新组 193 | group1 = root.addGroup("Group1") 194 | # 获取父级,如果图层不在图层组中返回空字符串 195 | parent = myvl.parent() 196 | # 移动图层节点到顶层 197 | group1.insertChildNode(0, myvlclone) 198 | # 从原有组中删除 199 | parent.removeChildNode(myvl) 200 | ``` 201 | 202 | 可用于修改组和图层的其他一些方法: 203 | 204 | ```python 205 | node_group1 = root.findGroup("Group1") 206 | # 修改图层组名称 207 | node_group1.setName("Group X") 208 | node_layer2 = root.findLayer(country_layer.id()) 209 | # 修改图层名称 210 | node_layer2.setName("Layer X") 211 | # 改变图层可见状态 212 | node_group1.setItemVisibilityChecked(True) 213 | node_layer2.setItemVisibilityChecked(False) 214 | # 折叠/展开图层组 215 | node_group1.setExpanded(True) 216 | node_group1.setExpanded(False) 217 | ``` -------------------------------------------------------------------------------- /docs/13-与用户通信.md: -------------------------------------------------------------------------------- 1 | # 13 与用户通信 2 | 3 | 本节代码片段需导入以下模块: 4 | 5 | ```python 6 | from qgis.core import ( 7 | QgsMessageLog, 8 | QgsGeometry, 9 | ) 10 | 11 | from qgis.gui import ( 12 | QgsMessageBar, 13 | ) 14 | 15 | from qgis.PyQt.QtWidgets import ( 16 | QSizePolicy, 17 | QPushButton, 18 | QDialog, 19 | QGridLayout, 20 | QDialogButtonBox, 21 | ) 22 | ``` 23 | 24 | 本节介绍用于与用户通信的一些方法和元素,以保持用户接口的一致性。 25 | 26 | ## 13.1 显示消息——QgsMessageBar 27 | 28 | 从用户体验的角度来看,使用消息框可能是个坏主意。为了显示一小行信息或警告/错误消息,QGIS消息栏通常是更好的选择。 29 | 30 | 使用QGIS接口对象的引用,你可以使用以下代码在消息栏中显示消息 31 | 32 | ```python 33 | from qgis.core import Qgis 34 | iface.messageBar().pushMessage("Error", "I'm sorry Dave, I'm afraid I can't do that", level=Qgis.Critical) 35 | ``` 36 | 37 | ![errorbar.png](./assets/errorbar.png) 38 | 39 | 40 |
QGIS消息栏
41 | 42 | 你可以设置持续时间,在有限时间内显示它 43 | 44 | ```python 45 | iface.messageBar().pushMessage("Ooops", "The plugin is not working as it should", level=Qgis.Critical, duration=3) 46 | ``` 47 | 48 | ![errorbar-timed.png](./assets/errorbar-timed.png) 49 | 50 |
带定时器的QGIS消息栏
51 | 52 | 上面的示例显示了错误栏,但`level`参数可用于创建警告消息或正常消息——使用[`Qgis.MessageLevel`](https://qgis.org/pyqgis/master/core/Qgis.html#qgis.core.Qgis.MessageLevel)枚举,你最多可以使用4个不同级别: 53 | 54 | 0. Info 55 | 56 | 2. Warning 57 | 3. Critical 58 | 4. Success 59 | 60 | ![infobar.png](./assets/infobar.png) 61 | 62 |
QGIS消息栏(info)
63 | 64 | 控件可以添加到消息栏中,例如用于显示更多信息的按钮 65 | 66 | ```python 67 | def showError(): 68 | pass 69 | 70 | widget = iface.messageBar().createMessage("Missing Layers", "Show Me") 71 | button = QPushButton(widget) 72 | button.setText("Show Me") 73 | button.pressed.connect(showError) 74 | widget.layout().addWidget(button) 75 | iface.messageBar().pushWidget(widget, Qgis.Warning) 76 | ``` 77 | 78 | ![bar-button.png](./assets/bar-button.png) 79 | 80 |
带有按钮的QGIS消息栏
81 | 82 | 你甚至可以在自己的对话框中使用消息栏,这样就不必显示消息框,或者在主QGIS窗口中显示消息时没有意义 83 | 84 | ```python 85 | class MyDialog(QDialog): 86 | def __init__(self): 87 | QDialog.__init__(self) 88 | self.bar = QgsMessageBar() 89 | self.bar.setSizePolicy( QSizePolicy.Minimum, QSizePolicy.Fixed ) 90 | self.setLayout(QGridLayout()) 91 | self.layout().setContentsMargins(0, 0, 0, 0) 92 | self.buttonbox = QDialogButtonBox(QDialogButtonBox.Ok) 93 | self.buttonbox.accepted.connect(self.run) 94 | self.layout().addWidget(self.buttonbox, 0, 0, 2, 1) 95 | self.layout().addWidget(self.bar, 0, 0, 1, 1) 96 | def run(self): 97 | self.bar.pushMessage("Hello", "World", level=Qgis.Info) 98 | 99 | myDlg = MyDialog() 100 | myDlg.show() 101 | ``` 102 | 103 | ![dialog-with-bar.png](./assets/dialog-with-bar.png) 104 | 105 |
自定义对话框中的QGIS消息栏
106 | 107 | ## 13.2 显示进度条 108 | 109 | 进度条也可以放在QGIS消息栏中,正如我们所见,它接受控件。以下是你可以在控制台中尝试的示例: 110 | 111 | ```python 112 | import time 113 | from qgis.PyQt.QtWidgets import QProgressBar 114 | from qgis.PyQt.QtCore import * 115 | progressMessageBar = iface.messageBar().createMessage("Doing something boring...") 116 | progress = QProgressBar() 117 | progress.setMaximum(10) 118 | progress.setAlignment(Qt.AlignLeft|Qt.AlignVCenter) 119 | progressMessageBar.layout().addWidget(progress) 120 | iface.messageBar().pushWidget(progressMessageBar, Qgis.Info) 121 | 122 | for i in range(10): 123 | time.sleep(1) 124 | progress.setValue(i + 1) 125 | 126 | iface.messageBar().clearWidgets() 127 | ``` 128 | 129 | 此外,你可以使用内置状态栏报告进度,如下示例所示: 130 | 131 | ```python 132 | vlayer = iface.activeLayer() 133 | 134 | count = vlayer.featureCount() 135 | features = vlayer.getFeatures() 136 | 137 | for i, feature in enumerate(features): 138 | # 做一些耗时任务 139 | print('') # 给予足够的时间来打印进度 140 | 141 | percent = i / float(count) * 100 142 | # iface.mainWindow().statusBar().showMessage("Processed {} %".format(int(percent))) 143 | iface.statusBarIface().showMessage("Processed {} %".format(int(percent))) 144 | 145 | iface.statusBarIface().clearMessage() 146 | ``` 147 | 148 | ## 13.3 日志 149 | 150 | QGIS有三种不同类型的日志记录,记录和保存有关代码执行的所有信息。每种类型都有特定的输出位置。请考虑使用正确的日志记录方式: 151 | 152 | - [`QgsMessageLog`](https://qgis.org/pyqgis/master/core/QgsMessageLog.html#qgis.core.QgsMessageLog) 用于向用户传达问题。QgsMessageLog的输出显示在日志消息面板中。 153 | - Python的 **logging** 模块用于调试QGIS Python API(PyQGIS)。建议Python开发人员使用其调试代码,例如,要素的id或者几何。 154 | - [`QgsLogger`](https://qgis.org/pyqgis/master/core/QgsLogger.html#qgis.core.QgsLogger) 用于QGIS内部调试/开发(例如:当你怀疑某些内容引起了崩溃)。只有QGIS的开发者版本可用。 155 | 156 | 不同日志记录类型的示例如下所示。 157 | 158 | !!! 警告 warning 159 | 160 | 在多线程的代码中使用Python `print`语句是不安全的,并且很大程度降低算法的速度。包括函数表达式,渲染器,符号层和处理算法(等等)。在这些情况下,你应该始终使用Python **logging** 模块或线程安全类([`QgsLogger`](https://qgis.org/pyqgis/master/core/QgsLogger.html#qgis.core.QgsLogger)或[`QgsMessageLog`](https://qgis.org/pyqgis/master/core/QgsMessageLog.html#qgis.core.QgsMessageLog))。 161 | 162 | 163 | ### 13.3.1 QgsMessageLog 164 | 165 | ```python 166 | # 你可以选择传递'tag'和'level'参数 167 | QgsMessageLog.logMessage("Your plugin code has been executed correctly", 'MyPlugin', level=Qgis.Info) 168 | QgsMessageLog.logMessage("Your plugin code might have some problems", level=Qgis.Warning) 169 | QgsMessageLog.logMessage("Your plugin code has crashed!", level=Qgis.Critical) 170 | ``` 171 | 172 | !!! 提示 173 | 174 | 你可以在[日志消息面板](https://docs.qgis.org/testing/en/docs/user_manual/introduction/general_tools.html#log-message-panel)中查看[`QgsMessageLog`](https://qgis.org/pyqgis/master/core/QgsMessageLog.html#qgis.core.QgsMessageLog)输出的日志。 175 | 176 | ### 13.3.2 python logging模块 177 | 178 | ```python 179 | import logging 180 | formatter = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 181 | logfilename=r'c:\temp\example.log' 182 | logging.basicConfig(filename=logfilename, level=logging.DEBUG, format=formatter) 183 | logging.info("This logging info text goes into the file") 184 | logging.debug("This logging debug text goes into the file as well") 185 | 186 | # 2020-10-08 13:14:42,998 - root - INFO - This logging text goes into the file 187 | # 2020-10-08 13:14:42,998 - root - DEBUG - This logging debug text goes into the file as well 188 | ``` 189 | 190 | basicConfig方法配置日志记录的基本设置。在上面的代码中,定义了文件名,日志记录级别和格式。文件名指的是将日志文件写入的位置,日志记录级别定义输出的级别,格式定义输出每个消息的格式。 191 | 192 | 如果你想要每次在执行脚本时删除日志文件,你可以执行以下操作: 193 | 194 | ```python 195 | if os.path.isfile(logfilename): 196 | with open(logfilename, 'w') as file: 197 | pass 198 | ``` 199 | 200 | 如何使用如何使用Python日志记录工具的更多资源: 201 | 202 | - https://docs.python.org/3/library/logging.html 203 | - https://docs.python.org/3/howto/logging.html 204 | - https://docs.python.org/3/howto/logging-cookbook.html 205 | 206 | !!! 警告 warning 207 | 208 | 请注意,如果不设置日志文件,日志可能是多线程的,这会严重减慢输出速度 209 | -------------------------------------------------------------------------------- /docs/5-使用栅格图层.md: -------------------------------------------------------------------------------- 1 | # 5 使用栅格图层 2 | 3 | 此页面上的代码段需要导入以下模块: 4 | 5 | ```python 6 | from qgis.core import ( 7 | Qgis, 8 | QgsRasterLayer, 9 | QgsProject, 10 | QgsPointXY, 11 | QgsRaster, 12 | QgsRasterBlock, 13 | QgsRasterShader, 14 | QgsColorRampShader, 15 | QgsSingleBandPseudoColorRenderer, 16 | QgsSingleBandColorDataRenderer, 17 | QgsSingleBandGrayRenderer, 18 | ) 19 | 20 | from qgis.PyQt.QtGui import ( 21 | QColor, 22 | ) 23 | ``` 24 | 25 | ## 5.1 图层细节 26 | 27 | 栅格图层由一个或多个栅格波段组成——称为单波段和多波段栅格。一个波段代表一个矩阵。彩色图像(例如航拍照片)是由红色,蓝色和绿色波段组成。单波段栅格通常表示连续变量(例如高程)或离散变量(例如土地使用)。在某些情况下,栅格图层带有调色板,栅格值指的是调色板中存储的颜色。 28 | 29 | 以下代码假定`rlayer`是一个 [`QgsRasterLayer`](https://qgis.org/pyqgis/master/core/QgsRasterLayer.html#qgis.core.QgsRasterLayer)对象。 30 | 31 | ```python 32 | rlayer = QgsProject.instance().mapLayersByName('srtm')[0] 33 | # 获取图层分辨率 34 | rlayer.width(), rlayer.height() 35 | (919, 619) 36 | # 获取图层范围,返回QgsRectangle对象 37 | rlayer.extent() 38 | # 39 | # 获取图层范围并转换为字符串 40 | rlayer.extent().toString() 41 | # '20.0685680819999988,-34.2700107699999990 : 20.8394528430000001,-33.7507750070000014' 42 | # 获取栅格类型: 0 = 灰度值(单波段), 1 = 调色板(单波段), 2 = 多波段 43 | rlayer.rasterType() 44 | # 0 45 | # 获取波段个数 46 | rlayer.bandCount() 47 | # 1 48 | # 获取第一个波段名称 49 | print(rlayer.bandName(1)) 50 | # Band 1: Height 51 | # 获取所有可用的元数据,返回QgsLayerMetadata对象 52 | rlayer.metadata() 53 | # '' 54 | ``` 55 | 56 | ## 5.2 渲染 57 | 58 | 加载栅格图层时,它会根据其类型获取默认渲染器。它可以在图层属性中更改,也可以通过编程方式更改。 59 | 60 | 查询当前渲染器: 61 | 62 | ```python 63 | rlayer.renderer() 64 | # 65 | rlayer.renderer().type() 66 | # 'singlebandgray' 67 | ``` 68 | 69 | 设置渲染器,使用[`QgsRasterLayer`](https://qgis.org/pyqgis/master/core/QgsRasterLayer.html#qgis.core.QgsRasterLayer)的[`setRenderer`](https://qgis.org/pyqgis/master/core/QgsRasterLayer.html#qgis.core.QgsRasterLayer.setRenderer) 方法。下面有许多渲染器类(派生自[`QgsRasterRenderer`](https://qgis.org/pyqgis/master/core/QgsRasterRenderer.html#qgis.core.QgsRasterRenderer)): 70 | 71 | - [`QgsHillshadeRenderer`](https://qgis.org/pyqgis/master/core/QgsHillshadeRenderer.html#qgis.core.QgsHillshadeRenderer) 72 | - [`QgsMultiBandColorRenderer`](https://qgis.org/pyqgis/master/core/QgsMultiBandColorRenderer.html#qgis.core.QgsMultiBandColorRenderer) 73 | - [`QgsPalettedRasterRenderer`](https://qgis.org/pyqgis/master/core/QgsPalettedRasterRenderer.html#qgis.core.QgsPalettedRasterRenderer) 74 | - [`QgsRasterContourRenderer`](https://qgis.org/pyqgis/master/core/QgsRasterContourRenderer.html#qgis.core.QgsRasterContourRenderer) 75 | - [`QgsSingleBandColorDataRenderer`](https://qgis.org/pyqgis/master/core/QgsSingleBandColorDataRenderer.html#qgis.core.QgsSingleBandColorDataRenderer) 76 | - [`QgsSingleBandGrayRenderer`](https://qgis.org/pyqgis/master/core/QgsSingleBandGrayRenderer.html#qgis.core.QgsSingleBandGrayRenderer) 77 | - [`QgsSingleBandPseudoColorRenderer`](https://qgis.org/pyqgis/master/core/QgsSingleBandPseudoColorRenderer.html#qgis.core.QgsSingleBandPseudoColorRenderer) 78 | 79 | 单波段栅格图层可以以灰色(低值=黑色,高值=白色)或使用伪彩色算法绘制,该算法为值指定颜色。也可以使用调色板绘制带调色板的单波段栅格。多波段图层通常通过将波段映射到RGB来绘制颜色。另一种可能是仅使用一个波段来绘图。 80 | 81 | ### 5.2.1 单波段栅格 82 | 83 | 假设我们想要渲染一个单波段栅格图层,颜色范围从绿色到黄色(对应于0到255之间的像素值)。在第一阶段,我们将准备一个:[QgsRasterShader](https://qgis.org/pyqgis/master/core/QgsRasterShader.html#qgis.core.QgsRasterShader)对象并配置其着色器函数: 84 | 85 | ```python 86 | fcn = QgsColorRampShader() 87 | fcn.setColorRampType(QgsColorRampShader.Interpolated) 88 | lst = [ QgsColorRampShader.ColorRampItem(0, QColor(0,255,0)),QgsColorRampShader.ColorRampItem(255, QColor(255,255,0)) ] 89 | fcn.setColorRampItemList(lst) 90 | shader = QgsRasterShader() 91 | shader.setRasterShaderFunction(fcn) 92 | ``` 93 | 94 | 着色器按其颜色映射指定的颜色。彩色地图被提供为具有相关颜色的像素值列表。插值有三种模式: 95 | 96 | - 线性(`Interpolated`):从像素值上方和下方的颜色表条带中线性插值颜色 97 | - 离散(`Discrete`):颜色取自具有相同或更高值的最接近的颜色表条带 98 | - 精确(`Exact`):不插值颜色,只绘制值等于颜色表条带的像素 99 | 100 | 在第二步中,我们将此着色器与栅格图层相关联: 101 | 102 | ```python 103 | renderer = QgsSingleBandPseudoColorRenderer(rlayer.dataProvider(), 1, shader) 104 | rlayer.setRenderer(renderer) 105 | ``` 106 | 107 | 上面代码中数值`1`是波段号(栅格波段的一个索引)。 108 | 109 | 最后我们必须使用 [`triggerRepaint`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.triggerRepaint)方法来查看结果: 110 | 111 | ```python 112 | rlayer.triggerRepaint() 113 | ``` 114 | 115 | ### 5.2.2 多波段栅格 116 | 117 | 默认情况下,QGIS将前三个波段映射为红色,绿色和蓝色来创建彩色图像(这是`MultiBandColor`绘图样式。在某些情况下,你可能希望覆盖这些设置。以下代码互换红色波段(1)和绿色波段(2): 118 | 119 | ```python 120 | rlayer_multi = QgsProject.instance().mapLayersByName('multiband')[0] 121 | rlayer_multi.renderer().setGreenBand(1) 122 | rlayer_multi.renderer().setRedBand(2) 123 | ``` 124 | 125 | 如果只需要一个波段来实现光栅的可视化,则可以选择单波段绘制,灰度级或伪彩色。 126 | 127 | 我们必须使用[`triggerRepaint`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.triggerRepaint) 更新地图并查看结果: 128 | 129 | ```python 130 | rlayer.triggerRepaint() 131 | ``` 132 | 133 | ## 5.3 查询值 134 | 135 | 查询栅格值的第一种方法是使用 [`QgsRasterDataProvider`](https://qgis.org/pyqgis/master/core/QgsRasterDataProvider.html#qgis.core.QgsRasterDataProvider)的[`sample()`](https://qgis.org/pyqgis/master/core/QgsRasterDataProvider.html#qgis.core.QgsRasterDataProvider.sample)方法查询。你必须指定栅格图层的[`QgsPointXY`](https://qgis.org/pyqgis/master/core/QgsPointXY.html#qgis.core.QgsPointXY)的和波段号。该方法返回一个value和result(true或false): 136 | 137 | ```python 138 | val, res = rlayer.dataProvider().sample(QgsPointXY(20.50, -34), 1) 139 | ``` 140 | 141 | 查询栅格值的另一种方法是使用[`identify()`](https://qgis.org/pyqgis/master/core/QgsRasterDataProvider.html#qgis.core.QgsRasterDataProvider.identify)方法,返回[`QgsRasterIdentifyResult`](https://qgis.org/pyqgis/master/core/QgsRasterIdentifyResult.html#qgis.core.QgsRasterIdentifyResult)对象 。 142 | 143 | ```python 144 | ident = rlayer.dataProvider().identify(QgsPointXY(20.5, -34), QgsRaster.IdentifyFormatValue) 145 | if ident.isValid(): 146 | print(ident.results()) 147 | 148 | # {1: 323.0} 149 | ``` 150 | 151 | 在这种情况下,[`results()`](https://qgis.org/pyqgis/master/core/QgsRasterIdentifyResult.html#qgis.core.QgsRasterIdentifyResult.results) 方法返回一个字典,波段索引作为字典键,波段值作为字典值。例如`{1: 323.0}`。 152 | 153 | ## 5.4 编辑栅格数据 154 | 155 | 可以使用[`QgsRasterBlock`](https://qgis.org/pyqgis/master/core/QgsRasterBlock.html#qgis.core.QgsRasterBlock)类创建栅格图层。例如,创建每像素一字节的2x2栅格块: 156 | 157 | ```python 158 | block = QgsRasterBlock(Qgis.Byte, 2, 2) 159 | block.setData(b'\xaa\xbb\xcc\xdd') 160 | ``` 161 | 162 | 使用[`writeBlock()`](https://qgis.org/pyqgis/master/core/QgsRasterDataProvider.html#qgis.core.QgsRasterDataProvider.writeBlock)方法可以覆盖栅格像素。用2x2块覆盖位置0,0处的现有栅格数据: 163 | 164 | ```python 165 | provider = rlayer.dataProvider() 166 | provider.setEditable(True) 167 | provider.writeBlock(block, 1, 0, 0) 168 | provider.setEditable(False) 169 | ``` -------------------------------------------------------------------------------- /docs/1-引言.md: -------------------------------------------------------------------------------- 1 | # 1 引言 2 | 3 | 本文档既可作为教程,也可作为参考指南。虽然没有列举所有可能的案例,但是对主要功能有一个很好的概述。 4 | 5 | 根据GNU免费文档许可证、版本1.3或自由软件基金会发布的任何后续版本的条款,允许复制、分发和/或修改该文档;没有固定章节,没有封面文本,也没有封底文本。 6 | 7 | [GNU免费文档许可证](https://docs.qgis.org/testing/en/docs/gentle_gis_introduction/gnu_free_documentation_license.html#gnu-fdl)一节中包含了该许可证的副本。 8 | 9 | 本许可证也适用于本文档中的所有代码段。 10 | 11 | 对于Python的支持最初是在QGIS 0.9中引入的。 目前,在QGIS桌面版中有几种方法可以使用Python,如下: 12 | 13 | - 在QGIS的Python控制台中 14 | - 创建并使用插件 15 | - QGIS启动时自动运行Python代码 16 | - 创建处理算法 17 | - 在QGIS中为表达式创建函数 18 | - 基于QGIS API创建自定义应用程序 19 | 20 | Python支持在QGIS 0.9中首次引入。这里有几种方法可以在QGIS桌面软件中使用Python(涵盖一下部分): 21 | 22 | - 在QGIS中的Python控制台中使用命令 23 | - 创建并使用插件 24 | - 当QGIS启动时自动运行Python代码 25 | - 创建处理算法 26 | - 在QGIS中创建函数表达式 27 | - 基于QGIS API创建自定义应用 28 | 29 | Python绑定也可用于QGIS服务,包括Python插件(请参阅[20-QGIS服务器和Python](20-QGIS服务器和Python.md))和Python绑定,可用于将QGIS服务嵌入到Python应用程序中。 30 | 这里有一个[完整的QGIS C++ API]()参考——用于记录QGIS库中的类。 [Pythonic QGIS API(pyqgis)](https://qgis.org/pyqgis)几乎与C ++ API相同。 31 | 学习执行常见任务的另一个好办法是从[插件仓库](https://plugins.qgis.org/)下载现有插件并学习它们的代码。 32 | 33 | ## 1.1 在Python控制台中编写脚本 34 | 35 | QGIS为脚本编写提供了一个集成的python控制台。可以从插件→python控制台菜单中打开: 36 | 37 | ![1559181577361](./assets/1559181577361.png) 38 | 39 | 上面的截图说明了如何获取图层列表中当前选定的图层,并显示其ID,如果是矢量图层,还可以显示要素个数。对于与QGIS交互环境,有一个`iface`变量,它是[QgisInterface](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)的一个实例。此接口允许访问地图画布、菜单、工具栏和QGIS应用程序的其他部分。 40 | 41 | 为了方便用户,在启动控制台时将会执行以下语句(将来可以设置更多的初始命令) 42 | 43 | ```python 44 | from qgis.core import * 45 | import qgis.utils 46 | ``` 47 | 48 | 对于经常使用控制台的用户,设置打开控制台的快捷键可能很有用(在设置→键盘快捷键中...) 49 | 50 | ## 1.2 Python插件 51 | 52 | 可以使用插件来扩展QGIS的功能,可以使用Python编写插件。与C ++插件相比,主要优点是分发简单(不对每个平台进行编译)、更容易的开发。 53 | 54 | 自从引入对Python的支持以来,已经编写了许多涵盖各种功能的插件。插件安装程序允许用户很容易获取、升级和删除Python插件。有关插件和插件开发的更多信息,请参阅[Python插件](https://plugins.qgis.org/)页面。 55 | 56 | 使用Python创建插件很简单,详细说明请参阅[16-开发Python插件](16-开发Python插件.md)。 57 | 58 | !!! 提示 59 | 60 | Python插件也可用于QGIS服务,更多信息,请参阅[20-QGIS服务器和Python](20-QGIS服务器和Python.md)。 61 | 62 | ### 1.2.1 处理插件 63 | 64 | 处理插件可被用于处理数据。它们比Python插件更容易开发、更具体、更轻量。[17-编写处理插件](17-编写处理插件.md)阐述了什么时候该使用处理算法,并且怎么开发它们。 65 | 66 | 67 | ## 1.3 QGIS启动时运行Python代码 68 | 69 | 每次QGIS启动时,都有不同的方法来运行Python代码。 70 | 71 | 1. 创建startup.py脚本 72 | 2. 将`PYQGIS_STARTUP`环境变量设置为现有Python文件 73 | 2. 使用 `--code init_qgis.py` 参数指定启动脚本 74 | 75 | ### 1.3.1 startup.py文件 76 | 77 | 每次QGIS启动时,都会在用户的Python主目录和系统路径列表中搜索名为`startup.py`的文件。 如果该文件存在,则由嵌入式Python解释器执行。 78 | 79 | 用户主目录中的路径通常位于: 80 | 81 | - Linux: `.local/share/QGIS/QGIS3` 82 | - Windows: `AppData\Roaming\QGIS\QGIS3` 83 | - macOS: `Library/Application Support/QGIS/QGIS3` 84 | 85 | 默认系统路径取决于操作系统。要查找适合您的路径,请打开 Python 控制台并运行 `QStandardPaths.standardLocations(QStandardPaths.AppDataLocation)`以查看默认目录列表。 86 | 87 | `startup.py`脚本在QGIS中初始化python时立即执行,在应用程序启动的早期。 88 | 89 | ### 1.3.2 PYQGIS_STARTUP环境变量 90 | 91 | 你可以在QGIS初始化完成之前将`PYQGIS_STARTUP`环境变量设置为现有Python文件的路径来运行Python代码。 92 | 93 | 此代码将在QGIS初始化完成之前运行。此方法对于清理sys.path非常有用——可能存在不需要的路径,或用于隔离/加载初始环境——无需虚拟环境,例如在Mac上使用homebrew或MacPorts。 94 | 95 | ### 1.3.3 `--code`参数 96 | 97 | 你可以提供自定义代码作为 QGIS 的启动参数执行。为此,请创建一个 python 文件,例如 `qgis_init.py`,使用 `qgis --code qgis_init.py `从命令行执行和启动 QGIS。 98 | 99 | 通过 `--code` 提供的代码在 QGIS 初始化阶段的后期,在加载应用程序组件之后执行。 100 | 101 | ### 1.3.4 Python 的其他参数 102 | 103 | 要为 `--code` 脚本或执行的其他 python 代码提供其他参数,可以使用 `--py-args` 参数。在 `--py-args` 之后和 `-- arg`(如果存在)之前的任何参数都将传递给 Python,但被 QGIS 应用程序本身忽略。 104 | 105 | 在下面的例子中,`myfile.tif` 将通过 Python 中的 `sys.argv` 提供,但不会由 QGIS 加载。而`otherfile.tif`将由QGIS加载,但不存在于`sys.argv`中。 106 | 107 | ```bash 108 | qgis --code qgis_init.py --py-args myfile.tif -- otherfile.tif 109 | ``` 110 | 111 | 如果你想从Python中访问每个命令行参数,你可以使用`QCoreApplication.arguments()` 112 | 113 | ```python 114 | QgsApplication.instance().arguments() 115 | ``` 116 | 117 | ## 1.4 Python应用程序 118 | 119 | 为自动化流程创建脚本通常很方便。使用PyQGIS,这是完全可能的——导入`qgis.core`模块,初始化它,你就可以进行处理了。 120 | 121 | 或者你可能想要创建一个使用GIS功能的交互式应用程序——执行测量、将地图导出为PDF或任何其他功能。`qgis.gui`模块提供了各种GUI部件,最值得注意的是可以合并到应用程序中的地图画布控件——支持缩放,平移和任何其他自定义地图工具。 122 | 123 | 必须配置PyQGIS自定义应用程序或独立脚本以定位QGIS资源,例如投影信息,用于读取矢量和栅格图层的数据提供者等。QGIS资源通过在应用程序或脚本的开头添加几行(代码)来初始化。自定义应用程序和独立脚本初始化QGIS的代码类似。以下提供各自的实例。 124 | 125 | !!! 提示 126 | 127 | 千万不能使用`qgis.py`作为你的测试脚本的名称,否则Python将无法导入绑定。 128 | 129 | ### 1.4.1 在独立脚本中使用PyQGIS 130 | 131 | 启动独立脚本,在脚本开头初始化QGIS资源,类似于以下代码: 132 | 133 | ```python 134 | from qgis.core import * 135 | # 提供qgis安装位置的路径(windows默认:C:\Program Files\QGIS 3.x\apps\qgis-ltr) 136 | QgsApplication.setPrefixPath("/path/to/qgis/installation", True) 137 | # 创建对QgsApplication的引用,第二个参数设置为False将禁用GUI 138 | qgs = QgsApplication([], False) 139 | # 加载提供者 140 | qgs.initQgis() 141 | # 在这里编写代码,加载一些图层,使用处理算法等 142 | # 脚本完成后,调用exitQgis()从内存中删除提供者和图层注册 143 | qgs.exitQgis() 144 | ``` 145 | 146 | 我们首先导入`qgis.core`模块,然后配置前缀路径。前缀路径是安装QGIS的路径。它通过调用`setPrefixPath`方法在脚本中配置。第二个参数设置为`True`,它控制是否使用默认路径。 147 | 148 | QGIS安装路径因平台而异,在系统中找到它的最简单方法是在QGIS中使用[1.1 在Python控制台中编写脚本](#11-python)运行 `QgsApplication.prefixPath()`并查看输出。 149 | 150 | 配置前缀路径后,我们在变量`qgs`中保存了一个对`QgsApplication`的引用。第二个参数设置为`False`,表示我们不打算使用GUI,因为我们正在编写一个独立的脚本。配置`QgsApplication`后 ,我们通过调用`qgs.initQgis()`方法加载QGIS数据提供者和图层注册。在QGIS初始化后,我们准备编写脚本的其余部分。最后,我们通过调用`qgs.exitQgis()`从内存中删除数据提供者和图层注册。 151 | 152 | ### 1.4.2 在自定义应用程序中使用PyQGIS 153 | 154 | [1.4.1 在独立脚本中使用PyQGIS](#141-pyqgis)和自定义PyQGIS应用程序之间的唯一的区别是在实例化`QgsApplication`时的第二个参数。传递`True`而不是`False`,表示我们计划使用GUI。 155 | 156 | ```python 157 | from qgis.core import * 158 | # 提供qgis安装位置的路径(windows默认:C:\Program Files\QGIS 3.x\apps\qgis-ltr) 159 | QgsApplication.setPrefixPath("/path/to/qgis/installation", True) 160 | # 创建对QgsApplication设置的引用第二个参数为True启用GUI,我们需要这样做,因为这是一个自定义应用程序 161 | qgs = QgsApplication([], True) 162 | # 加载提供者 163 | qgs.initQgis() 164 | # 在这里编写代码,加载一些图层,使用处理算法等 165 | # 脚本完成后,调用exitQgis()从内存中删除提供者和图层注册 166 | qgs.exitQgis() 167 | ``` 168 | 169 | 现在,你可以使用QGIS API——加载图层并执行一些处理或使用地图画布启动GUI。可能性是无止境的:-) 170 | 171 | ### 1.4.3 运行自定义应用程序 172 | 173 | 如果它们不在一个众所周知的位置,你需要告诉系统在哪里搜索QGIS库和合适的Python模块——否则Python会抛出异常: 174 | 175 | ```python 176 | >>> import qgis.core 177 | ImportError: No module named qgis.core 178 | ``` 179 | 180 | 可以通过设置`PYTHONPATH`环境变量来修复。在以下命令中,``应替换为你的实际QGIS安装路径: 181 | 182 | - 在Linux上:**export PYTHONPATH=//share/qgis/python** 183 | - 在Windows上:**set PYTHONPATH=c:\\python** 184 | - 在macOS上:**export PYTHONPATH=//Contents/Resources/python** 185 | 186 | 现在,PyQGIS模块的路径设置完成,但它们依赖于`qgis_core`和`qgis_gui`库(仅仅作为封装的Python模块)。这些库的路径通常是操作系统未知的,因此再次出现导入错误(错误消息可能因系统而异): 187 | 188 | ```python 189 | >>> import qgis.core 190 | ImportError: libqgis_core.so.3.2.0: cannot open shared object file:No such file or directory 191 | ``` 192 | 193 | 通过将QGIS库所在的目录添加到动态链接器的搜索路径来解决此问题: 194 | 195 | - 在Linux上:**export LD_LIBRARY_PATH=/qgispath/lib** 196 | - 在Windows上:**set PATH=C:\qgispath\BIN; C:\qgispath\APPS\qgisrelease\BIN;PATH%** ,其中`qgisrelease`应替换成你的发布类型(例如,`qgis-ltr`,`qgis`,`qgis-dev`) 197 | 198 | 这些命令可以放入一个引导脚本,负责启动。使用PyQGIS部署自定义应用程序时,通常有两种可能: 199 | 200 | - 要求用户在安装应用程序之前在其平台上安装QGIS。应用程序安装程序应查找QGIS库的默认位置,并允许用户设置路径(如果未找到)。该方法更简单,但是它需要用户执行更多步骤。 201 | - 将QGIS与你的应用程序一起打包。发布应用程序可能更具挑战性,并且程序包将更大,但用户将免于下载和安装其他软件的负担。 202 | 203 | 这两种部署方式可以混合使用——在Windows和macOS上部署独立应用程序,但是对于Linux,将QGIS的安装留给用户和他的包管理器。 204 | 205 | ## 1.5 关于PyQt和SIP的技术说明 206 | 207 | 我们决定使用Python,因为它是最受欢迎的脚本语言之一。QGIS3中的PyQGIS绑定依赖于SIP和PyQt5。使用SIP而不是使用更广泛使用的SWIG的原因是QGIS代码依赖于Qt库。Qt(PyQt)的Python绑定使用SIP完成,这允许PyQGIS与PyQt无缝集成。 -------------------------------------------------------------------------------- /docs/10-地图渲染和打印.md: -------------------------------------------------------------------------------- 1 | 本节代码片段需要导入以下模块: 2 | 3 | ```python 4 | import os 5 | 6 | from qgis.core import ( 7 | QgsGeometry, 8 | QgsMapSettings, 9 | QgsPrintLayout, 10 | QgsMapSettings, 11 | QgsMapRendererParallelJob, 12 | QgsLayoutItemLabel, 13 | QgsLayoutItemLegend, 14 | QgsLayoutItemMap, 15 | QgsLayoutItemPolygon, 16 | QgsLayoutItemScaleBar, 17 | QgsLayoutExporter, 18 | QgsLayoutItem, 19 | QgsLayoutPoint, 20 | QgsLayoutSize, 21 | QgsUnitTypes, 22 | QgsProject, 23 | QgsFillSymbol, 24 | QgsAbstractValidityCheck, 25 | check, 26 | ) 27 | 28 | from qgis.PyQt.QtGui import ( 29 | QPolygonF, 30 | QColor, 31 | ) 32 | 33 | from qgis.PyQt.QtCore import ( 34 | QPointF, 35 | QRectF, 36 | QSize, 37 | ) 38 | ``` 39 | 40 | # 10 地图渲染和打印 41 | 42 | 当输入数据作为地图呈现时,通常有两种方法:使用*QgsMapRendererJob*快速进行,或者使用[`QgsLayout`](https://qgis.org/pyqgis/master/core/QgsLayout.html#qgis.core.QgsLayout)类组合地图来生成更精细的输出。 43 | 44 | ## 10.1 简单的渲染 45 | 46 | 渲染完成后,创建一个[`QgsMapSettings`](https://qgis.org/pyqgis/master/core/QgsMapSettings.html#qgis.core.QgsMapSettings)对象来定义渲染选项,然后使用这些选项构建一个[`QgsMapRendererJob`](https://qgis.org/pyqgis/master/core/QgsMapRendererJob.html#qgis.core.QgsMapRendererJob)类。然后使用后者来创建图像。 47 | 48 | 这是一个例子: 49 | 50 | ```python 51 | image_location = os.path.join(QgsProject.instance().homePath(), "render.png") 52 | 53 | vlayer = iface.activeLayer() 54 | settings = QgsMapSettings() 55 | settings.setLayers([vlayer]) 56 | settings.setBackgroundColor(QColor(255, 255, 255)) 57 | settings.setOutputSize(QSize(800, 600)) 58 | settings.setExtent(vlayer.extent()) 59 | 60 | render = QgsMapRendererParallelJob(options) 61 | 62 | def finished(): 63 | img = render.renderedImage() 64 | # 保存图像; e.g. img.save("/Users/myuser/render.png","png") 65 | img.save(image_location, "png") 66 | 67 | render.finished.connect(finished) 68 | 69 | # 开始渲染 70 | render.start() 71 | 72 | # 通常不需要循环,因为这里是单独使用 73 | from qgis.PyQt.QtCore import QEventLoop 74 | loop = QEventLoop() 75 | render.finished.connect(loop.quit) 76 | loop.exec_() 77 | ``` 78 | 79 | ## 10.2 使用不同的CRS渲染图层 80 | 81 | 如果你有多个图层并且它们具有不同的CRS,上面的简单示例可能不起作用:从范围计算中获取正确的值,你必须显式设置目标CRS 82 | 83 | ```python 84 | layers = [iface.activeLayer()] 85 | settings = QgsMapSettings() 86 | settings.setLayers(layers) 87 | render.setDestinationCrs(layers[0].crs()) 88 | ``` 89 | 90 | ## 10.3 使用打印布局输出 91 | 92 | 如果你想要比上面显示的简单渲染更复杂的输出,打印布局是一个非常方便的工具。可以创建复杂的地图布局,包括地图视图,标签,图例,表格以及通常出现在纸质地图上的其他元素。然后可以将布局导出为PDF,SVG,栅格图像或直接打印在打印机上。 93 | 94 | 布局由一堆类组成。它们都属于核心库。QGIS应用程序有一个方便的GUI布局元素,虽然它在GUI库中不可用。如果你不熟悉 [Qt Graphics View框架](http://doc.qt.io/qt-5/graphicsview.html),那么建议你查看文档,因为布局是基于它的。 95 | 96 | 布局的中心类是[`QgsLayout`](https://qgis.org/pyqgis/master/core/QgsLayout.html#qgis.core.QgsLayout) 类,它是从Qt [QGraphicsScene](https://doc.qt.io/qt-5/qgraphicsscene.html) 类派生的。让我们创建一个它的实例: 97 | 98 | ```python 99 | project = QgsProject.instance() 100 | layout = QgsPrintLayout(project) 101 | layout.initializeDefaults() 102 | ``` 103 | 104 | 这将使用一些默认设置初始化布局,将空的A4页添加到布局中。你可以在不调用[`initializeDefaults()`](https://qgis.org/pyqgis/master/core/QgsLayout.html#qgis.core.QgsLayout.initializeDefaults)方法的情况下创建布局,但你需要自己向布局中添加页面。 105 | 106 | 之前的代码创建了在GUI中不可见的“临时”布局。它可以方便快速地添加某些项并导出,而不修改项本身,也不会向户公开这些更改。如果你希望将布局与项目一起保存或恢复,并使其在布局管理器中可用,添加: 107 | 108 | ```python 109 | layout.setName("MyLayout") 110 | project.layoutManager().addLayout(layout) 111 | ``` 112 | 113 | 现在我们可以在布局中添加各种元素(map,label,...)。所有这些对象都继承自基类[`QgsLayoutItem`](https://qgis.org/pyqgis/master/core/QgsLayoutItem.html#qgis.core.QgsLayoutItem)。 114 | 115 | 以下是可以添加到布局的一些主要布局项的说明。 116 | 117 | - 地图—— 在这里,我们创建一个地图并将其拉伸到整个纸张大小 118 | 119 | ```python 120 | map = QgsLayoutItemMap(layout) 121 | # 设置地图项的位置和大小(默认是宽高都是0,位置在0,0) 122 | map.attemptMove(QgsLayoutPoint(5,5, QgsUnitTypes.LayoutMillimeters)) 123 | map.attemptResize(QgsLayoutSize(200,200, QgsUnitTypes.LayoutMillimeters)) 124 | # 提供渲染范围 125 | map.zoomToExtent(iface.mapCanvas().extent()) 126 | layout.addItem(map) 127 | ``` 128 | 129 | - 标签——允许显示标签。可以修改其字体,颜色,对齐和边距 130 | 131 | ```python 132 | label = QgsLayoutItemLabel(layout) 133 | label.setText("Hello world") 134 | label.adjustSizeToText() 135 | layout.addItem(label) 136 | ``` 137 | 138 | - 图例 139 | 140 | ```python 141 | legend = QgsLayoutItemLegend(layout) 142 | legend.setLinkedMap(map) # map是一个QgsLayoutItemMap实例 143 | layout.addItem(legend) 144 | ``` 145 | 146 | - 比例尺 147 | 148 | ```python 149 | item = QgsLayoutItemScaleBar(layout) 150 | item.setStyle('Numeric') # 可选择修改样式 151 | item.setLinkedMap(map) # map是一个QgsLayoutItemMap实例 152 | item.applyDefaultSize() 153 | layout.addItem(item) 154 | ``` 155 | 156 | - 箭头 157 | 158 | - 图片 159 | 160 | - 基本形状 161 | 162 | - 基于节点的形状 163 | 164 | ```python 165 | polygon = QPolygonF() 166 | polygon.append(QPointF(0.0, 0.0)) 167 | polygon.append(QPointF(100.0, 0.0)) 168 | polygon.append(QPointF(200.0, 100.0)) 169 | polygon.append(QPointF(100.0, 200.0)) 170 | 171 | polygonItem = QgsLayoutItemPolygon(polygon, layout) 172 | layout.addItem(polygonItem) 173 | 174 | props = {} 175 | props["color"] = "green" 176 | props["style"] = "solid" 177 | props["style_border"] = "solid" 178 | props["color_border"] = "black" 179 | props["width_border"] = "10.0" 180 | props["joinstyle"] = "miter" 181 | 182 | symbol = QgsFillSymbol.createSimple(props) 183 | polygonItem.setSymbol(symbol) 184 | ``` 185 | 186 | - 表格 187 | 188 | 将项添加到布局后,可以移动并调整其大小: 189 | 190 | ```python 191 | item.attemptMove(QgsLayoutPoint(1.4, 1.8, QgsUnitTypes.LayoutCentimeters)) 192 | item.attemptResize(QgsLayoutSize(2.8, 2.2, QgsUnitTypes.LayoutCentimeters)) 193 | ``` 194 | 195 | 默认情况下,每个项目周围都会绘制一个框架,你可以按如下方式删除它: 196 | 197 | ```python 198 | label.setFrameEnabled(False) 199 | ``` 200 | 201 | 除了手动创建布局项外,QGIS还支持布局模板,这些布局模板本质上是将所有项保存到.qpt文件中(使用XML语法)。 202 | 203 | 一旦组合准备就绪(布局项已经创建并添加到组合中),我们就可以继续生成栅格或者矢量输出。 204 | 205 | ### 10.3.1 检查布局有效性 206 | 207 | 布局是由一组互连的项组成的,在修改过程中可能会发生这些连接断开的情况(图例连接到已删除的地图、缺少源文件的图像项等),或者你可能希望对布局项应用自定义约束。[QgsAbstractValidatyCheck](https://qgis.org/pyqgis/master/core/QgsAbstractValidityCheck.html#qgis.core.QgsAbstractValidityCheck)可帮助你实现这一点。 208 | 209 | 基本检查如下: 210 | 211 | ```python 212 | @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) 213 | def my_layout_check(context, feedback): 214 | results = ... 215 | return results 216 | ``` 217 | 218 | 这里有一个检查,每当布局地图项目设置为WEB墨卡托投影时,就会发出警告: 219 | 220 | ```python 221 | @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) 222 | def layout_map_crs_choice_check(context, feedback): 223 | layout = context.layout 224 | results = [] 225 | for i in layout.items(): 226 | if isinstance(i, QgsLayoutItemMap) and i.crs().authid() == 'EPSG:3857': 227 | res = QgsValidityCheckResult() 228 | res.type = QgsValidityCheckResult.Warning 229 | res.title = 'Map projection is misleading' 230 | res.detailedDescription = 'The projection for the map item {} is set to Web Mercator (EPSG:3857) which misrepresents areas and shapes. Consider using an appropriate local projection instead.'.format(i.displayName()) 231 | results.append(res) 232 | 233 | return results 234 | ``` 235 | 236 | 这里有一个更复杂的例子,如果任何布局地图项被设置为CRS,则会发出警告,该CRS仅在该地图项中显示的范围之外有效: 237 | 238 | ```python 239 | @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) 240 | def layout_map_crs_area_check(context, feedback): 241 | layout = context.layout 242 | results = [] 243 | for i in layout.items(): 244 | if isinstance(i, QgsLayoutItemMap): 245 | bounds = i.crs().bounds() 246 | ct = QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), i.crs(), QgsProject.instance()) 247 | bounds_crs = ct.transformBoundingBox(bounds) 248 | 249 | if not bounds_crs.contains(i.extent()): 250 | res = QgsValidityCheckResult() 251 | res.type = QgsValidityCheckResult.Warning 252 | res.title = 'Map projection is incorrect' 253 | res.detailedDescription = 'The projection for the map item {} is set to \'{}\', which is not valid for the area displayed within the map.'.format(i.displayName(), i.crs().authid()) 254 | results.append(res) 255 | 256 | return results 257 | ``` 258 | 259 | ### 10.3.2 导出布局 260 | 261 | 导出布局,必须使用[`QgsLayoutExporter`](https://qgis.org/pyqgis/master/core/QgsLayoutExporter.html#qgis.core.QgsLayoutExporter)类。 262 | 263 | ```python 264 | pdf_path = os.path.join(QgsProject.instance().homePath(), "output.pdf") 265 | 266 | exporter = QgsLayoutExporter(layout) 267 | exporter.exportToPdf(pdf_path, QgsLayoutExporter.PdfExportSettings()) 268 | ``` 269 | 270 | 如果要分别导出到SVG或图像文件,请使用[`exportToSvg()`](https://qgis.org/pyqgis/master/core/QgsLayoutExporter.html#qgis.core.QgsLayoutExporter.exportToSvg)或者[`exportToImage()`](https://qgis.org/pyqgis/master/core/QgsLayoutExporter.html#qgis.core.QgsLayoutExporter.exportToImage)导出图像。 271 | 272 | ### 10.3.3 导出布局图集 273 | 274 | 如果要从布局中导出所有页面(在配置中启用了图集选项),则需要在导出器([QgsLayoutExporter](https://qgis.org/pyqgis/master/core/QgsLayoutExporter.html#qgis.core.QgsLayoutExporter))中使用[altas()](https://qgis.org/pyqgis/master/core/QgsPrintLayout.html#qgis.core.QgsPrintLayout.atlas)方法,并进行少量调整。在以下示例中,页面导出为PNG图像: 275 | 276 | ```python 277 | exporter.exportToImage(layout.atlas(), base_path, 'png', QgsLayoutExporter.ImageExportSettings()) 278 | ``` 279 | 280 | 请注意,输出保存在基本路径文件夹中,使用图集上配置的输出文件名表达式。 281 | 282 | -------------------------------------------------------------------------------- /docs/7-几何处理.md: -------------------------------------------------------------------------------- 1 | # 7 几何处理 2 | 3 | 此页面上的代码片段需要导入以下模块: 4 | 5 | ```python 6 | from qgis.core import ( 7 | QgsGeometry, 8 | QgsGeometryCollection 9 | QgsPoint, 10 | QgsPointXY, 11 | QgsWkbTypes, 12 | QgsProject, 13 | QgsFeatureRequest, 14 | QgsVectorLayer, 15 | QgsDistanceArea, 16 | QgsUnitTypes, 17 | QgsCoordinateTransform, 18 | QgsCoordinateReferenceSystem 19 | ) 20 | ``` 21 | 22 | 表示空间要素的点、线和多边形通常称为几何。在QGIS中,它们用[`QgsGeometry`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry)类来表示 。 23 | 24 | 有时,一种几何实际上是简单(单部件)几何的集合。另一种几何形状称为多部件几何。如果它只包含一种类型的简单几何,我们称之为多点、多线或多多边形。例如,由多个岛组成的国家可以表示为多多边形。 25 | 26 | 几何的坐标可以在任何坐标参考系统(CRS)中。从图层中提取要素时,关联的几何图形将在图层的CRS中具有坐标。 27 | 28 | 有关所有可访问的几何结构和关系说明,请参阅[OGC简单要素访问标准](https://www.ogc.org/standards/sfa),以获取更详细的信息。 29 | 30 | ## 7.1 几何构造 31 | 32 | PyQGIS提供了几种创建几何的选项: 33 | 34 | - 坐标 35 | 36 | ```python 37 | gPnt = QgsGeometry.fromPointXY(QgsPointXY(1,1)) 38 | print(gPnt) 39 | gLine = QgsGeometry.fromPolyline([QgsPoint(1, 1), QgsPoint(2, 2)]) 40 | print(gLine) 41 | gPolygon = QgsGeometry.fromPolygonXY([[QgsPointXY(1, 1), 42 | QgsPointXY(2, 2), QgsPointXY(2, 1)]]) 43 | print(gPolygon) 44 | ``` 45 | 46 | 使用[`QgsPoint`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint)类或[`QgsPointXY`](https://qgis.org/pyqgis/master/core/QgsPointXY.html#qgis.core.QgsPointXY) 类创建坐标。这些类之间的区别在于[`QgsPoint`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint)支持M和Z维度。 47 | 48 | 折线(Linestring)由一系列点表示。 49 | 50 | 多边形由线环列表(即闭合的线段)表示。第一个环是外环(边界),第二个可选项线环是多边形中的孔。请注意,与某些程序不同,QGIS会为你闭合环,因此无需将第一个点复制为最后一个。 51 | 52 | 多部件几何图形更进一步:多点是一个点列表,多线是一个线列表,多多边形是一个多边形列表。 53 | 54 | - WKT 55 | 56 | ```python 57 | gem = QgsGeometry.fromWkt("POINT(3 4)") 58 | print(geom) 59 | ``` 60 | 61 | - WKB 62 | 63 | ```python 64 | g = QgsGeometry() 65 | wkb = bytes.fromhex("010100000000000000000045400000000000001440") 66 | g.fromWkb(wkb) 67 | 68 | #使用WKT打印几何 69 | print(g.asWkt()) 70 | ``` 71 | 72 | ## 7.2 访问几何 73 | 74 | 首先,你应该找出几何类型。[`wkbType()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.wkbType) 方法是其中一种方法。它从[`QgsWkbTypes.Type`](https://qgis.org/pyqgis/master/core/QgsWkbTypes.html#qgis.core.QgsWkbTypes) 枚举中返回一个值。 75 | 76 | ```python 77 | if gPnt.wkbType() == QgsWkbTypes.Point: 78 | print(gPnt.wkbType()) 79 | # output: 1 for Point 80 | if gLine.wkbType() == QgsWkbTypes.LineString: 81 | print(gLine.wkbType()) 82 | # output: 2 for LineString 83 | if gPolygon.wkbType() == QgsWkbTypes.Polygon: 84 | print(gPolygon.wkbType()) 85 | # output: 3 for Polygon 86 | ``` 87 | 88 | 作为替代方案,可以使用[`type()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.type) ,从[`QgsWkbTypes.GeometryType`](https://qgis.org/pyqgis/master/core/QgsWkbTypes.html#qgis.core.QgsWkbTypes) 枚举中返回值。 89 | 90 | 你可以使用[`displayString()`](https://qgis.org/pyqgis/master/core/QgsWkbTypes.html#qgis.core.QgsWkbTypes.displayString) 函数来获取人类可读的几何类型。 91 | 92 | ```python 93 | print(QgsWkbTypes.displayString(gPnt.wkbType())) 94 | # output: 'Point' 95 | print(QgsWkbTypes.displayString(gLine.wkbType())) 96 | # output: 'LineString' 97 | print(QgsWkbTypes.displayString(gPolygon.wkbType())) 98 | # output: 'Polygon' 99 | ``` 100 | 101 | 还有一个辅助函数 [`isMultipart()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.isMultipart)可以确定几何是否是多部件 。 102 | 103 | 从几何中提取信息,每种矢量类型都有访问器函数。以下是如何使用这些访问器的示例: 104 | 105 | ```python 106 | gPnt.asPoint() 107 | # output: 108 | gLine.asPolyline() 109 | # output: [, ] 110 | gPolygon.asPolygon() 111 | # output: [[, , , ]] 112 | ``` 113 | 114 | !!! 提示 115 | 116 | 元组(x,y)不是真正的元组,它们是[`QgsPoint`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint) 对象,可以使用[`x()`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint.x) 和[`y()`](https://qgis.org/pyqgis/master/core/QgsPoint.html#qgis.core.QgsPoint.y)方法访问这些值。 117 | 118 | 对于多部件几何也有类似的访问函数: [`asMultiPoint()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.asMultiPoint),[`asMultiPolyline()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.asMultiPolyline)和[`asMultiPolygon()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.asMultiPolygon)。 119 | 120 | 不用管几何的类型,直接遍历所有几何是可以的,例如: 121 | 122 | ```python 123 | geom = QgsGeometry.fromWkt( 'MultiPoint( 0 0, 1 1, 2 2)' ) 124 | for part in geom.parts(): 125 | print(part.asWkt()) 126 | 127 | # Point (0 0) 128 | # Point (1 1) 129 | # Point (2 2) 130 | ``` 131 | 132 | ```python 133 | geom = QgsGeometry.fromWkt( 'LineString( 0 0, 10 10 )' ) 134 | for part in geom.parts(): 135 | print(part.asWkt()) 136 | 137 | # LineString (0 0, 10 10) 138 | ``` 139 | 140 | ```python 141 | gc = QgsGeometryCollection() 142 | gc.fromWkt('GeometryCollection( Point(1 2), Point(11 12), LineString(33 34, 44 45))') 143 | print(gc[1].asWkt()) 144 | 145 | # Point (11 12) 146 | ``` 147 | 148 | 使用 [`QgsGeometry.parts()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.parts) 方法修改所有几何。 149 | 150 | ```python 151 | geom = QgsGeometry.fromWkt( 'MultiPoint( 0 0, 1 1, 2 2)' ) 152 | for part in geom.parts(): 153 | part.transform(QgsCoordinateTransform( 154 | QgsCoordinateReferenceSystem("EPSG:4326"), 155 | QgsCoordinateReferenceSystem("EPSG:3111"), 156 | QgsProject.instance()) 157 | ) 158 | 159 | print(geom.asWkt()) 160 | 161 | # MultiPoint ((-10334728.12541878595948219 -5360106.25905461423099041),(-10462135.16126426123082638 -5217485.4735023295506835),(-10589399.84444035589694977 -5072021.45942386891692877)) 162 | ``` 163 | 164 | ## 7.3 几何谓词与操作 165 | 166 | QGIS使用GEOS库进行高级几何操作,如几何谓词([`contains()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.contains),[`intersects()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.intersects),...),操作([`combine()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.combine),[`difference()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.difference),...)。它还可以计算几何的几何属性,例如面积(多边形)或长度(多边形和线)。 167 | 168 | 让我们看一个结合遍历指定图层要素,并基于它们的几何执行一些几何计算的示例。下面的代码计算并打印`countries` 图层中每个国家的面积和周长。 169 | 170 | 以下代码假定`layer`是具有多边形要素类型的[`QgsVectorLayer`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer)对象。 171 | 172 | ```python 173 | # 访问'countries'图层 174 | layer = QgsProject.instance().mapLayersByName('countries')[0] 175 | 176 | # 过滤以Z开头的国家,然后获取其要素 177 | query = '"name" LIKE \'Z%\'' 178 | features = layer.getFeatures(QgsFeatureRequest().setFilterExpression(query)) 179 | 180 | 181 | # 现在遍历要素,执行几何计算并打印结果 182 | for f in features: 183 | geom = f.geometry() 184 | name = f.attribute('NAME') 185 | print(name) 186 | print('Area: ', geom.area()) 187 | print('Perimeter: ', geom.length()) 188 | 189 | 190 | # Zambia 191 | # Area: 62.822790653431205 192 | # Perimeter: 50.65232014052552 193 | # Zimbabwe 194 | # Area: 33.41113559136521 195 | # Perimeter: 26.608288555013935 196 | ``` 197 | 198 | 现在,你已经计算并打印了几何图形的面积和周长。但是,你可能会很快注意到这些值很奇怪。这是因为当使用 [`QgsGeometry`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry)类中的[`area()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.area)和[`length()`](https://qgis.org/pyqgis/master/core/QgsGeometry.html#qgis.core.QgsGeometry.length) 方法计算时,面积和周长不会考虑CRS。可以使用更强大的[`QgsDistanceArea`](https://qgis.org/pyqgis/master/core/QgsDistanceArea.html#qgis.core.QgsDistanceArea) 类计算面积和周长,它可以执行基于椭球的计算: 199 | 200 | 以下代码假定`layer`是具有多边形要素类型的[`QgsVectorLayer`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer)对象。 201 | 202 | ```python 203 | d = QgsDistanceArea() 204 | d.setEllipsoid('WGS84') 205 | 206 | layer = QgsProject.instance().mapLayersByName('countries')[0] 207 | 208 | # 过滤以Z开头的国家,然后获取其要素 209 | query = '"name" LIKE \'Z%\'' 210 | features = layer.getFeatures(QgsFeatureRequest().setFilterExpression(query)) 211 | 212 | for f in features: 213 | geom = f.geometry() 214 | name = f.attribute('NAME') 215 | print(name) 216 | print("Perimeter (m):", d.measurePerimeter(geom)) 217 | print("Area (m2):", d.measureArea(geom)) )) 218 | # 打印(“面积(m2):” , d 。measureArea (GEOM )) 219 | 220 | # 计算并重新打印面积,单位为平方公里 221 | print("Area (km2):", d.convertAreaMeasurement(d.measureArea(geom), QgsUnitTypes.AreaSquareKilometers)) 222 | 223 | # Zambia 224 | # Perimeter (m): 5539361.250294601 225 | # Area (m2): 751989035032.9031 226 | # Area (km2): 751989.0350329031 227 | # Zimbabwe 228 | # Perimeter (m): 2865021.3325076113 229 | # Area (m2): 389267821381.6008 230 | # Area (km2): 389267.8213816008 231 | ``` 232 | 233 | 或者,你可能想知道两点之间的距离。 234 | 235 | ```python 236 | d = QgsDistanceArea() 237 | d.setEllipsoid('WGS84') 238 | 239 | # 让我们创造两个点 240 | # 圣诞老人是一个工作狂,他需要放个暑假,让我们看一下他家离特内里费有多远 241 | santa = QgsPointXY(25.847899, 66.543456) 242 | tenerife = QgsPointXY(-16.5735, 28.0443) 243 | 244 | print("Distance in meters: ", d.measureLine(santa, tenerife)) 245 | ``` 246 | 247 | 你可以在QGIS中找到许多算法示例,并使用这些方法来分析和转换矢量数据。以下是一些代码的链接。 248 | 249 | - 使用[`QgsDistanceArea`](https://qgis.org/pyqgis/master/core/QgsDistanceArea.html#qgis.core.QgsDistanceArea)类的距离和面积: [距离矩阵算法](https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/qgis/PointDistance.py) 250 | - [线到多边形算法](https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/qgis/LinesToPolygons.py) 251 | -------------------------------------------------------------------------------- /docs/14-认证基础.md: -------------------------------------------------------------------------------- 1 | # 14 认证基础 2 | 3 | 本节的代码片段需要导入以下模块: 4 | 5 | ```python 6 | from qgis.core import ( 7 | QgsApplication, 8 | QgsRasterLayer, 9 | QgsAuthMethodConfig, 10 | QgsDataSourceUri, 11 | QgsPkiBundle, 12 | QgsMessageLog, 13 | ) 14 | 15 | from qgis.gui import ( 16 | QgsAuthAuthoritiesEditor, 17 | QgsAuthConfigEditor, 18 | QgsAuthConfigSelect, 19 | QgsAuthSettingsWidget, 20 | ) 21 | 22 | from qgis.PyQt.QtWidgets import ( 23 | QWidget, 24 | QTabWidget, 25 | ) 26 | 27 | from qgis.PyQt.QtNetwork import QSslCertificate 28 | ``` 29 | 30 | ## 14.1 介绍 31 | 32 | 认证基础的用户参考可以在用户手册的[“认证系统概述”](https://docs.qgis.org/3.4/en/docs/user_manual/auth_system/auth_overview.html#authentication-overview)中阅读。 33 | 34 | 本章是描述从开发人员角度使用认证系统的最佳实践。 35 | 36 | 在QGIS桌面中,当需要凭证来访问特定资源时,例如当一个图层建立在Postgres数据库连接时,数据提供者就广泛地使用了认证系统。 37 | 38 | QGIS gui库中还有一些部件,插件开发人员可以使用它们轻松地将认证基础集成到代码中: 39 | 40 | - [`QgsAuthConfigEditor`](https://qgis.org/pyqgis/master/gui/QgsAuthConfigEditor.html#qgis.gui.QgsAuthConfigEditor) 41 | - [`QgsAuthConfigSelect`](https://qgis.org/pyqgis/master/gui/QgsAuthConfigSelect.html#qgis.gui.QgsAuthConfigSelect) 42 | - [`QgsAuthSettingsWidget`](https://qgis.org/pyqgis/master/gui/QgsAuthSettingsWidget.html#qgis.gui.QgsAuthSettingsWidget) 43 | 44 | 可以从认证基础[测试代码](https://github.com/qgis/QGIS/blob/master/tests/src/python/test_qgsauthsystem.py)中学习良好的代码引用。 45 | 46 | !!! 警告 warning 47 | 48 | 由于认证基础设计过程中考虑到安全约束,只有选定的内部方法集可以暴露给Python。 49 | 50 | ## 14.2 词汇表 51 | 52 | 以下是本章中最常见对象的一些定义。 53 | 54 | - 主密码 55 | 56 | 允许访问和解密存储在QGIS认证中凭据的密码 57 | 58 | - 认证数据库 59 | 60 | 一个[主密码](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Master-Password)加密后的SQLite数据库`qgis-auth.db` ,其中[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Configuration)存储在这里。例如用户名/密码,个人证书和密钥,证书颁发机构 61 | 62 | - 认证数据库 63 | 64 | [认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Database) 65 | 66 | - 认证配置 67 | 68 | 一组认证数据,取决于[认证方法](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Method)。例如,基本认证方法存储一对用户名/密码。 69 | 70 | - 认证方法 71 | 72 | 用于获取认证的特定方法。每个方法都有自己的协议,用于获取认证级别。每个方法都是在QGIS认证基础初始化期间动态加载的共享库。 73 | 74 | ## 14.3 QgsAuthManager入口 75 | 76 | 单例类[`QgsAuthManager`](https://qgis.org/pyqgis/master/core/QgsAuthManager.html#qgis.core.QgsAuthManager)是使用存储在QGIS[认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-DB)加密证书的入口,即,在当前[用户资料](https://docs.qgis.org/testing/en/docs/user_manual/introduction/qgis_configuration.html#user-profiles)文件夹下的`qgis-auth.db`文件。 77 | 78 | 此类负责用户交互:通过设置主密码或透明地使用它来访问加密的存储信息。 79 | 80 | ### 14.3.1 初始化管理器并设置主密码 81 | 82 | 以下代码段提供了一个示例,设置主密码来打开对认证设置的访问权限。代码注释对于理解代码非常重要。 83 | 84 | ```python 85 | authMgr = QgsApplication.authManager() 86 | # 检查QgsAuthManager是否已经初始化... QgsAuthManager.init()的额外作用是设置了AuthDbPath。 87 | # QgsAuthManager.init()在QGIS应用程序初始化期间执行,因此通常不需要直接调用它。 88 | if authMgr.authenticationDatabasePath(): 89 | if authMgr.masterPasswordIsSet(): 90 | msg = 'Authentication master password not recognized' 91 | assert authMgr.masterPasswordSame( "your master password" ), msg 92 | else: 93 | msg = 'Master password could not be set' 94 | # 验证verify参数密码的哈希值是否已保存在认证数据库中 95 | assert authMgr.setMasterPassword( "your master password",verify=True), msg 96 | else: 97 | # 在qgis环境之外,例如在测试环境中=>在数据库初始化之前设置环境变量 98 | os.environ['QGIS_AUTH_DB_DIR_PATH'] = "/path/where/located/qgis-auth.db" 99 | msg = 'Master password could not be set' 100 | assert authMgr.setMasterPassword("your master password", True), msg 101 | authMgr.init( "/path/where/located/qgis-auth.db" ) 102 | ``` 103 | 104 | ### 14.3.2 使用新的认证配置项填充认证数据库 105 | 106 | 任何存储的凭证都是[`QgsAuthMethodConfig`](https://qgis.org/pyqgis/master/core/QgsAuthMethodConfig.html#qgis.core.QgsAuthMethodConfig)类的[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Configuration)实例——使用唯一字符串访问: 107 | 108 | ```python 109 | authcfg = 'fm1s770' 110 | ``` 111 | 112 | 该字符串是在使用QGIS API或GUI创建条目时自动生成的,但如果配置必须在一个组织内的多个用户之间共享(不同的凭证),将其手动设置为一个已知值可能是有用的。 113 | 114 | [`QgsAuthMethodConfig`](https://qgis.org/pyqgis/master/core/QgsAuthMethodConfig.html#qgis.core.QgsAuthMethodConfig)是任何[认证方法](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Method)的基类。任何认证方法集都会配置哈希映射,其中存储认证信息。下面是一个有用的代码片段,用于存储alice用户的PKI路径凭证: 115 | 116 | ```python 117 | authMgr = QgsApplication.authManager() 118 | # 设置 alice PKI 数据 119 | p_config = QgsAuthMethodConfig() 120 | p_config.setName("alice") 121 | p_config.setMethod("PKI-Paths") 122 | p_config.setUri("https://example.com") 123 | p_config.setConfig("certpath", "path/to/alice-cert.pem" ) 124 | p_config.setConfig("keypath", "path/to/alice-key.pem" ) 125 | # 检查方法参数是否正确设置 126 | assert p_config.isValid() 127 | 128 | # 在认证数据库中注册alice数据,返回存储的‘authcfg’配置 129 | authMgr.storeAuthenticationConfig(p_config) 130 | newAuthCfgId = p_config.id() 131 | assert (newAuthCfgId) 132 | ``` 133 | 134 | #### 14.3.2.1 可用的认证方法 135 | 136 | [认证方法](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Method)在认证管理器初始化时动态加载。可用的认证方法列表如下: 137 | 138 | 1. `Basic` 用户和密码验证 139 | 2. `EsriToken` ESRI token 基础认证 140 | 3. `Identity-Cert` 身份证书认证 141 | 4. `PKI-Paths` PKI路径认证 142 | 5. `PKI-PKCS#12` PKI PKCS#12认证 143 | 144 | #### 14.3.2.2 填充权限 145 | 146 | ```python 147 | authMgr = QgsApplication.authManager() 148 | # 添加权限 149 | cacerts = QSslCertificate.fromPath( "/path/to/ca_chains.pem" ) 150 | assert cacerts is not None 151 | # 存储 CA 152 | authMgr.storeCertAuthorities(cacerts) 153 | # 重建CA缓存 154 | authMgr.rebuildCaCertsCache() 155 | authMgr.rebuildTrustedCaCertsCache() 156 | ``` 157 | 158 | #### 14.3.2.3 使用QgsPkiBundle管理PKI 159 | 160 | [QgsPkiBundle](https://qgis.org/pyqgis/master/core/QgsPkiBundle.html#qgis.core.QgsPkiBundle)类是一个方便的类,用于打包由SslCert、SslKey和CA链组成的PKI包。下面是一个获得密码保护的片段: 161 | 162 | ```python 163 | # 密钥与密码一起添加alice证书 164 | caBundlesList = [] 165 | bundle = QgsPkiBundle.fromPemPaths( "/path/to/alice-cert.pem", 166 | "/path/to/alice-key_w-pass.pem", 167 | "unlock_pwd", 168 | caBundlesList ) 169 | assert bundle is not None 170 | # 你可以检查它是否正确 171 | # bundle.isValid() 172 | ``` 173 | 174 | 请参阅[`QgsPkiBundle`](https://qgis.org/pyqgis/master/core/QgsPkiBundle.html#qgis.core.QgsPkiBundle)类文档,从包中提取证书/密钥/ CA. 175 | 176 | ### 14.3.3 从认证数据库中删除条目 177 | 178 | 我们可以使用`authcfg`标识符从[认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Database)中删除条目: 179 | 180 | ```python 181 | authMgr = QgsApplication.authManager() 182 | authMgr.removeAuthenticationConfig( "authCfg_Id_to_remove" ) 183 | ``` 184 | 185 | ### 14.3.4 使用QgsAuthManager扩展authcfg 186 | 187 | 使用存储在[认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-DB)中的[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Config)的最好方法是用唯一的标识符`authcfg`来引用它。扩展,意味着把它从一个标识符转换成一套完整的凭证。使用存储的[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Config)的最佳做法,是让它由认证管理器自动管理。存储配置的常见用途是连接到一个启用了认证的服务,如WMS、WFS或数据库连接。 188 | 189 | !!! 提示 190 | 191 | 并非所有的QGIS数据提供者都与认证基础集成。每个认证方法都是从基类[QgsAuthMethod](https://qgis.org/pyqgis/master/core/QgsAuthMethod.html#qgis.core.QgsAuthMethod)派生出来的,并支持一组不同的提供者。例如,[certIdentity()](https://qgis.org/pyqgis/master/core/QgsAuthManager.html#qgis.core.QgsAuthManager.certIdentity)方法支持以下的提供者列表: 192 | ```python 193 | authM = QgsApplication.authManager() 194 | print(authM.authMethod("Identity-Cert").supportedDataProviders()) 195 | 196 | # ['ows', 'wfs', 'wcs', 'wms', 'postgres'] 197 | ``` 198 | 例如,要使用`authcfg = 'fm1s770'`标识的存储凭证访问WMS服务,我们只需在数据源URL中使用`authcfg`,如以下代码: 199 | ```python 200 | authCfg = 'fm1s770' 201 | quri = QgsDataSourceUri() 202 | quri.setParam("layers", 'usa:states') 203 | quri.setParam("styles", '') 204 | quri.setParam("format", 'image/png') 205 | quri.setParam("crs", 'EPSG:4326') 206 | quri.setParam("dpiMode", '7') 207 | quri.setParam("featureCount", '10') 208 | quri.setParam("authcfg", authCfg) # <---- authCfg url 配置 209 | quri.setParam("contextualWMSLegend", '0') 210 | quri.setParam("url", 'https://my_auth_enabled_server_ip/wms') 211 | rlayer = QgsRasterLayer(str(quri.encodedUri(), "utf-8"), 'states', 'wms') 212 | ``` 213 | 以上案例,`wms`提供者在设置HTTP连接之前,会将`authcfg` URI参数与凭证展开。 214 | 215 | !!! 警告 warning 216 | 217 | 开发者必须将`authcfg`扩展留给[QgsAuthManager](https://qgis.org/pyqgis/master/core/QgsAuthManager.html#qgis.core.QgsAuthManager),这样,它就能确保扩展不会太早完成。 218 | 219 | 通常情况下,使用[QgsDataSourceURI](https://qgis.org/pyqgis/master/core/QgsDataSourceUri.html#qgis.core.QgsDataSourceUri)类构建的URI字符串被用来设置数据源,其方式如下: 220 | 221 | ```python 222 | authCfg = 'fm1s770' 223 | quri = QgsDataSourceUri("my WMS uri here") 224 | quri.setParam("authcfg", authCfg) 225 | rlayer = QgsRasterLayer( quri.uri(False), 'states', 'wms') 226 | ``` 227 | 228 | !!! 提示 229 | 230 | `False`参数很重要,可以避免URL中已存在的`authcfg` 231 | 232 | #### 14.3.4.1 使用其它数据提供者的PKI例子 233 | 234 | 其它例子可以在QGIS测试文件夹中直接读取,如[test_authmanager_pki_ows](https://github.com/qgis/QGIS/blob/master/tests/src/python/test_authmanager_pki_ows.py)或[test_authmanager_pki_postgres](https://github.com/qgis/QGIS/blob/master/tests/src/python/test_authmanager_pki_postgres.py). 235 | 236 | ## 14.4 调整插件使用认证基础 237 | 238 | 许多第三方插件使用httplib2或其他Python网络库来管理HTTP连接,而不是与[`QgsNetworkAccessManager`](https://qgis.org/pyqgis/master/core/QgsNetworkAccessManager.html#qgis.core.QgsNetworkAccessManager)及其相关认证基础集成。 239 | 240 | 为了便于集成,我们创建了一个名为`NetworkAccessManager`的Python帮助函数。它的代码可以在[这里](https://github.com/rduivenvoorde/pdokservicesplugin/blob/master/networkaccessmanager.py)找到。 241 | 242 | 此帮助程序类可以在以下代码段中使用: 243 | 244 | ```python 245 | http = NetworkAccessManager(authid="my_authCfg", exception_class=My_FailedRequestError) 246 | try: 247 | response, content = http.request( "my_rest_url" ) 248 | except My_FailedRequestError, e: 249 | # 处理异常 250 | pass 251 | ``` 252 | 253 | ## 14.5 认证GUI 254 | 255 | 本节列出了可用于在自定义接口中集成认证基础的可用GUI。 256 | 257 | ### 14.5.1 GUI选择证书 258 | 259 | 如果需要从存储在[认证数据库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-DB)中选择[认证配置](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Configuration),则可以在GUI类`QgsAuthConfigSelect`中使用。 260 | 261 | ![QgsAuthConfigSelect.png](./assets/QgsAuthConfigSelect.png) 262 | 263 | 可在以下代码段中使用: 264 | 265 | ```python 266 | # 创建一个QgsAuthConfigSelect GUI实例,该实例与`parent`有父子关系 267 | parent = QWidget() # GUI父级控件 268 | gui = QgsAuthConfigSelect( parent, "postgres" ) 269 | # 在一个新标签中添加上述创建的GUI控件。 270 | tabGui = QTabWidget() 271 | tabGui.insertTab( 1, gui, "Configurations" ) 272 | ``` 273 | 274 | 以上示例摘自QGIS[源代码](https://github.com/qgis/QGIS/blob/master/src/providers/postgres/qgspgnewconnection.cpp#L42)。GUI构造函数的第二个参数引用数据提供者类型。参数用于限制与指定提供者兼容的[认证方法](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/authentication.html#term-Authentication-Method)。 275 | 276 | ### 14.5.2 认证编辑器 277 | 278 | 用于管理凭据、权限和访问认证实用程序的完整GUI由[`QgsAuthEditorWidgets`](https://qgis.org/pyqgis/master/gui/QgsAuthEditorWidgets.html#qgis.gui.QgsAuthEditorWidgets)类管理。 279 | 280 | ![QgsAuthEditorWidgets.png](./assets/QgsAuthEditorWidgets.png) 281 | 282 | 可在以下代码段中使用: 283 | 284 | ```python 285 | # 创建一个QgsAuthConfigSelect GUI实例,该实例与`parent`有父子关系 286 | parent = QWidget() # GUI父级控件 287 | gui = QgsAuthConfigSelect( parent ) 288 | gui.show() 289 | ``` 290 | 291 | 可以在相关的[测试](https://github.com/qgis/QGIS/blob/master/tests/src/python/test_qgsauthsystem.py#L80)代码中找到一个综合的例子。 292 | 293 | ### 14.5.3 机构(证书颁发)编辑器 294 | 295 | 仅用于管理机构的GUI由[`QGSAuthoritiesEditor`](https://qgis.org/pyqgis/master/gui/QgsAuthAuthoritiesEditor.html#qgis.gui.QgsAuthAuthoritiesEditor)类管理。 296 | 297 | ![QgsAuthAuthoritiesEditor.png](./assets/QgsAuthAuthoritiesEditor.png) 298 | 299 | 可在以下代码段中使用: 300 | 301 | ```python 302 | # 创建一个QgsAuthConfigSelect GUI实例,该实例与`parent`有父子关系 303 | parent = QWidget() # GUI父级控件 304 | gui = QgsAuthAuthoritiesEditor( parent ) 305 | gui.show() 306 | ``` 307 | -------------------------------------------------------------------------------- /docs/3-加载图层.md: -------------------------------------------------------------------------------- 1 | # 3 加载图层 2 | 3 | 此页面上的代码片段需要导入以下模块: 4 | 5 | ```python 6 | import os # pyqgis控制台同样需要 7 | from qgis.core import ( 8 | QgsVectorLayer 9 | ) 10 | ``` 11 | 12 | 让我们使用数据打开一些图层。QGIS可识别矢量和栅格图层。此外,自定义图层类型也可以使用,但我们不打算在此讨论。 13 | 14 | ## 3.1 矢量图层 15 | 16 | 创建一个矢量图层实例,指定图层的数据源标识、图层名称和提供者名称: 17 | 18 | ```python 19 | # 获取shapefile的路径,例如:/home/project/data/ports.shp 20 | path_to_airports_layer = "testdata/airports.shp" 21 | 22 | # 格式为: 23 | # vlayer = QgsVectorLayer(data_source, layer_name, provider_name) 24 | 25 | vlayer = QgsVectorLayer(path_to_ports_layer, "Airports layer", "ogr") 26 | 27 | if not vlayer.isValid(): 28 | print("图层加载失败!") 29 | else: 30 | QgsProject.instance().addMapLayer(vlayer) 31 | ``` 32 | 33 | 数据源标识是一个字符串,它特定于每个矢量数据提供者。图层名称用于图层列表部件。检查图层是否已成功加载非常重要。如果不是,则返回无效的图层实例。 34 | 35 | `geopackage`矢量图层: 36 | 37 | ```python 38 | # 获取geopackage的路径,例如:/home/project/data/data.gpkg 39 | path_to_gpkg = os.path.join(QgsProject.instance().homePath(), "data", "data.gpkg") 40 | # 追加图层名称 41 | gpkg_places_layer = path_to_gpkg + "|layername=places" 42 | # 例如:gpkg_places_layer = "/home/project/data/data.gpkg|layername=places" 43 | vlayer = QgsVectorLayer(gpkg_places_layer, "Places layer", "ogr") 44 | if not vlayer.isValid(): 45 | print("图层加载失败!") 46 | else: 47 | QgsProject.instance().addMapLayer(vlayer) 48 | ``` 49 | 50 | 在QGIS中打开并显示矢量图层的最快方式是使用[`QgisInterface`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)类的 [`addVectorLayer()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addVectorLayer) 方法: 51 | 52 | ```python 53 | vlayer = iface.addVectorLayer(path_to_ports_layer, "Ports layer", "ogr") 54 | if not vlayer: 55 | print("图层加载失败!") 56 | ``` 57 | 58 | 这将创建一个新图层,并将其添加到当前QGIS项目中(使其显示在图层列表中)。该函数返回图层实例,如果无法加载图层则返回`None` 。 59 | 60 | 以下列表展示了如何使用矢量数据提供者访问各种数据源: 61 | 62 | - GDAL 库(Shapefile和许多其他文件格式)——数据源是文件的路径: 63 | 64 | - Shapefile: 65 | 66 | ```python 67 | vlayer = QgsVectorLayer("/path/to/shapefile/file.shp", "layer_name_you_like", "ogr") 68 | QgsProject.instance().addMapLayer(vlayer) 69 | ``` 70 | 71 | - dxf(注意数据源uri中的内部选项): 72 | 73 | ```python 74 | uri = "/path/to/dxffile/file.dxf|layername=entities|geometrytype=Point" 75 | vlayer = QgsVectorLayer(uri, "layer_name_you_like", "ogr") 76 | QgsProject.instance().addMapLayer(vlayer) 77 | ``` 78 | 79 | - PostGIS 数据库——数据源是一个字符串,其中包含与PostgreSQL数据库创建连接所需的所有信息。 80 | 81 | [`QgsDataSourceUri`](https://qgis.org/pyqgis/master/core/QgsDataSourceUri.html#qgis.core.QgsDataSourceUri)类可以为你生成这个字符串。请注意,必须在编译QGIS时支持Postgres,否则此提供者不可用: 82 | 83 | ```python 84 | uri = QgsDataSourceUri() 85 | # 设置主机,端口,数据库名称,用户名和密码 86 | uri.setConnection("localhost", "5432", "dbname", "johny", "xxx") 87 | # 设置数据库架构,表名,几何列和可选项(WHERE 语句) 88 | uri.setDataSource("public", "roads", "the_geom", "cityid = 2643", "primary_key_field") 89 | vlayer = QgsVectorLayer(uri.uri(False), "layer name you like", "postgres") 90 | ``` 91 | 92 | : !!! 提示 93 | 94 | `uri.uri(False)`中的`False`参数可以防止扩展认证配置参数,如果你没有使用任何身份验证配置,则此参数不会产生任何差异。 95 | 96 | - CSV或其他分隔的文本文件——打开一个用分号作为分隔符的文件,X坐标使用字段“x”,Y坐标使用字段“y”: 97 | 98 | ```python 99 | uri = "file://{}/testdata/delimited_xy.csv?delimiter={}&xField={}&yField={}".format(os.getcwd(), ";", "x", "y") 100 | vlayer = QgsVectorLayer(uri, "layer name you like", "delimitedtext") 101 | QgsProject.instance().addMapLayer(vlayer) 102 | ``` 103 | 104 | : !!! 提示 105 | 106 | 提供者的字符串作为一个URL,因此路径必须以file://为前缀。它还允许WKT((well known text)格式的几何作为`x`和`y`的替代字段,并允许指定坐标参考系统。例如: 107 | 108 | ```python 109 | uri = "file:///some/path/file.csv?delimiter={}&crs=epsg:4723&wktField={}".format(";", "shape") 110 | ``` 111 | 112 | - GPX文件——“gpx”数据提供者从gpx文件中读取轨迹,路线和路点。要打开文件,需要在url中指定类型(track / route / waypoint): 113 | 114 | ```python 115 | uri = "path/to/gpx/file.gpx?type=track" 116 | vlayer = QgsVectorLayer(uri, "layer name you like", "gpx") 117 | QgsProject.instance().addMapLayer(vlayer) 118 | ``` 119 | 120 | - SpatiaLite数据库——与PostGIS数据库类似, [`QgsDataSourceUri`](https://qgis.org/pyqgis/master/core/QgsDataSourceUri.html#qgis.core.QgsDataSourceUri)可用于生成数据源标识: 121 | 122 | ```python 123 | uri = QgsDataSourceUri() 124 | uri.setDatabase('/home/martin/test-2.3.sqlite') 125 | schema = '' 126 | table = 'Towns' 127 | geom_column = 'Geometry' 128 | uri.setDataSource(schema, table, geom_column) 129 | display_name = 'Towns' 130 | vlayer = QgsVectorLayer(uri.uri(), display_name, 'spatialite') 131 | QgsProject.instance().addMapLayer(vlayer) 132 | ``` 133 | 134 | - 基于MySQL WKB的几何,通过GDAL——数据源是连接表的字符串: 135 | 136 | ```python 137 | uri = "MySQL:dbname,host=localhost,port=3306,user=root,password=xxx|layername=my_table" 138 | vlayer = QgsVectorLayer( uri, "my table", "ogr" ) 139 | QgsProject.instance().addMapLayer(vlayer) 140 | ``` 141 | 142 | - WFS连接:连接使用URI定义并使用`WFS`提供者: 143 | 144 | ```python 145 | uri = "https://demo.geo-solutions.it/geoserver/ows?service=WFS&version=1.1.0&request=GetFeature&typename=geosolutions:regioni" 146 | vlayer = QgsVectorLayer(uri, "my wfs layer", "WFS") 147 | ``` 148 | 可以使用标准库`urllib`创建uri: 149 | ```python 150 | import urllib 151 | 152 | params = { 153 | 'service': 'WFS', 154 | 'version': '1.1.0', 155 | 'request': 'GetFeature', 156 | 'typename': 'geosolutions:regioni', 157 | 'srsname': "EPSG:4326" 158 | } 159 | uri2 = 'https://demo.geo-solutions.it/geoserver/ows?' + urllib.parse.unquote(urllib.parse.urlencode(params)) 160 | ``` 161 | 162 | : !!! 提示 163 | 164 | 可以通过调用[`QgsVectorLayer`](https://qgis.org/pyqgis/master/core/QgsVectorLayer.html#qgis.core.QgsVectorLayer)的 [`setDataSource()`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.setDataSource) 方法更改现有图层的数据源,如下面例子: 165 | 166 | ```python 167 | uri = "https://demo.geo-solutions.it/geoserver/ows?service=WFS&version=1.1.0&request=GetFeature&typename=geosolutions:regioni" 168 | provider_options = QgsDataProvider.ProviderOptions() 169 | # 使用项目的转换上下文 170 | provider_options.transformContext = QgsProject.instance().transformContext() 171 | vlayer.setDataSource(uri, "layer name you like", "WFS", provider_options) 172 | 173 | del(vlayer) 174 | ``` 175 | 176 | 177 | ## 3.2 栅格图层 178 | 179 | 访问栅格文件,用到了GDAL库。它支持多种文件格式。如果你在打开某些文件时遇到问题,请检查你的GDAL是否支持(默认情况下并非所有格式都可用)。从文件加载栅格,需要指定其文件名和显示名称: 180 | 181 | ```python 182 | # 获取tif文件的路径,例如:/home/project/data/srtm.tif 183 | path_to_tif = "qgis-projects/python_cookbook/data/srtm.tif" 184 | rlayer = QgsRasterLayer(path_to_tif, "SRTM layer name") 185 | if not rlayer.isValid(): 186 | print("图层加载失败!") 187 | ``` 188 | 189 | 从`geopackage`中加载栅格: 190 | 191 | ```python 192 | # 获取geopackage的路径,例如:/home/project/data/data.gpkg 193 | path_to_gpkg = os.path.join(os.getcwd(), "testdata", "sublayers.gpkg") 194 | # gpkg_raster_layer = "GPKG:/home/project/data/data.gpkg:srtm" 195 | gpkg_raster_layer = "GPKG:" + path_to_gpkg + ":srtm" 196 | rlayer = QgsRasterLayer(gpkg_raster_layer, "layer name you like", "gdal") 197 | if not rlayer.isValid(): 198 | print("图层加载失败!") 199 | ``` 200 | 201 | 与矢量图层类似,可以使用[`QgisInterface`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)对象的`addRasterLayer`函数加载栅格图层: 202 | 203 | ```python 204 | iface.addRasterLayer("/path/to/raster/file.tif", "layer name you like") 205 | ``` 206 | 207 | 这将创建一个新图层,并将其添加到当前项目中(使其显示在图层列表中)。 208 | 209 | ### 3.2.1 加载PostGIS栅格图层 210 | 211 | PostGIS栅格图层,类似于PostGIS矢量图层,可以使用URI字符串添加到项目中。为数据库连接参数保留可复用字典是有效的。这样可以轻松编辑适用连接的字典。使用`postgresraster`提供者的元数据对象将字典编码为URI。之后,可以将栅格图层添加到项目中。 212 | 213 | ```python 214 | uri_config = { 215 | # 数据库参数 216 | 'dbname':'gis_db', # 数据库名称 217 | 'host':'localhost', # IP地址或者localhost 218 | 'port':'5432', # 端口 219 | 'sslmode':QgsDataSourceUri.SslDisable, # SslAllow, SslPrefer, SslRequire, SslVerifyCa, SslVerifyFull 220 | # 如果存储在authcfg或服务中,则不需要用户名和密码 221 | 'authcfg':'QconfigId', # QGIS认证数据库ID包含连接信息 222 | 'service': None, # 连接数据库的服务 223 | 'username':None, # 用户名 224 | 'password':None, # 密码 225 | # 数据库表和栅格字段信息 226 | 'schema':'public', # 数据库架构 227 | 'table':'my_rasters', # 数据库表名称 228 | 'geometrycolumn':'rast',# 栅格字段名称 229 | 'sql':None, # WHERE查询语句,应该放在字符串末尾。 230 | 'key':None, # 数据库表主键 231 | 'srid':None, # 字符串,指定坐标参考系 232 | 'estimatedmetadata':'False', # A boolean value telling if the metadata is estimated. 233 | 'type':None, # WKT字符串,指定WKB类型 234 | 'selectatid':None, # 设置为True可禁用按要素ID进行的选择。 235 | 'options':None, # 不在此列表中的其他PostgreSQL连接选项。 236 | 'enableTime': None, 237 | 'temporalDefaultTime': None, 238 | 'temporalFieldIndex': None, 239 | 'mode':'2', # GDAL参数, 2 unions raster tiles, 1 adds tiles separately (may require user input) 240 | } 241 | # 删除空参数 242 | uri_config = {key:val for key, val in uri_config.items() if val is not None} 243 | # 获取数据提供者和配置的元数据 244 | md = QgsProviderRegistry.instance().providerMetadata('postgresraster') 245 | uri = QgsDataSourceUri(md.encodeUri(uri_config)) 246 | 247 | # 加载栅格图层到项目 248 | rlayer = iface.addRasterLayer(uri.uri(False), "raster layer name", "postgresraster") 249 | ``` 250 | 251 | ### 3.2.2 加载WCS服务栅格图层: 252 | 253 | ```python 254 | layer_name = 'nurc:mosaic' 255 | uri = "https://demo.geo-solutions.it/geoserver/ows?identifier={}".format(layer_name) 256 | rlayer = QgsRasterLayer(uri, 'my wcs layer', 'wcs') 257 | ``` 258 | 259 | 以下是WCS URI可以包含的参数说明: 260 | 261 | WCS URI由 **键=值** 对组成,分隔符:`&`。它与URL中的查询字符串格式相同,编码方式相同。[`QgsDataSourceUri`](https://qgis.org/pyqgis/master/core/QgsDataSourceUri.html#qgis.core.QgsDataSourceUri) 可以用于构造URI以确保正确编码特殊字符。 262 | 263 | - **url**(必填):WCS服务器URL。不要在URL中使用VERSION,因为每个版本的WCS对 **GetCapabilities** 版本使用不同的参数名称,请参阅param版本。 264 | - **identifier**(必填):覆盖范围名称 265 | - **time**(可选):时间位置或时间段(beginPosition / endPosition / timeResolution) 266 | - **format**(可选):支持的格式名称。默认是第一个支持的格式,其名称为tif或第一个支持的格式。 267 | - **crs**(可选):CRS格式为AUTHORITY:ID,例如,EPSG:4326。默认为EPSG:4326(如果支持)或第一个支持的CRS。 268 | - **username**(可选):基本身份验证的用户名。 269 | - **password**(可选):基本身份验证的密码。 270 | - **IgnoreGetMapUrl**(可选,hack):如果指定(设置为1),则忽略GetCapabilities公布的GetCoverage URL。如果未正确配置服务器,则可能需要。 271 | - **InvertAxisOrientation**(可选,hack):如果指定(设置为1),则在GetCoverage请求中切换轴。如果服务器使用错误的轴顺序,则可能需要地理CRS。 272 | - **IgnoreAxisOrientation**(可选,hack):如果指定(设置为1),则不要根据地理CRS的WCS标准反转轴方向。 273 | - **cache**(可选):缓存加载控制,如QNetworkRequest :: CacheLoadControl中所述,但如果使用AlwaysCache失败,请求将重新发送为PreferCache。允许的值:AlwaysCache,PreferCache,PreferNetwork,AlwaysNetwork。默认为AlwaysCache。 274 | 275 | 另外,你可以从WMS服务器加载栅格图层。但是目前无法从API访问GetCapabilities响应——你必须知道所需的图层: 276 | 277 | ```python 278 | urlWithParams = 'url=http://irs.gis-lab.info/?layers=landsat&styles=&format=image/jpeg&crs=EPSG:4326' 279 | rlayer = QgsRasterLayer(urlWithParams, 'some layer name', 'wms') 280 | if not rlayer.isValid(): 281 | print("图层加载失败!") 282 | ``` 283 | 284 | ## 3.3 QgsProject 实例 285 | 286 | 如果你想使用打开的图层进行渲染,请不要忘记将它们添加到[`QgsProject`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)实例中。[`QgsProject`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject)实例拥有图层的所有权,可以通过其唯一ID从应用程序的任何部分访问它们。从项目中删除图层时,它也会被删除。用户可以在QGIS接口中删除图层,也可以通过Python使用[`removeMapLayer()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.removeMapLayer)方法删除图层。 287 | 288 | 使用[`addMapLayer()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.addMapLayer)方法将图层添加到当前项目: 289 | 290 | ```python 291 | QgsProject.instance().addMapLayer(rlayer) 292 | ``` 293 | 294 | 在绝对位置添加图层: 295 | 296 | ```python 297 | # 首先添加图层但不显示 298 | QgsProject.instance().addMapLayer(rlayer, False) 299 | # 在项目中获取图层树的根图层组 300 | layerTree = iface.layerTreeCanvasBridge().rootGroup() 301 | # 第一个参数是一个从0开始的数字,-1表示结束 302 | layerTree.insertChildNode(-1, QgsLayerTreeLayer(rlayer)) 303 | ``` 304 | 305 | 如果要删除图层,使用[`removeMapLayer()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.removeMapLayer)方法: 306 | 307 | ```python 308 | QgsProject.instance().removeMapLayer(rlayer.id()) 309 | ``` 310 | 311 | 在上面的代码中,传递了图层ID(你可以调用图层的[`id()`](https://qgis.org/pyqgis/master/core/QgsMapLayer.html#qgis.core.QgsMapLayer.id)方法),但是你也可以传递图层对象本身。 312 | 313 | 获取已加载图层和图层ID的列表,使用[`mapLayers()`](https://qgis.org/pyqgis/master/core/QgsProject.html#qgis.core.QgsProject.mapLayers)方法: 314 | 315 | ```python 316 | QgsProject.instance().mapLayers() 317 | ``` 318 | 319 | -------------------------------------------------------------------------------- /docs/9-使用地图画布.md: -------------------------------------------------------------------------------- 1 | 本节代码片段需要导入以下模块: 2 | 3 | ```python 4 | from qgis.PyQt.QtGui import ( 5 | QColor, 6 | ) 7 | 8 | from qgis.PyQt.QtCore import Qt, QRectF 9 | 10 | from qgis.PyQt.QtWidgets import QMenu 11 | 12 | from qgis.core import ( 13 | QgsVectorLayer, 14 | QgsPoint, 15 | QgsPointXY, 16 | QgsProject, 17 | QgsGeometry, 18 | QgsMapRendererJob, 19 | QgsWkbTypes, 20 | ) 21 | 22 | from qgis.gui import ( 23 | QgsMapCanvas, 24 | QgsVertexMarker, 25 | QgsMapCanvasItem, 26 | QgsMapMouseEvent, 27 | QgsRubberBand, 28 | ) 29 | ``` 30 | 31 | # 9 使用地图画布 32 | 33 | 地图画布控件可能是QGIS中最重要的控件,因为它显示了由重叠地图图层组成的地图,并允许与地图和图层进行交互。画布始终显示由当前画布范围定义地图的一部分。通过使用 **地图工具** 完成交互:有平移,缩放,识别图层,测量,矢量编辑等工具。与其他图形程序类似,总有一个工具处于活动状态,用户可以在可用工具之间切换。 34 | 35 | 地图画布由`qgis.gui`模块中的[`QgsMapCanvas`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas)类实现。该类基于Qt Graphics View框架。该框架通常提供场景和视图,其中放置自定义图形项,并且用户可以与它们交互。我们假设你对Qt足够熟悉,了解图形场景,视图和项的概念。如果没有,请阅读[框架概述](https://doc.qt.io/qt-5/graphicsview.html)。 36 | 37 | 无论何时平移,放大/缩小(或触发刷新的其他操作)地图,地图都会在当前范围内再次渲染。图层将渲染为图像(使用[`QgsMapRendererJob`](https://qgis.org/pyqgis/master/core/QgsMapRendererJob.html#qgis.core.QgsMapRendererJob)类),并显示在画布上。[`QgsMapCanvas`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas)类还控制渲染图的刷新。除了作为背景的项,可能还有更多的 **地图画布项** 。 38 | 39 | 典型的地图画布项是橡皮条(用于测量,矢量编辑等)或顶点标记。画布项通常用于给地图工具提供视觉反馈,例如,在创建新多边形时,地图工具会创建一个橡皮条画布项,显示多边形的当前形状。所有地图画布项都是[`QgsMapCanvasItem`](https://qgis.org/pyqgis/master/gui/QgsMapCanvasItem.html#qgis.gui.QgsMapCanvasItem) 的子类,它为基类`QGraphicsItem`对象添加了更多功能。 40 | 41 | 总而言之,地图画布架构包含三个概念: 42 | 43 | - 地图画布——用于浏览地图 44 | - 地图画布项——可以在地图画布上显示的其他项 45 | - 地图工具——用于与地图画布交互 46 | 47 | ## 9.1 嵌入地图画布 48 | 49 | 地图画布是一个控件,就像任何其他Qt控件一样,因此使用它就像创建和显示它一样简单 50 | 51 | ```python 52 | canvas = QgsMapCanvas() 53 | canvas.show() 54 | ``` 55 | 56 | 这将生成一个带有地图画布的独立窗口。它也可以嵌入到现有的控件或窗口中。使用`.ui`文件和Qt设计师时,在表单上放置一个`QWidget`并将其提升为新类:设置`QgsMapCanvas`为类名并设置`qgis.gui`为头文件。`pyuic5`工具将搞定它(译者注:编译为py脚本文件)。这是嵌入画布的一种非常方便的方法。另一种可能性是手动编写代码构造地图画布和其他控件(作为主窗口或对话框的子窗口)并创建布局。 57 | 58 | 默认情况下,地图画布具有黑色背景,不使用消除锯齿。设置白色背景并启用抗锯齿来实现平滑渲染 59 | 60 | ```python 61 | canvas.setCanvasColor(Qt.white) 62 | canvas.enableAntiAliasing(True) 63 | ``` 64 | 65 | (如果你想知道,`Qt`来自`PyQt.QtCore`模块,并且 `Qt.white`是预定义的`QColor`实例之一。) 66 | 67 | 现在是时候添加一些地图图层了。我们首先打开一个图层并将其添加到当前项目中。然后我们将设置画布范围并设置画布的图层列表 68 | 69 | ```python 70 | vlayer = QgsVectorLayer("testdata/airports.shp", "Airports layer", "ogr") 71 | if not vlayer.isValid(): 72 | print("图层加载失败!") 73 | 74 | # 将图层添加到注册表 75 | QgsProject.instance().addMapLayer(vlayer) 76 | 77 | # 缩放到图层 78 | canvas.setExtent(vlayer.extent()) 79 | 80 | # 设置地图画布的图层集 81 | canvas.setLayers([vlayer]) 82 | ``` 83 | 84 | 执行这些命令后,画布将显示已加载的图层。 85 | 86 | ## 9.2 橡皮条和顶点标记 87 | 88 | 在画布上的地图顶部显示一些其他数据,使用地图画布项。可以创建自定义画布项类(如下所述),但为方便起见,有两个有用的画布项类:[`QgsRubberBand`](https://qgis.org/pyqgis/master/gui/QgsRubberBand.html#qgis.gui.QgsRubberBand)用于绘制折线或多边形,[`QgsVertexMarker`](https://qgis.org/pyqgis/master/gui/QgsVertexMarker.html#qgis.gui.QgsVertexMarker)绘制点。它们都使用地图坐标,因此在平移或缩放画布时会自动移动/缩放形状。 89 | 90 | 显示折线 91 | 92 | ```python 93 | r = QgsRubberBand(canvas, QgsWkbTypes.LineGeometry) # 线 94 | points = [QgsPointXY(-100, 45), QgsPointXY(10, 60), QgsPointXY(120, 45)] 95 | r.setToGeometry(QgsGeometry.fromPolyline(points), None) 96 | ``` 97 | 98 | 显示多边形 99 | 100 | ```python 101 | r = QgsRubberBand(canvas, QgsWkbTypes.PolygonGeometry) # 多边形 102 | points = [[QgsPointXY(-100, 35), QgsPointXY(10, 50), QgsPointXY(120, 35)]] 103 | r.setToGeometry(QgsGeometry.fromPolygonXY(points), None) 104 | ``` 105 | 106 | 请注意,多边形的点不是普通列表:实际上,它是包含多边形线环的环列表:第一个环是外边框,第二个(可选)环对应于多边形中的孔。 107 | 108 | 橡皮条允许一些定制,即改变它们的颜色和线宽 109 | 110 | ```python 111 | r.setColor(QColor(0, 0, 255)) 112 | r.setWidth(3) 113 | ``` 114 | 115 | 画布项绑定到画布场景。要暂时隐藏它们(并再次显示它们),请使用`hide()`和`show()`组合。完全删除该项,你必须将其从画布的场景中删除 116 | 117 | ```python 118 | canvas.scene().removeItem(r) 119 | ``` 120 | 121 | (在C ++中,可以只删除该项,但是在Python`del r`中 只删除引用,并且该对象仍然存在,因为它由画布拥有) 122 | 123 | 橡皮条也可用于绘制点,但 [`QgsVertexMarker`](https://qgis.org/pyqgis/master/gui/QgsVertexMarker.html#qgis.gui.QgsVertexMarker)类更适合于此([`QgsRubberBand`](https://qgis.org/pyqgis/master/gui/QgsRubberBand.html#qgis.gui.QgsRubberBand)仅在所需点周围绘制一个矩形)。 124 | 125 | 你可以像这样使用顶点标记: 126 | 127 | ```python 128 | m = QgsVertexMarker(canvas) 129 | m.setCenter(QgsPointXY(10,40)) 130 | ``` 131 | 132 | 这将在位置 **[10,45]** 上绘制一个红十字。可以自定义图标类型,大小,颜色和宽度 133 | 134 | ```python 135 | m.setColor(QColor(0, 255, 0)) 136 | m.setIconSize(5) 137 | m.setIconType(QgsVertexMarker.ICON_BOX) # or ICON_CROSS, ICON_X 138 | m.setPenWidth(3) 139 | ``` 140 | 141 | 临时隐藏顶点标记并从画布中删除它们,使用与橡皮条相同的方法。 142 | 143 | ## 9.3 在画布中使用地图工具 144 | 145 | 以下示例构造一个窗口,其中包含用于地图平移和缩放的地图画布和基本地图工具。激活每个工具:平移工具[`QgsMapToolPan`](https://qgis.org/pyqgis/master/gui/QgsMapToolPan.html#qgis.gui.QgsMapToolPan),放大缩小工具[`QgsMapToolZoom`](https://qgis.org/pyqgis/master/gui/QgsMapToolZoom.html#qgis.gui.QgsMapToolZoom)。设置为可被选中,允许自动处理选中/未选中的操作状态——当激活地图工具时,一个工具被选中时,取消选中上一个工具。使用[`setMapTool()`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas.setMapTool)方法激活地图工具。 146 | 147 | ```python 148 | from qgis.gui import * 149 | from qgis.PyQt.QtWidgets import QAction, QMainWindow 150 | from qgis.PyQt.QtCore import Qt 151 | 152 | 153 | class MyWnd(QMainWindow): 154 | def __init__(self, layer): 155 | QMainWindow.__init__(self) 156 | 157 | self.canvas = QgsMapCanvas() 158 | self.canvas.setCanvasColor(Qt.white) 159 | 160 | self.canvas.setExtent(layer.extent()) 161 | self.canvas.setLayers([layer]) 162 | 163 | self.setCentralWidget(self.canvas) 164 | 165 | self.actionZoomIn = QAction("Zoom in", self) 166 | self.actionZoomOut = QAction("Zoom out", self) 167 | self.actionPan = QAction("Pan", self) 168 | 169 | self.actionZoomIn.setCheckable(True) 170 | self.actionZoomOut.setCheckable(True) 171 | self.actionPan.setCheckable(True) 172 | 173 | self.actionZoomIn.triggered.connect(self.zoomIn) 174 | self.actionZoomOut.triggered.connect(self.zoomOut) 175 | self.actionPan.triggered.connect(self.pan) 176 | 177 | self.toolbar = self.addToolBar("Canvas actions") 178 | self.toolbar.addAction(self.actionZoomIn) 179 | self.toolbar.addAction(self.actionZoomOut) 180 | self.toolbar.addAction(self.actionPan) 181 | 182 | # 创建地图工具 183 | self.toolPan = QgsMapToolPan(self.canvas) 184 | self.toolPan.setAction(self.actionPan) 185 | self.toolZoomIn = QgsMapToolZoom(self.canvas, False) # false = in 186 | self.toolZoomIn.setAction(self.actionZoomIn) 187 | self.toolZoomOut = QgsMapToolZoom(self.canvas, True) # true = out 188 | self.toolZoomOut.setAction(self.actionZoomOut) 189 | 190 | self.pan() 191 | 192 | def zoomIn(self): 193 | self.canvas.setMapTool(self.toolZoomIn) 194 | 195 | def zoomOut(self): 196 | self.canvas.setMapTool(self.toolZoomOut) 197 | 198 | def pan(self): 199 | self.canvas.setMapTool(self.toolPan) 200 | 201 | ``` 202 | 203 | 你可以在Python控制台编辑器中尝试上述代码。调用画布窗口,添加以下代码以实例化`MyWnd`类。它们将在新创建的画布上渲染当前选定的图层 204 | 205 | ```python 206 | w = MyWnd(iface.activeLayer()) 207 | w.show() 208 | ``` 209 | 210 | ### 9.3.1 使用QgsMapToolIdentifyFeature选择要素 211 | 212 | 你可以使用地图工具[`QgsMapToolIdentifyFeature`](https://qgis.org/pyqgis/master/gui/QgsMapToolIdentifyFeature.html#qgis.gui.QgsMapToolIdentifyFeature)为用户选择一个要素,这个要素将被传递给回调函数。 213 | 214 | ```python 215 | def callback(feature): 216 | """当用户选择要素后被调用""" 217 | print("You clicked on feature {}".format(feature.id())) 218 | 219 | 220 | canvas = iface.mapCanvas() 221 | feature_identifier = QgsMapToolIdentifyFeature(canvas) 222 | 223 | # 表示被选择的图层 224 | feature_identifier.setLayer(vlayer) 225 | 226 | # 当用户识别要素时触发槽函数,使用回调函数 227 | feature_identifier.featureIdentified.connect(callback) 228 | 229 | # 激活这个地图工具 230 | canvas.setMapTool(feature_identifier) 231 | ``` 232 | 233 | ### 9.3.2 将项目添加到地图画布上下文菜单 234 | 235 | 使用地图画布也可以通过使用[`contextMenuAboutToShow`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas.contextMenuAboutToShow)信号添加到其上下文菜单中的条目来完成。 236 | 237 | 当你右键单击地图画布时,以下代码会在默认条目旁边添加 **My Menu** ► **My Action** 操作。 238 | 239 | ```python 240 | # 用于填充上下文菜单的插槽 241 | def populateContextMenu(menu: QMenu, event: QgsMapMouseEvent): 242 | subMenu = menu.addMenu('My Menu') 243 | action = subMenu.addAction('My Action') 244 | action.triggered.connect(lambda *args: 245 | print(f'Action triggered at {event.x()},{event.y()}')) 246 | 247 | canvas.contextMenuAboutToShow.connect(populateContextMenu) 248 | canvas.show() 249 | ``` 250 | 251 | ## 9.4 编写自定义地图工具 252 | 253 | 你可以编写自定义工具,来实现用户在画布上执行自定义行为的操作。 254 | 255 | 地图工具应继承自[`QgsMapTool`](https://qgis.org/pyqgis/master/gui/QgsMapTool.html#qgis.gui.QgsMapTool)类或任何派生类,并使用[`setMapTool()`](https://qgis.org/pyqgis/master/gui/QgsMapCanvas.html#qgis.gui.QgsMapCanvas.setMapTool) 在画布中选择为激活工具。 256 | 257 | 下面是一个地图工具示例,它允许通过在画布上单击并拖动来定义矩形范围。定义矩形后,它会在控制台中打印其边界坐标。它使用前面描述的橡皮条元素来显示所定义的矩形。 258 | 259 | ```python 260 | class RectangleMapTool(QgsMapToolEmitPoint): 261 | def __init__(self, canvas): 262 | self.canvas = canvas 263 | QgsMapToolEmitPoint.__init__(self, self.canvas) 264 | self.rubberBand = QgsRubberBand(self.canvas, QgsWkbTypes.PolygonGeometry) 265 | self.rubberBand.setColor(Qt.red) 266 | self.rubberBand.setWidth(1) 267 | self.reset() 268 | 269 | def reset(self): 270 | self.startPoint = self.endPoint = None 271 | self.isEmittingPoint = False 272 | self.rubberBand.reset(QgsWkbTypes.PolygonGeometry) 273 | 274 | def canvasPressEvent(self, e): 275 | self.startPoint = self.toMapCoordinates(e.pos()) 276 | self.endPoint = self.startPoint 277 | self.isEmittingPoint = True 278 | self.showRect(self.startPoint, self.endPoint) 279 | 280 | def canvasReleaseEvent(self, e): 281 | self.isEmittingPoint = False 282 | r = self.rectangle() 283 | if r is not None: 284 | print("Rectangle:", r.xMinimum(), 285 | r.yMinimum(), r.xMaximum(), r.yMaximum() 286 | ) 287 | 288 | def canvasMoveEvent(self, e): 289 | if not self.isEmittingPoint: 290 | return 291 | 292 | self.endPoint = self.toMapCoordinates(e.pos()) 293 | self.showRect(self.startPoint, self.endPoint) 294 | 295 | def showRect(self, startPoint, endPoint): 296 | self.rubberBand.reset(QgsWkbTypes.PolygonGeometry) 297 | if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y(): 298 | return 299 | 300 | point1 = QgsPointXY(startPoint.x(), startPoint.y()) 301 | point2 = QgsPointXY(startPoint.x(), endPoint.y()) 302 | point3 = QgsPointXY(endPoint.x(), endPoint.y()) 303 | point4 = QgsPointXY(endPoint.x(), startPoint.y()) 304 | 305 | self.rubberBand.addPoint(point1, False) 306 | self.rubberBand.addPoint(point2, False) 307 | self.rubberBand.addPoint(point3, False) 308 | self.rubberBand.addPoint(point4, True) # true to update canvas 309 | self.rubberBand.show() 310 | 311 | def rectangle(self): 312 | if self.startPoint is None or self.endPoint is None: 313 | return None 314 | elif (self.startPoint.x() == self.endPoint.x() or \ 315 | self.startPoint.y() == self.endPoint.y()): 316 | return None 317 | 318 | return QgsRectangle(self.startPoint, self.endPoint) 319 | 320 | def deactivate(self): 321 | QgsMapTool.deactivate(self) 322 | self.deactivated.emit() 323 | 324 | ``` 325 | 326 | ## 9.5 编写自定义地图画布项 327 | 328 | 这是一个自定义画布项的示例,该画布项绘制了一个圆: 329 | 330 | ```python 331 | class CircleCanvasItem(QgsMapCanvasItem): 332 | def __init__(self, canvas): 333 | super().__init__(canvas) 334 | self.center = QgsPoint(0, 0) 335 | self.size = 100 336 | 337 | def setCenter(self, center): 338 | self.center = center 339 | 340 | def center(self): 341 | return self.center 342 | 343 | def setSize(self, size): 344 | self.size = size 345 | 346 | def size(self): 347 | return self.size 348 | 349 | def boundingRect(self): 350 | return QRectF(self.center.x() - self.size / 2, 351 | self.center.y() - self.size / 2, 352 | self.center.x() + self.size / 2, 353 | self.center.y() + self.size / 2) 354 | 355 | def paint(self, painter, option, widget): 356 | path = QPainterPath() 357 | path.moveTo(self.center.x(), self.center.y()); 358 | path.arcTo(self.boundingRect(), 0.0, 360.0) 359 | painter.fillPath(path, QColor("red")) 360 | 361 | 362 | # 使用自定义项: 363 | item = CircleCanvasItem(iface.mapCanvas()) 364 | item.setCenter(QgsPointXY(200, 200)) 365 | item.setSize(80) 366 | 367 | ``` 368 | -------------------------------------------------------------------------------- /docs/15-任务——在后台做繁重的工作.md: -------------------------------------------------------------------------------- 1 | 本节代码片段需导入以下模块: 2 | 3 | ```python 4 | from qgis.core import ( 5 | Qgis, 6 | QgsApplication, 7 | QgsMessageLog, 8 | QgsProcessingAlgRunnerTask, 9 | QgsProcessingContext, 10 | QgsProcessingFeedback, 11 | QgsProject, 12 | QgsTask, 13 | QgsTaskManager, 14 | ) 15 | ``` 16 | 17 | # 15 任务 - 在后台做繁重的工作 18 | 19 | ## 15.1 引言 20 | 21 | 使用线程的后台处理,是在进行繁重处理时保持用户界面响应的一种方式。任务可用于在QGIS中实现线程。 22 | 23 | 任务([`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask))是在后台执行代码的容器,任务管理([`QgsTaskManager`](https://qgis.org/pyqgis/master/core/QgsTaskManager.html#qgis.core.QgsTaskManager))用于控制任务的运行。这些类通过提供信号传递机制、进度报告和后台进程状态访问机制,简化了QGIS中的后台处理。可以使用子任务对任务进行分组。 24 | 25 | 全局任务管理器([`QgsApplication.taskManager()`](https://qgis.org/pyqgis/master/core/QgsApplication.html#qgis.core.QgsApplication.taskManager))通常被使用。这意味着你的任务可能不是由任务管理器控制的唯一任务。 26 | 27 | 有几种方法可以创建QGIS任务: 28 | 29 | - 通过扩展[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask)创建自己的任务 30 | 31 | ```python 32 | class SpecialisedTask(QgsTask): 33 | pass 34 | ``` 35 | 36 | - 从函数创建任务 37 | 38 | ```python 39 | def heavyFunction(): 40 | # 一些CPU密集型处理 ... 41 | pass 42 | 43 | def workdone(): 44 | # ... 使用结果做一些有用的事情 45 | pass 46 | 47 | task = QgsTask.fromFunction('heavy function', heavyFunction, 48 | on_finished=workdone) 49 | ``` 50 | 51 | - 从处理算法创建任务 52 | 53 | ```python 54 | params = dict() 55 | context = QgsProcessingContext() 56 | context.setProject(QgsProject.instance()) 57 | feedback = QgsProcessingFeedback() 58 | 59 | buffer_alg = QgsApplication.instance().processingRegistry().algorithmById('native:buffer') 60 | task = QgsProcessingAlgRunnerTask(buffer_alg, params, context,feedback) 61 | ``` 62 | 63 | !!! warning 警告 64 | 65 | 任何后台任务(无论如何创建)决不能使用任何主线程上的QObject,比如访问QgsVectorLayer, QgsProject或者执行任何GUI操作——比如创建新的部件或者与现有部件交互。只能从主线程访问或修改Qt控件。在任务启动之前,必须复制任务中使用的数据。试图从后台线程使用它们将导致崩溃。 66 | 67 | 此外,请始终确保并 [context](https://qgis.org/pyqgis/master/core/QgsProcessingContext.html#qgis.core.QgsProcessingContext) 和 [feedback](https://qgis.org/pyqgis/3.34/core/QgsProcessingFeedback.html#qgis.core.QgsProcessingFeedback) 至少与使用它们的任务一样长。如果在完成任务后,QGSTaskManager无法访问计划任务的上下文和反馈,则QGIS将崩溃。 68 | 69 | !!! info 70 | 71 | 在调用 `QgsProcessingContext` 后不久调用 [setProject()](https://qgis.org/pyqgis/master/core/QgsProcessingContext.html#qgis.core.QgsProcessingContext.setProject) 是一种常见的模式。这允许任务及其回调函数使用大多数项目范围的设置。这在回调函数中使用空间图层时特别有价值。 72 | 73 | 可以使用[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask)中的[`addSubTask()`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask.addSubTask) 函数来描述任务之间的依赖关系。当声明依赖关系时,任务管理器将自动确定如何执行这些依赖关系。只要有可能,依赖项将并行执行,以便尽快满足它们。如果取消了一个任务所依赖的任务,则相关任务也将被取消。循环依赖可能造成死锁,所以要小心。 74 | 75 | 如果任务依赖于可用的图层,则可以使用[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask)中的[`setDependentLayers`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask.setDependentLayers) 函数来声明。如果任务所依赖的图层不可用,则该任务将被取消。 76 | 77 | 创建任务后,可以使用任务管理器的[`addTask()`](https://qgis.org/pyqgis/master/core/QgsTaskManager.html#qgis.core.QgsTaskManager.addTask)函数调度任务运行。向管理器添加任务会自动将该任务的所有权转移给管理员,管理员将在执行完后清理和删除任务。任务的调度受任务优先级的影响,任务优先级在[`addTask()`](https://qgis.org/pyqgis/master/core/QgsTaskManager.html#qgis.core.QgsTaskManager.addTask)中设置。 78 | 79 | 任务的状态可以使用[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask)、[`QgsTaskManager`](https://qgis.org/pyqgis/master/core/QgsTaskManager.html#qgis.core.QgsTaskManager)的信号和函数进行监控。 80 | 81 | ## 15.2 示例 82 | 83 | ### 15.2.1 扩展QgsTask 84 | 85 | 在此示例中,`RandomIntegerSumTask`扩展了[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask),它将在指定的时间段内生成0到500之间的100个随机整数。如果随机数为42,则中止任务并引发异常。`RandomIntegerSumTask`(带子任务)生成了几个实例并将其添加到任务管理器,展示两种类型的依赖项: 86 | 87 | ```python 88 | import random 89 | from time import sleep 90 | 91 | from qgis.core import ( 92 | QgsApplication, QgsTask, QgsMessageLog, Qgis 93 | ) 94 | 95 | MESSAGE_CATEGORY = 'RandomIntegerSumTask' 96 | 97 | class RandomIntegerSumTask(QgsTask): 98 | """展示如何子类化QgsTask""" 99 | def __init__(self, description, duration): 100 | super().__init__(description, QgsTask.CanCancel) 101 | self.duration = duration 102 | self.total = 0 103 | self.iterations = 0 104 | self.exception = None 105 | 106 | def run(self): 107 | """在这里你要实现你的任务。 108 | 应该定期测试isCanceled(),以便优雅地终止。 109 | 此方法必须返回True或False。 110 | 引发异常将使QGIS崩溃,因此我们在内部处理这些异常,并在self.finished中抛出。 111 | """ 112 | QgsMessageLog.logMessage('Started task "{}"'.format( 113 | self.description()), 114 | MESSAGE_CATEGORY, Qgis.Info) 115 | wait_time = self.duration / 100 116 | for i in range(100): 117 | sleep(wait_time) 118 | # 使用setProgress报告进度 119 | self.setProgress(i) 120 | arandominteger = random.randint(0, 500) 121 | self.total += arandominteger 122 | self.iterations += 1 123 | # 检查isCanceled()处理取消 124 | if self.isCanceled(): 125 | return False 126 | # 模拟异常情况 127 | if arandominteger == 42: 128 | # 不要raise Exception('bad value!'),否则将使QGIS崩溃 129 | self.exception = Exception('bad value!') 130 | return False 131 | return True 132 | 133 | def finished(self, result): 134 | """ 135 | 当任务完成(无论成功与否)时,这个函数会被自动调用。 136 | 你可以通过实现 finished() 来执行任务完成后的后续事情。 137 | finished总是从主线程中调用的,因此在这里进行GUI操作和引发 Python 异常是安全的。 138 | result是self.run的返回值。 139 | """ 140 | if result: 141 | QgsMessageLog.logMessage( 142 | 'Task "{name}" completed\n' \ 143 | 'Total: {total} (with {iterations} '\ 144 | 'iterations)'.format( 145 | name=self.description(), 146 | total=self.total, 147 | iterations=self.iterations), 148 | MESSAGE_CATEGORY, Qgis.Success) 149 | else: 150 | if self.exception is None: 151 | QgsMessageLog.logMessage( 152 | 'Task "{name}" not successful but without '\ 153 | 'exception (probably the task was manually '\ 154 | 'canceled by the user)'.format( 155 | name=self.description()), 156 | MESSAGE_CATEGORY, Qgis.Warning) 157 | else: 158 | QgsMessageLog.logMessage( 159 | 'Task "{name}" Exception: {exception}'.format( 160 | name=self.description(), 161 | exception=self.exception), 162 | MESSAGE_CATEGORY, Qgis.Critical) 163 | raise self.exception 164 | 165 | def cancel(self): 166 | QgsMessageLog.logMessage( 167 | 'Task "{name}" was canceled'.format( 168 | name=self.description()), 169 | MESSAGE_CATEGORY, Qgis.Info) 170 | super().cancel() 171 | 172 | 173 | longtask = RandomIntegerSumTask('waste cpu long', 20) 174 | shorttask = RandomIntegerSumTask('waste cpu short', 10) 175 | minitask = RandomIntegerSumTask('waste cpu mini', 5) 176 | shortsubtask = RandomIntegerSumTask('waste cpu subtask short', 5) 177 | longsubtask = RandomIntegerSumTask('waste cpu subtask long', 10) 178 | shortestsubtask = RandomIntegerSumTask('waste cpu subtask shortest', 4) 179 | 180 | # 添加子任务(shortsubtask)到shorttask——必须在minitask和longtask完成后执行 181 | shorttask.addSubTask(shortsubtask, [minitask, longtask]) 182 | # 添加子任务(longsubtask)到longtask——必须父级任务之前运行 183 | longtask.addSubTask(longsubtask, [], QgsTask.ParentDependsOnSubTask) 184 | # 添加子任务(shortestsubtask)到longtask 185 | longtask.addSubTask(shortestsubtask) 186 | 187 | QgsApplication.taskManager().addTask(longtask) 188 | QgsApplication.taskManager().addTask(shorttask) 189 | QgsApplication.taskManager().addTask(minitask) 190 | 191 | 192 | 193 | 194 | # RandomIntegerSumTask(0): Started task "waste cpu subtask shortest" 195 | # RandomIntegerSumTask(0): Started task "waste cpu short" 196 | # RandomIntegerSumTask(0): Started task "waste cpu mini" 197 | # RandomIntegerSumTask(0): Started task "waste cpu subtask long" 198 | # RandomIntegerSumTask(3): Task "waste cpu subtask shortest" completed 199 | # RandomTotal: 25452 (with 100 iterations) 200 | # RandomIntegerSumTask(3): Task "waste cpu mini" completed 201 | # RandomTotal: 23810 (with 100 iterations) 202 | # RandomIntegerSumTask(3): Task "waste cpu subtask long" completed 203 | # RandomTotal: 26308 (with 100 iterations) 204 | # RandomIntegerSumTask(0): Started task "waste cpu long" 205 | # RandomIntegerSumTask(3): Task "waste cpu long" completed 206 | # RandomTotal: 22534 (with 100 iterations) 207 | ``` 208 | 209 | ### 15.2.2 从函数创建任务 210 | 211 | 从函数创建任务(本示例中的`doSomething`)。该函数的第一个参数为[`QgsTask`](https://qgis.org/pyqgis/master/core/QgsTask.html#qgis.core.QgsTask) 。一个重要的参数是`on_finished`,它是在任务完成时被调用的函数。示例中的`doSomething`函数有另一个参数`wait_time`。 212 | 213 | ```python 214 | import random 215 | from time import sleep 216 | 217 | MESSAGE_CATEGORY = 'TaskFromFunction' 218 | 219 | def doSomething(task, wait_time): 220 | """ 221 | 抛出一个异常终止任务 222 | 成功则返回结果 223 | 结果将和异常一起传递给 (成功则为空)on_finished函数. 224 | 如果存在异常,结果为空 225 | """ 226 | QgsMessageLog.logMessage('Started task {}'.format(task.description()), 227 | MESSAGE_CATEGORY, Qgis.Info) 228 | wait_time = wait_time / 100 229 | total = 0 230 | iterations = 0 231 | for i in range(100): 232 | sleep(wait_time) 233 | # 使用task.setProgress报告进度 234 | task.setProgress(i) 235 | arandominteger = random.randint(0, 500) 236 | total += arandominteger 237 | iterations += 1 238 | # 检查task.isCanceled()处理取消 239 | if task.isCanceled(): 240 | stopped(task) 241 | return None 242 | # 抛出异常终止任务 243 | if arandominteger == 42: 244 | raise Exception('bad value!') 245 | return {'total': total, 'iterations': iterations, 246 | 'task': task.description()} 247 | 248 | def stopped(task): 249 | QgsMessageLog.logMessage( 250 | 'Task "{name}" was canceled'.format( 251 | name=task.description()), 252 | MESSAGE_CATEGORY, Qgis.Info) 253 | 254 | def completed(exception, result=None): 255 | """当doSomething完成时呗调佣 256 | 如果抛出异常则异常信息不是空 257 | 结果是doSomething返回的结果""" 258 | if exception is None: 259 | if result is None: 260 | QgsMessageLog.logMessage( 261 | 'Completed with no exception and no result '\ 262 | '(probably manually canceled by the user)', 263 | MESSAGE_CATEGORY, Qgis.Warning) 264 | else: 265 | QgsMessageLog.logMessage( 266 | 'Task {name} completed\n' 267 | 'Total: {total} ( with {iterations} ' 268 | 'iterations)'.format( 269 | name=result['task'], 270 | total=result['total'], 271 | iterations=result['iterations']), 272 | MESSAGE_CATEGORY, Qgis.Info) 273 | else: 274 | QgsMessageLog.logMessage("Exception: {}".format(exception), 275 | MESSAGE_CATEGORY, Qgis.Critical) 276 | raise exception 277 | 278 | # 创建一些任务 279 | task1 = QgsTask.fromFunction(u'Waste cpu 1', doSomething, 280 | on_finished=completed, wait_time=4) 281 | task2 = QgsTask.fromFunction(u'Waste cpu 2', dosomething, 282 | on_finished=completed, wait_time=3) 283 | QgsApplication.taskManager().addTask(task1) 284 | QgsApplication.taskManager().addTask(task2) 285 | 286 | 287 | # RandomIntegerSumTask(0): Started task "waste cpu subtask short" 288 | # RandomTaskFromFunction(0): Started task Waste cpu 1 289 | # RandomTaskFromFunction(0): Started task Waste cpu 2 290 | # RandomTaskFromFunction(0): Task Waste cpu 2 completed 291 | # RandomTotal: 23263 ( with 100 iterations) 292 | # RandomTaskFromFunction(0): Task Waste cpu 1 completed 293 | # RandomTotal: 25044 ( with 100 iterations) 294 | ``` 295 | 296 | ### 14.2.3 处理算法任务 297 | 298 | 创建一个使用算法[`qgis:randompointsinextent`](https://docs.qgis.org/testing/en/docs/user_manual/processing_algs/qgis/vectorcreation.html#qgisrandompointsinextent)的任务,在指定范围内生成50000个随机点。结果以安全的方式添加到项目中。 299 | 300 | ```python 301 | from functools import partial 302 | from qgis.core import (QgsTaskManager, QgsMessageLog, 303 | QgsProcessingAlgRunnerTask, QgsApplication, 304 | QgsProcessingContext, QgsProcessingFeedback, 305 | QgsProject) 306 | 307 | MESSAGE_CATEGORY = 'AlgRunnerTask' 308 | 309 | def task_finished(context, successful, results): 310 | if not successful: 311 | QgsMessageLog.logMessage('Task finished unsucessfully', 312 | MESSAGE_CATEGORY, Qgis.Warning) 313 | output_layer = context.getMapLayer(results['OUTPUT']) 314 | # 因为getMapLayer没有移交所有权, 当上下文超出范围时图层将被删除,你会遇到崩溃 315 | # takeMapLayer移交所有权,因此它将安全地添加到QgsProject中,并赋予QgsProject所有权 316 | if output_layer and output_layer.isValid(): 317 | QgsProject.instance().addMapLayer( 318 | context.takeResultLayer(output_layer.id())) 319 | 320 | alg = QgsApplication.processingRegistry().algorithmById( 321 | u'qgis:randompointsinextent') 322 | context = QgsProcessingContext() 323 | feedback = QgsProcessingFeedback() 324 | params = { 325 | 'EXTENT': '0.0,10.0,40,50 [EPSG:4326]', 326 | 'MIN_DISTANCE': 0.0, 327 | 'POINTS_NUMBER': 50000, 328 | 'TARGET_CRS': 'EPSG:4326', 329 | 'OUTPUT': 'memory:My random points' 330 | } 331 | task = QgsProcessingAlgRunnerTask(alg, params, context, feedback) 332 | task.executed.connect(partial(task_finished, context)) 333 | QgsApplication.taskManager().addTask(task) 334 | ``` 335 | 336 | 也可以查看博客:https://www.opengis.ch/2018/06/22/threads-in-pyqgis3/ -------------------------------------------------------------------------------- /docs/19-网络分析库.md: -------------------------------------------------------------------------------- 1 | 本节代码片段需导入以下模块: 2 | 3 | ```python 4 | from qgis.core import ( 5 | QgsVectorLayer, 6 | QgsPointXY, 7 | ) 8 | ``` 9 | 10 | # 19 网络分析库 11 | 12 | 网络分析库可用于: 13 | 14 | - 从地理数据(矢量线折线图层)创建图 15 | - 实现图论中的基本算法(目前只有Dijkstra的算法) 16 | 17 | 网络分析库是通过从`RoadGraph`核心插件导出基本函数创建的,现在你可以在插件中使用它的方法,也可以直接从Python控制台使用它。 18 | 19 | ## 19.1 一般信息 20 | 21 | 简而言之,一个典型用例可以描述为: 22 | 23 | - 从地理数据创建图(矢量折线图层) 24 | - 运行图算法 25 | - 使用分析结果 26 | 27 | ## 19.2 构建一个图 28 | 29 | 你需要做的第一件事是准备输入数据,也就是将矢量图层转换为图。所有进一步的操作都将使用这个图,而不是图层。 30 | 31 | 作为数据源,我们可以使用任何折线矢量图层。折线的节点成为图的顶点,折线的线段成为图的边。如果几个节点具有相同的坐标,那么它们就是相同的图顶点。因此,具有公共节点的两条线就会相互连接。 32 | 33 | 此外,在图创建过程中,可以将任意数量的附加点“固定”(“绑”)到输入矢量图层。对于每个附加点,将找到一个匹配——最近的顶点或最近的边。在后一种情况下,边将被分割并添加一个新顶点。 34 | 35 | 矢量图层属性和边的长度可以用作边的属性。 36 | 37 | 使用[Builder](https://en.wikipedia.org/wiki/Builder_pattern)编程模式完成从矢量图层到图的转换。图是使用所谓的控制器构造的。目前只有一个控制器:[`QgsVectorLayerDirector`](https://qgis.org/pyqgis/master/analysis/QgsVectorLayerDirector.html#qgis.analysis.QgsVectorLayerDirector)。控制器设置了基本的设置——这些设置将用于从线矢量图层构造图,构建器用来创建图。目前,控制器一样,只有一个构建器存在:[`QgsGraphBuilder`](https://qgis.org/pyqgis/master/analysis/QgsGraphBuilder.html#qgis.analysis.QgsGraphBuilder),它可以创建[`QgsGraph`](https://qgis.org/pyqgis/master/analysis/QgsGraph.html#qgis.analysis.QgsGraph)对象。你可能希望实现自己的构建器,以建立一个与[BGL](https://www.boost.org/doc/libs/1_48_0/libs/graph/doc/index.html)或[NetworkX](https://networkx.org/)等库兼容的图。 38 | 39 | 为了计算边属性,使用编程模式[策略](https://en.wikipedia.org/wiki/Strategy_pattern)。目前只有[`QGSNetworkDistanceTreatgy`](https://qgis.org/pyqgis/master/analysis/QgsNetworkDistanceStrategy.html#qgis.analysis.QgsNetworkDistanceStrategy)策略(考虑到路线的长度)和[`QgsNetworkSpeedStrategy`](https://qgis.org/pyqgis/master/analysis/QgsNetworkSpeedStrategy.html#qgis.analysis.QgsNetworkSpeedStrategy)(也考虑到速度)可用。你可以实现自己的策略,使用所有必要的参数。例如,RoadGraph插件使用的策略是使用边长度和属性中的速度值来计算行程时间。 40 | 41 | 是时候深入研究这个过程了。 42 | 43 | 首先,要使用这个库,我们应该导入分析模块 44 | 45 | ```python 46 | from qgis.analysis import * 47 | ``` 48 | 49 | 然后是一些创建控制器的示例: 50 | 51 | ```python 52 | # 不要使用图层属性中有关道路方向的信息,所有道路都是双向的 53 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth) 54 | 55 | # 使用第5个字段作为道路的方向信息. 56 | # 正向单方向道路使用 "yes", 57 | # 反向单方向道路使用 "1" 58 | # 因此,双向道路使用 “no”。默认情况道路是双向路。 59 | # 此方案可用于OpenStreetMap数据 60 | director = QgsVectorLayerDirector(vectorLayer, 5, 'yes', '1', 'no', QgsVectorLayerDirector.DirectionBoth) 61 | ``` 62 | 63 | 为了构造一个控制器,我们应该传递一个矢量图层,该矢量图层作为图结构的源,以及关于每个路段上允许移动的信息(单向或双向移动、直接或反向),像这样: 64 | 65 | ```python 66 | director = QgsVectorLayerDirector(vectorLayer, 67 | directionFieldId, 68 | directDirectionValue, 69 | reverseDirectionValue, 70 | bothDirectionValue, 71 | defaultDirection) 72 | ``` 73 | 74 | 以下是这些参数的全部含义: 75 | 76 | - `vectorLayer`——用于构建图的矢量图层 77 | - `directionFieldId`——字段的索引值,用于存储道路方向的信息。如果是`-1`,表示不使用这些信息。整型。 78 | - `directDirectionValue`——正向的字段值(从第一个直线点移动到最后一个直线点)。字符串。 79 | - `reverseDirectionValue`——反向道路的字段值(从最后一个直线点移动到第一个直线点)。字符串。 80 | - `bothDirectionValue`——双向道路的字段值(对于这样的道路,我们可以从第一点移动到最后一点,也可以从最后一点移动到第一点)。字符串。 81 | - `defaultDirectio`——默认道路方向。该值将用于这些道路当字段`directionFieldId`未设置时或具有与上面指定的三个值中的任何一个不同的值。可用的值是: 82 | - [`QgsVectorLayerDirector.DirectionForward`](https://qgis.org/pyqgis/master/analysis/QgsVectorLayerDirector.html#qgis.analysis.QgsVectorLayerDirector.DirectionForward)——正向单向道路 83 | - [`QgsVectorLayerDirector.DirectionBackward`](https://qgis.org/pyqgis/master/analysis/QgsVectorLayerDirector.html#qgis.analysis.QgsVectorLayerDirector.DirectionBackward)——反向单向道路 84 | - [`QgsVectorLayerDirector.DirectionBoth`](https://qgis.org/pyqgis/master/analysis/QgsVectorLayerDirector.html#qgis.analysis.QgsVectorLayerDirector.DirectionBoth)双向道路 85 | 86 | 然后有必要创建用于计算边属性的策略: 87 | 88 | ```python 89 | # 包含速度信息的字段索引值 90 | attributeId = 1 91 | # 默认速度 92 | defaultValue = 50 93 | # 转化速度到米制单位 ('1' 表示不转化) 94 | toMetricFactor = 1 95 | strategy = QgsNetworkSpeedStrategy(attributeId, defaultValue, toMetricFactor) 96 | ``` 97 | 98 | 告诉控制器这个策略 99 | 100 | ```python 101 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', 3) 102 | director.addStrategy(strategy) 103 | ``` 104 | 105 | 现在我们可以使用构造器来创建图。[`QgsGraphBuilder`](https://qgis.org/pyqgis/master/analysis/QgsGraphBuilder.html#qgis.analysis.QgsGraphBuilder)类构造函数接受几个参数: 106 | 107 | - `crs`——坐标参考系统。必需。 108 | - `otfEnabled`——使用“on the fly” 重投影. 默认为`True` (use OTF). 109 | - `topologyTolerance`——拓扑容差。默认为0。 110 | - `ellipsoidID`——参考椭球。默认为“WGS84”。 111 | 112 | ```python 113 | # 只设置CRS,,其它值默认 114 | builder = QgsGraphBuilder(vectorLayer.crs()) 115 | ``` 116 | 117 | 我们还可以定义几个点,这些点将用于分析。例如: 118 | 119 | ```python 120 | startPoint = QgsPointXY(1179720.1871, 5419067.3507) 121 | endPoint = QgsPointXY(1180616.0205, 5419745.7839) 122 | ``` 123 | 124 | 现在一切就绪,我们可以构建图表并将这些点“连接”到它 125 | 126 | ```python 127 | tiedPoints = director.makeGraph(builder, [startPoint, endPoint]) 128 | ``` 129 | 130 | 构建图可能需要一些时间(这取决于图层中的要素数量和图层大小)。`tiedPoints`是一个包含“绑定”点坐标的列表。当构建操作完成时,我们可以得到图并将其用于分析 131 | 132 | ```python 133 | graph = builder.graph() 134 | ``` 135 | 136 | 通过下面的代码,我们可以得到点的顶点索引 137 | 138 | ```python 139 | startId = graph.findVertex(tiedPoints[0]) 140 | endId = graph.findVertex(tiedPoints[1]) 141 | ``` 142 | 143 | ## 19.3 图分析 144 | 145 | 网络分析用于找到两个问题的答案:哪些顶点是相连的,以及如何找到最短路径。为了解决这些问题,网络分析库提供了Dijkstra算法。 146 | 147 | Dijkstra算法找到从图的一个顶点到所有其他顶点的最短路径以及优化参数的值。结果可以表示为最短路径树。 148 | 149 | 最短路径树是一个有向加权图(或更准确地说是一棵树),具有以下特性: 150 | 151 | - 只有一个顶点没有入射边——树的根 152 | - 所有其他顶点只有一条入射边 153 | - 如果顶点B可以从顶点A到达,那么从A到B的路径是唯一可用的路径,并且它在该图上是最优的(最短的)路径 154 | 155 | 要获得最短路径树,可以使用[`QgsGraphAnalyzer`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer)类的[`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree) 和[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法。建议使用[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法,因为它更快,而且更有效地使用内存。 156 | 157 | [`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree)方法在你想在最短路径树上行走时很有用。它总是创建一个新的图对象(QgsGraph)并接受三个变量: 158 | 159 | - `source`——输入的图 160 | - `startVertexIdx`——树上的点的索引(树的根) 161 | - `criterionNum`——使用的边属性的数量(从0开始) 162 | 163 | ```python 164 | tree = QgsGraphAnalyzer.shortestTree(graph, startId, 0) 165 | ``` 166 | 167 | [`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法有相同的参数,但返回两个数组。在第一个数组中,n元素包含传入边的索引,如果没有传入边则为-1。在第二个数组中,n元素包含从树的根到顶点n的距离,如果顶点n从根部无法到达,则为DOUBLE_MAX。 168 | 169 | ```python 170 | (tree, cost) = QgsGraphAnalyzer.dijkstra(graph, startId, 0) 171 | ``` 172 | 173 | 下面是一些非常简单的代码,使用[`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree)方法创建的图显示最短路径树(在`Layers`面板中选择`linestring`图层,用你自己的坐标替换)。 174 | 175 | !!! 警告 warning 176 | 177 | 这段代码仅作为一个例子,它创建了大量的[`QgsRubberBand`](https://qgis.org/pyqgis/master/gui/QgsRubberBand.html#qgis.gui.QgsRubberBand)对象,在大数据集上可能会很慢。 178 | 179 | ```python 180 | from qgis.core import * 181 | from qgis.gui import * 182 | from qgis.analysis import * 183 | from qgis.PyQt.QtCore import * 184 | from qgis.PyQt.QtGui import * 185 | 186 | vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines') 187 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth) 188 | strategy = QgsNetworkDistanceStrategy() 189 | director.addStrategy(strategy) 190 | builder = QgsGraphBuilder(vectorLayer.crs()) 191 | 192 | pStart = QgsPointXY(1179661.925139,5419188.074362) 193 | tiedPoint = director.makeGraph(builder, [pStart]) 194 | pStart = tiedPoint[0] 195 | 196 | graph = builder.graph() 197 | 198 | idStart = graph.findVertex(pStart) 199 | 200 | tree = QgsGraphAnalyzer.shortestTree(graph, idStart, 0) 201 | 202 | i = 0 203 | while (i < tree.edgeCount()): 204 | rb = QgsRubberBand(iface.mapCanvas()) 205 | rb.setColor (Qt.red) 206 | rb.addPoint (tree.vertex(tree.edge(i).fromVertex()).point()) 207 | rb.addPoint (tree.vertex(tree.edge(i).toVertex()).point()) 208 | i = i + 1 209 | ``` 210 | 211 | 使用[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法 212 | 213 | ```python 214 | from qgis.core import * 215 | from qgis.gui import * 216 | from qgis.analysis import * 217 | from qgis.PyQt.QtCore import * 218 | from qgis.PyQt.QtGui import * 219 | 220 | vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines') 221 | 222 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth) 223 | strategy = QgsNetworkDistanceStrategy() 224 | director.addStrategy(strategy) 225 | builder = QgsGraphBuilder(vectorLayer.crs()) 226 | 227 | pStart = QgsPointXY(1179661.925139,5419188.074362) 228 | tiedPoint = director.makeGraph(builder, [pStart]) 229 | pStart = tiedPoint[0] 230 | 231 | graph = builder.graph() 232 | 233 | idStart = graph.findVertex(pStart) 234 | 235 | (tree, costs) = QgsGraphAnalyzer.dijkstra(graph, idStart, 0) 236 | 237 | for edgeId in tree: 238 | if edgeId == -1: 239 | continue 240 | rb = QgsRubberBand(iface.mapCanvas()) 241 | rb.setColor (Qt.red) 242 | rb.addPoint (graph.vertex(graph.edge(edgeId).fromVertex()).point()) 243 | rb.addPoint (graph.vertex(graph.edge(edgeId).toVertex()).point()) 244 | ``` 245 | 246 | ### 19.3.1 查找最短路径 247 | 248 | 为了找到两点之间的最佳路径,采用了以下方法。两点(起点A和终点B)在图建立时都被 "捆绑 "在一起。然后使用[`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree)或[`dijkstra()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法,我们建立以起点A为根的最短路径树。在同一棵树上,我们也找到了终点B,并开始从B点走到A点,整个算法可以写成这样: 249 | 250 | ```c 251 | assign T = B 252 | while T != B 253 | add point T to path 254 | get incoming edge for point T 255 | look for point TT, that is start point of this edge 256 | assign T = TT 257 | add point A to path 258 | ``` 259 | 260 | 在这一点上,我们有一个路径,其形式是顶点的倒置列表(顶点是按照从终点到起点的相反顺序排列的),这些顶点将在这条路径的行程中被访问。 261 | 262 | 以下是使用[`shortestTree()`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.shortestTree)方法的QGIS Python控制台示例代码(你可能需要在图层目录树中中加载并选择一个线图层,并将代码中的坐标替换为你的坐标)。 263 | 264 | ```python 265 | from qgis.core import * 266 | from qgis.gui import * 267 | from qgis.analysis import * 268 | 269 | from qgis.PyQt.QtCore import * 270 | from qgis.PyQt.QtGui import * 271 | 272 | vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines') 273 | builder = QgsGraphBuilder(vectorLayer.sourceCrs()) 274 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth) 275 | strategy = QgsNetworkDistanceStrategy() 276 | director.addStrategy(strategy) 277 | 278 | startPoint = QgsPointXY(1179661.925139,5419188.074362) 279 | endPoint = QgsPointXY(1180942.970617,5420040.097560) 280 | 281 | tiedPoints = director.makeGraph(builder, [startPoint, endPoint]) 282 | tStart, tStop = tiedPoints 283 | 284 | graph = builder.graph() 285 | idxStart = graph.findVertex(tStart) 286 | 287 | tree = QgsGraphAnalyzer.shortestTree(graph, idxStart, 0) 288 | 289 | idxStart = tree.findVertex(tStart) 290 | idxEnd = tree.findVertex(tStop) 291 | 292 | if idxEnd == -1: 293 | raise Exception('No route!') 294 | 295 | # 添加最后一个点 296 | route = [tree.vertex(idxEnd).point()] 297 | 298 | # 遍历图 299 | while idxEnd != idxStart: 300 | edgeIds = tree.vertex(idxEnd).incomingEdges() 301 | if len(edgeIds) == 0: 302 | break 303 | edge = tree.edge(edgeIds[0]) 304 | route.insert(0, tree.vertex(edge.fromVertex()).point()) 305 | idxEnd = edge.fromVertex() 306 | 307 | # 显示 308 | rb = QgsRubberBand(iface.mapCanvas()) 309 | rb.setColor(Qt.green) 310 | 311 | # 如果项目的坐标系和图层的坐标系不一样,则需要坐标转换 312 | for p in route: 313 | rb.addPoint(p) 314 | ``` 315 | 316 | 使用[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法 317 | 318 | ```python 319 | from qgis.core import * 320 | from qgis.gui import * 321 | from qgis.analysis import * 322 | 323 | from qgis.PyQt.QtCore import * 324 | from qgis.PyQt.QtGui import * 325 | 326 | vectorLayer = QgsVectorLayer('testdata/network.gpkg|layername=network_lines', 'lines') 327 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth) 328 | strategy = QgsNetworkDistanceStrategy() 329 | director.addStrategy(strategy) 330 | 331 | builder = QgsGraphBuilder(vectorLayer.sourceCrs()) 332 | 333 | startPoint = QgsPointXY(1179661.925139,5419188.074362) 334 | endPoint = QgsPointXY(1180942.970617,5420040.097560) 335 | 336 | tiedPoints = director.makeGraph(builder, [startPoint, endPoint]) 337 | tStart, tStop = tiedPoints 338 | 339 | graph = builder.graph() 340 | idxStart = graph.findVertex(tStart) 341 | idxEnd = graph.findVertex(tStop) 342 | 343 | (tree, costs) = QgsGraphAnalyzer.dijkstra(graph, idxStart, 0) 344 | 345 | if tree[idxEnd] == -1: 346 | raise Exception('No route!') 347 | 348 | # Total cost 349 | cost = costs[idxEnd] 350 | 351 | # 添加最后一个点 352 | route = [graph.vertex(idxEnd).point()] 353 | 354 | # 遍历图 355 | while idxEnd != idxStart: 356 | idxEnd = graph.edge(tree[idxEnd]).fromVertex() 357 | route.insert(0, graph.vertex(idxEnd).point()) 358 | 359 | # 显示 360 | rb = QgsRubberBand(iface.mapCanvas()) 361 | rb.setColor(Qt.red) 362 | 363 | # 如果项目的坐标系和图层的坐标系不一样,则需要坐标转换 364 | for p in route: 365 | rb.addPoint(p) 366 | ``` 367 | 368 | ### 19.3.2 可达区域 369 | 370 | 顶点A的可达区域是指可以从顶点A进入的图形顶点的子集,并且从A到这些顶点的路径成本不超过某个值。 371 | 372 | 这一点可以通过以下例子更清楚地表明。"有一个消防站,消防车可以在5分钟内到达城市的哪些地方?10分钟?15分钟?"。这些问题的答案就是消防站的可达区域。 373 | 374 | 为了找到可达的区域,我们可以使用[`QgsGraphAnalyzer`](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer)类的[`dijkstra() `](https://qgis.org/pyqgis/master/analysis/QgsGraphAnalyzer.html#qgis.analysis.QgsGraphAnalyzer.dijkstra)方法。只需将cost数组的元素与预定义的值进行比较。如果cost[i]小于或等于一个预定义的值,那么顶点i就在可达区域内,否则它就在外面。 375 | 376 | 一个更困难的问题是要得到可达区域的边界。底部边界是仍可访问的顶点集合,而顶部边界是不可访问的顶点集合。事实上这很简单:它是基于最短路径树的边的可达性边界,对于这些边的源顶点是可访问的,而边的目标顶点则不是。 377 | 378 | 下面是一个例子: 379 | 380 | ```python 381 | director = QgsVectorLayerDirector(vectorLayer, -1, '', '', '', QgsVectorLayerDirector.DirectionBoth) 382 | strategy = QgsNetworkDistanceStrategy() 383 | director.addStrategy(strategy) 384 | builder = QgsGraphBuilder(vectorLayer.crs()) 385 | 386 | 387 | pStart = QgsPointXY(1179661.925139, 5419188.074362) 388 | delta = iface.mapCanvas().getCoordinateTransform().mapUnitsPerPixel() * 1 389 | 390 | rb = QgsRubberBand(iface.mapCanvas()) 391 | rb.setColor(Qt.green) 392 | rb.addPoint(QgsPointXY(pStart.x() - delta, pStart.y() - delta)) 393 | rb.addPoint(QgsPointXY(pStart.x() + delta, pStart.y() - delta)) 394 | rb.addPoint(QgsPointXY(pStart.x() + delta, pStart.y() + delta)) 395 | rb.addPoint(QgsPointXY(pStart.x() - delta, pStart.y() + delta)) 396 | 397 | tiedPoints = director.makeGraph(builder, [pStart]) 398 | graph = builder.graph() 399 | tStart = tiedPoints[0] 400 | 401 | idStart = graph.findVertex(tStart) 402 | 403 | (tree, cost) = QgsGraphAnalyzer.dijkstra(graph, idStart, 0) 404 | 405 | upperBound = [] 406 | r = 1500.0 407 | i = 0 408 | tree.reverse() 409 | 410 | while i < len(cost): 411 | if cost[i] > r and tree[i] != -1: 412 | outVertexId = graph.edge(tree [i]).toVertex() 413 | if cost[outVertexId] < r: 414 | upperBound.append(i) 415 | i = i + 1 416 | 417 | for i in upperBound: 418 | centerPoint = graph.vertex(i).point() 419 | rb = QgsRubberBand(iface.mapCanvas()) 420 | rb.setColor(Qt.red) 421 | rb.addPoint(QgsPointXY(centerPoint.x() - delta, centerPoint.y() - delta)) 422 | rb.addPoint(QgsPointXY(centerPoint.x() + delta, centerPoint.y() - delta)) 423 | rb.addPoint(QgsPointXY(centerPoint.x() + delta, centerPoint.y() + delta)) 424 | rb.addPoint(QgsPointXY(centerPoint.x() - delta, centerPoint.y() + delta)) 425 | ``` 426 | 427 | -------------------------------------------------------------------------------- /docs/21-PyQGIS速查表.md: -------------------------------------------------------------------------------- 1 | # 21 PyQGIS速查表 2 | 3 | ## 21.1 用户接口 4 | 5 | **改变外观** 6 | 7 | ```python 8 | from qgis.PyQt.QtWidgets import QApplication 9 | 10 | app = QApplication.instance() 11 | app.setStyleSheet(".QWidget {color: blue; background-color: yellow;}") 12 | # 你可以从文件读取样式 13 | with open("testdata/file.qss") as qss_file_content: 14 | app.setStyleSheet(qss_file_content.read()) 15 | ``` 16 | **改变图标和标题** 17 | 18 | ```python 19 | from qgis.PyQt.QtGui import QIcon 20 | 21 | icon = QIcon(r"/path/to/logo/file.png") 22 | iface.mainWindow().setWindowIcon(icon) 23 | iface.mainWindow().setWindowTitle("My QGIS") 24 | ``` 25 | ## 21.2 设置 26 | 27 | **获得设置列表** 28 | 29 | ```python 30 | from qgis.PyQt.QtCore import QSettings 31 | 32 | qs = QSettings() 33 | 34 | for k in sorted(qs.allKeys()): 35 | print (k) 36 | ``` 37 | ## 21.3 工具栏 38 | **移除工具栏** 39 | 40 | ```python 41 | from qgis.utils import iface 42 | 43 | toolbar = iface.helpToolBar() 44 | parent = toolbar.parentWidget() 45 | parent.removeToolBar(toolbar) 46 | 47 | # 添加 48 | parent.addToolBar(toolbar) 49 | ``` 50 | **移除操作** 51 | 52 | ```python 53 | actions = iface.attributesToolBar().actions() 54 | iface.attributesToolBar().clear() 55 | iface.attributesToolBar().addAction(actions[4]) 56 | iface.attributesToolBar().addAction(actions[3]) 57 | ``` 58 | ## 21.4 菜单 59 | 60 | **移除菜单** 61 | 62 | ```python 63 | from qgis.utils import iface 64 | 65 | # 帮助菜单 66 | menu = iface.helpMenu() 67 | menubar = menu.parentWidget() 68 | menubar.removeAction(menu.menuAction()) 69 | 70 | # 添加 71 | menubar.addAction(menu.menuAction()) 72 | ``` 73 | ## 21.5 画布 74 | **访问画布** 75 | 76 | ```python 77 | from qgis.utils import iface 78 | 79 | canvas = iface.mapCanvas() 80 | ``` 81 | **改变画布颜色** 82 | 83 | ```python 84 | from qgis.PyQt.QtCore import Qt 85 | 86 | iface.mapCanvas().setCanvasColor(Qt.black) 87 | iface.mapCanvas().refresh() 88 | ``` 89 | **画布刷新间隔** 90 | 91 | ```python 92 | from qgis.PyQt.QtCore import QSettings 93 | # 150毫秒 94 | QSettings().setValue("/qgis/map_update_interval", 150) 95 | ``` 96 | ## 21.6 图层 97 | **添加图层** 98 | 99 | ```python 100 | from qgis.utils import iface 101 | 102 | layer = iface.addVectorLayer("/path/to/shapefile/file.shp", "layer name you like", "ogr") 103 | if not layer: 104 | print("Layer failed to load!") 105 | ``` 106 | **获取当前图层** 107 | 108 | ```python 109 | layer = iface.activeLayer() 110 | ``` 111 | **图层列表** 112 | 113 | ```python 114 | from qgis.core import QgsProject 115 | 116 | QgsProject.instance().mapLayers().values() 117 | ``` 118 | 119 | **获得图层名称** 120 | 121 | ```python 122 | from qgis.core import QgsVectorLayer 123 | layer = QgsVectorLayer("Point?crs=EPSG:4326", "layer name you like", "memory") 124 | QgsProject.instance().addMapLayer(layer) 125 | 126 | layers_names = [] 127 | for layer in QgsProject.instance().mapLayers().values(): 128 | layers_names.append(layer.name()) 129 | 130 | print("layers TOC = {}".format(layers_names)) 131 | 132 | layers_names = [layer.name() for layer in QgsProject.instance().mapLayers().values()] 133 | print("layers TOC = {}".format(layers_names)) 134 | ``` 135 | **通过名称查找图层** 136 | 137 | ```python 138 | from qgis.core import QgsProject 139 | 140 | layer = QgsProject.instance().mapLayersByName("layer name you like")[0] 141 | print(layer.name()) 142 | ``` 143 | **设置当前图层** 144 | 145 | ```python 146 | from qgis.core import QgsProject 147 | 148 | layer = QgsProject.instance().mapLayersByName("layer name you like")[0] 149 | iface.setActiveLayer(layer) 150 | ``` 151 | **刷新图层** 152 | 153 | ```python 154 | from qgis.core import QgsProject 155 | 156 | layer = QgsProject.instance().mapLayersByName("layer name you like")[0] 157 | # 5秒 158 | layer.setAutoRefreshInterval(5000) 159 | # 自动刷新 160 | layer.setAutoRefreshMode(Qgis.AutoRefreshMode.ReloadData) 161 | ``` 162 | **添加表单要素** 163 | 164 | ```python 165 | from qgis.core import QgsFeature, QgsGeometry 166 | 167 | feat = QgsFeature() 168 | geom = QgsGeometry() 169 | feat.setGeometry(geom) 170 | feat.setFields(layer.fields()) 171 | 172 | iface.openFeatureForm(layer, feat, False) 173 | ``` 174 | **添加无表单要素** 175 | 176 | ```python 177 | from qgis.core import QgsPointXY 178 | 179 | pr = layer.dataProvider() 180 | feat = QgsFeature() 181 | feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10,10))) 182 | pr.addFeatures([feat]) 183 | ``` 184 | **获得要素** 185 | 186 | ```python 187 | for f in layer.getFeatures(): 188 | print (f) 189 | ``` 190 | **获得已选要素** 191 | 192 | ```python 193 | for f in layer.selectedFeatures(): 194 | print (f) 195 | ``` 196 | **获得要素id** 197 | 198 | ```python 199 | selected_ids = layer.selectedFeatureIds() 200 | print(selected_ids) 201 | ``` 202 | **从已选要素id创建内存图层** 203 | 204 | ```python 205 | from qgis.core import QgsFeatureRequest 206 | 207 | memory_layer = layer.materialize(QgsFeatureRequest().setFilterFids(layer.selectedFeatureIds())) 208 | QgsProject.instance().addMapLayer(memory_layer) 209 | ``` 210 | **获得几何** 211 | 212 | ```python 213 | # 点图层 214 | for f in layer.getFeatures(): 215 | geom = f.geometry() 216 | print ('%f, %f' % (geom.asPoint().y(), geom.asPoint().x())) 217 | ``` 218 | **移动几何** 219 | 220 | ```python 221 | from qgis.core import QgsFeature, QgsGeometry 222 | poly = QgsFeature() 223 | geom = QgsGeometry.fromWkt("POINT(7 45)") 224 | geom.translate(1, 1) 225 | poly.setGeometry(geom) 226 | print(poly.geometry()) 227 | ``` 228 | **设置坐标参考系统** 229 | 230 | ```python 231 | from qgis.core import QgsProject, QgsCoordinateReferenceSystem 232 | 233 | for layer in QgsProject.instance().mapLayers().values(): 234 | layer.setCrs(QgsCoordinateReferenceSystem(4326, QgsCoordinateReferenceSystem.EpsgCrsId)) 235 | ``` 236 | **查看坐标参考系统** 237 | 238 | ```python 239 | from qgis.core import QgsProject 240 | 241 | for layer in QgsProject.instance().mapLayers().values(): 242 | crs = layer.crs().authid() 243 | layer.setName('{} ({})'.format(layer.name(), crs)) 244 | ``` 245 | **隐藏字段** 246 | 247 | ```python 248 | from qgis.core import QgsEditorWidgetSetup 249 | 250 | def fieldVisibility (layer,fname): 251 | setup = QgsEditorWidgetSetup('Hidden', {}) 252 | for i, column in enumerate(layer.fields()): 253 | if column.name()==fname: 254 | layer.setEditorWidgetSetup(idx, setup) 255 | break 256 | else: 257 | continue 258 | ``` 259 | **图层添加WKT要素** 260 | 261 | ```python 262 | from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry, QgsProject 263 | 264 | layer = QgsVectorLayer('Polygon?crs=epsg:4326', 'Mississippi', 'memory') 265 | pr = layer.dataProvider() 266 | poly = QgsFeature() 267 | geom = QgsGeometry.fromWkt("POLYGON ((-88.82 34.99,-88.09 34.89,-88.39 30.34,-89.57 30.18,-89.73 31,-91.63 30.99,-90.87 32.37,-91.23 33.44,-90.93 34.23,-90.30 34.99,-88.82 34.99))") 268 | poly.setGeometry(geom) 269 | pr.addFeatures([poly]) 270 | layer.updateExtents() 271 | QgsProject.instance().addMapLayers([layer]) 272 | ``` 273 | **从GeoPackage加载所有图层** 274 | 275 | ```python 276 | from qgis.core import QgsVectorLayer, QgsProject 277 | 278 | fileName = "/path/to/gpkg/file.gpkg" 279 | layer = QgsVectorLayer(fileName,"test","ogr") 280 | subLayers =layer.dataProvider().subLayers() 281 | 282 | for subLayer in subLayers: 283 | name = subLayer.split('!!::!!')[1] 284 | uri = "%s|layername=%s" % (fileName, name,) 285 | # 创建图层 286 | sub_vlayer = QgsVectorLayer(uri, name, 'ogr') 287 | # Add layer to map 288 | QgsProject.instance().addMapLayer(sub_vlayer) 289 | ``` 290 | **加载瓦片图层(xyz)** 291 | 292 | ```python 293 | from qgis.core import QgsRasterLayer, QgsProject 294 | 295 | def loadXYZ(url, name): 296 | rasterLyr = QgsRasterLayer("type=xyz&url=" + url, name, "wms") 297 | QgsProject.instance().addMapLayer(rasterLyr) 298 | 299 | urlWithParams = 'type=xyz&url=https://a.tile.openstreetmap.org/%7Bz%7D/%7Bx%7D/%7By%7D.png&zmax=19&zmin=0&crs=EPSG3857' 300 | loadXYZ(urlWithParams, 'OpenStreetMap') 301 | ``` 302 | **移除所有图层** 303 | 304 | ```python 305 | QgsProject.instance().removeAllMapLayers() 306 | ``` 307 | **移除全部** 308 | 309 | ```python 310 | QgsProject.instance().clear() 311 | ``` 312 | ## 21.7 目录 313 | **访问选中的图层** 314 | 315 | ```python 316 | from qgis.utils import iface 317 | 318 | iface.mapCanvas().layers() 319 | ``` 320 | **移除上下文菜单** 321 | 322 | ```python 323 | ltv = iface.layerTreeView() 324 | mp = ltv.menuProvider() 325 | ltv.setMenuProvider(None) 326 | # 恢复 327 | ltv.setMenuProvider(mp) 328 | ``` 329 | ## 21.8 高级目录 330 | **根节点** 331 | 332 | ```python 333 | from qgis.core import QgsVectorLayer, QgsProject, QgsLayerTreeLayer 334 | 335 | root = QgsProject.instance().layerTreeRoot() 336 | node_group = root.addGroup("My Group") 337 | 338 | layer = QgsVectorLayer("Point?crs=EPSG:4326", "layer name you like", "memory") 339 | QgsProject.instance().addMapLayer(layer, False) 340 | 341 | node_group.addLayer(layer) 342 | 343 | print(root) 344 | print(root.children()) 345 | ``` 346 | **访问第一个子节点** 347 | 348 | ```python 349 | from qgis.core import QgsLayerTreeGroup, QgsLayerTreeLayer, QgsLayerTree 350 | 351 | child0 = root.children()[0] 352 | print (child0.name()) 353 | print (type(child0)) 354 | print (isinstance(child0, QgsLayerTreeLayer)) 355 | print (isinstance(child0.parent(), QgsLayerTree)) 356 | ``` 357 | **查找图层组和所有节点** 358 | 359 | ```python 360 | from qgis.core import QgsLayerTreeGroup, QgsLayerTreeLayer 361 | 362 | def get_group_layers(group): 363 | print('- group: ' + group.name()) 364 | for child in group.children(): 365 | if isinstance(child, QgsLayerTreeGroup): 366 | # 遍历嵌套图层组 367 | get_group_layers(child) 368 | else: 369 | print(' - layer: ' + child.name()) 370 | 371 | 372 | root = QgsProject.instance().layerTreeRoot() 373 | for child in root.children(): 374 | if isinstance(child, QgsLayerTreeGroup): 375 | get_group_layers(child) 376 | elif isinstance(child, QgsLayerTreeLayer): 377 | print ('- layer: ' + child.name()) 378 | ``` 379 | **通过名称查找图层组** 380 | 381 | ```python 382 | print (root.findGroup("My Group")) 383 | ``` 384 | **通过id查找图层组** 385 | 386 | ```python 387 | print (root.findLayer(layer.layerId())) 388 | ``` 389 | **添加图层** 390 | 391 | ```python 392 | from qgis.core import QgsVectorLayer, QgsProject 393 | 394 | layer1 = QgsVectorLayer("Point?crs=EPSG:4326", "layer name you like", "memory") 395 | QgsProject.instance().addMapLayer(layer1, False) 396 | node_layer1 = root.addLayer(layer1) 397 | ``` 398 | **添加图层组** 399 | 400 | ```python 401 | from qgis.core import QgsLayerTreeGroup 402 | 403 | node_group2 = QgsLayerTreeGroup("Group 2") 404 | root.addChildNode(node_group2) 405 | ``` 406 | **移除加载的图层** 407 | 408 | ```python 409 | layer = QgsProject.instance().mapLayersByName("layer name you like")[0] 410 | root = QgsProject.instance().layerTreeRoot() 411 | 412 | myLayer = root.findLayer(layer.id()) 413 | myClone = myLayer.clone() 414 | parent = myLayer.parent() 415 | 416 | myGroup = root.findGroup("My Group") 417 | # 插入到第一个位置 418 | myGroup.insertChildNode(0, myClone) 419 | 420 | parent.removeChildNode(myLayer) 421 | ``` 422 | **移除指定图层组** 423 | 424 | ```python 425 | QgsProject.instance().addMapLayer(layer, False) 426 | 427 | root = QgsProject.instance().layerTreeRoot() 428 | myGroup = root.findGroup("My Group") 429 | myOriginalLayer = root.findLayer(layer.id()) 430 | myLayer = myOriginalLayer.clone() 431 | myGroup.insertChildNode(0, myLayer) 432 | parent.removeChildNode(myOriginalLayer) 433 | ``` 434 | **改变可见性** 435 | 436 | ```python 437 | root = QgsProject.instance().layerTreeRoot() 438 | node = root.findLayer(layer.id()) 439 | new_state = Qt.Checked if node.isVisible() == Qt.Unchecked else Qt.Unchecked 440 | node.setItemVisibilityChecked(new_state) 441 | ``` 442 | 443 | **图层组是否被选择** 444 | 445 | ```python 446 | def isMyGroupSelected( groupName ): 447 | myGroup = QgsProject.instance().layerTreeRoot().findGroup( groupName ) 448 | return myGroup in iface.layerTreeView().selectedNodes() 449 | 450 | print(isMyGroupSelected( 'my group name' )) 451 | ``` 452 | 453 | **移动节点** 454 | 455 | ```python 456 | cloned_group1 = node_group.clone() 457 | root.insertChildNode(0, cloned_group1) 458 | root.removeChildNode(node_group) 459 | ``` 460 | **展开节点** 461 | 462 | ```python 463 | print(myGroup.isExpanded()) 464 | myGroup.setExpanded(False) 465 | ``` 466 | 467 | **隐藏节点** 468 | 469 | ```python 470 | from qgis.core import QgsProject 471 | 472 | model = iface.layerTreeView().layerTreeModel() 473 | ltv = iface.layerTreeView() 474 | root = QgsProject.instance().layerTreeRoot() 475 | 476 | layer = QgsProject.instance().mapLayersByName('layer name you like')[0] 477 | node = root.findLayer(layer.id()) 478 | 479 | index = model.node2index( node ) 480 | ltv.setRowHidden( index.row(), index.parent(), True ) 481 | node.setCustomProperty( 'nodeHidden', 'true') 482 | ltv.setCurrentIndex(model.node2index(root)) 483 | ``` 484 | 485 | **节点信号** 486 | 487 | ```python 488 | def onWillAddChildren(node, indexFrom, indexTo): 489 | print ("WILL ADD", node, indexFrom, indexTo) 490 | 491 | def onAddedChildren(node, indexFrom, indexTo): 492 | print ("ADDED", node, indexFrom, indexTo) 493 | 494 | root.willAddChildren.connect(onWillAddChildren) 495 | root.addedChildren.connect(onAddedChildren) 496 | ``` 497 | 498 | **移除图层** 499 | 500 | ```python 501 | root.removeLayer(layer) 502 | ``` 503 | 504 | **移除图层组** 505 | 506 | ```python 507 | root.removeChildNode(node_group2) 508 | ``` 509 | 510 | **创建新的目录树** 511 | 512 | ```python 513 | root = QgsProject.instance().layerTreeRoot() 514 | model = QgsLayerTreeModel(root) 515 | view = QgsLayerTreeView() 516 | view.setModel(model) 517 | view.show() 518 | ``` 519 | 520 | **移动节点** 521 | 522 | ```python 523 | cloned_group1 = node_group.clone() 524 | root.insertChildNode(0, cloned_group1) 525 | root.removeChildNode(node_group) 526 | ``` 527 | 528 | **重命名节点** 529 | 530 | ```python 531 | cloned_group1.setName("Group X") 532 | node_layer1.setName("Layer X") 533 | ``` 534 | ## 21.9 处理算法 535 | **获得算法列表** 536 | 537 | ```python 538 | from qgis.core import QgsApplication 539 | 540 | for alg in QgsApplication.processingRegistry().algorithms(): 541 | if 'buffer' == alg.name(): 542 | print("{}:{} --> {}".format(alg.provider().name(), alg.name(), alg.displayName())) 543 | ``` 544 | **获得算法帮助** 545 | 546 | ```python 547 | from qgis import processing 548 | 549 | processing.algorithmHelp("qgis:randomselection") 550 | ``` 551 | **运行算法** 552 | 553 | 本示例,结果存储在添加到项目的临时内存层中。 554 | 555 | ```python 556 | from qgis import processing 557 | result = processing.run("native:buffer", {'INPUT': layer, 'OUTPUT': 'memory:'}) 558 | QgsProject.instance().addMapLayer(result['OUTPUT']) 559 | ``` 560 | **算法统计** 561 | 562 | ```python 563 | from qgis.core import QgsApplication 564 | 565 | len(QgsApplication.processingRegistry().algorithms()) 566 | ``` 567 | **数据提供者统计** 568 | 569 | ```python 570 | from qgis.core import QgsApplication 571 | 572 | len(QgsApplication.processingRegistry().providers()) 573 | ``` 574 | **表达式统计** 575 | 576 | ```python 577 | from qgis.core import QgsExpression 578 | 579 | len(QgsExpression.Functions()) 580 | ``` 581 | ## 21.10 装饰器 582 | **版权** 583 | 584 | ```python 585 | from qgis.PyQt.Qt import QTextDocument 586 | from qgis.PyQt.QtGui import QFont 587 | 588 | mQFont = "Sans Serif" 589 | mQFontsize = 9 590 | mLabelQString = "© QGIS 2019" 591 | mMarginHorizontal = 0 592 | mMarginVertical = 0 593 | mLabelQColor = "#FF0000" 594 | 595 | INCHES_TO_MM = 0.0393700787402 # 1 毫米 = 0.0393700787402 英寸 596 | case = 2 597 | 598 | def add_copyright(p, text, xOffset, yOffset): 599 | p.translate( xOffset , yOffset ) 600 | text.drawContents(p) 601 | p.setWorldTransform( p.worldTransform() ) 602 | 603 | def _on_render_complete(p): 604 | deviceHeight = p.device().height() # 获取绘制设备高度 605 | deviceWidth = p.device().width() # 获取绘制设备宽度 606 | # 创建一个新的富文本容器 607 | text = QTextDocument() 608 | font = QFont() 609 | font.setFamily(mQFont) 610 | font.setPointSize(int(mQFontsize)) 611 | text.setDefaultFont(font) 612 | style = "" 613 | text.setHtml( style + "

" + mLabelQString + "

" ) 614 | # 文本大小 615 | size = text.size() 616 | 617 | # 渲染 618 | pixelsInchX = p.device().logicalDpiX() 619 | pixelsInchY = p.device().logicalDpiY() 620 | xOffset = pixelsInchX * INCHES_TO_MM * int(mMarginHorizontal) 621 | yOffset = pixelsInchY * INCHES_TO_MM * int(mMarginVertical) 622 | 623 | # 计算点位 624 | if case == 0: 625 | # 左上 626 | add_copyright(p, text, xOffset, yOffset) 627 | 628 | elif case == 1: 629 | # 左下 630 | yOffset = deviceHeight - yOffset - size.height() 631 | add_copyright(p, text, xOffset, yOffset) 632 | 633 | elif case == 2: 634 | # 右上 635 | xOffset = deviceWidth - xOffset - size.width() 636 | add_copyright(p, text, xOffset, yOffset) 637 | 638 | elif case == 3: 639 | # 右下 640 | yOffset = deviceHeight - yOffset - size.height() 641 | xOffset = deviceWidth - xOffset - size.width() 642 | add_copyright(p, text, xOffset, yOffset) 643 | 644 | elif case == 4: 645 | # 上中心点 646 | xOffset = deviceWidth / 2 647 | add_copyright(p, text, xOffset, yOffset) 648 | 649 | else: 650 | # 下中心点 651 | yOffset = deviceHeight - yOffset - size.height() 652 | xOffset = deviceWidth / 2 653 | add_copyright(p, text, xOffset, yOffset) 654 | 655 | # 当画布渲染完成后发送信号 656 | iface.mapCanvas().renderComplete.connect(_on_render_complete) 657 | # 重绘画布 658 | iface.mapCanvas().refresh() 659 | ``` 660 | 661 | ## 21.11 创作者 662 | 663 | **按名称获取打印布局** 664 | 665 | ```python 666 | composerTitle = 'MyComposer' # 创作者名称 667 | 668 | project = QgsProject.instance() 669 | projectLayoutManager = project.layoutManager() 670 | layout = projectLayoutManager.layoutByName(composerTitle) 671 | ``` 672 | 673 | ## 21.12 来源 674 | 675 | - [QGIS Python (PyQGIS) API](https://qgis.org/pyqgis/master/) 676 | - [QGIS C++ API](https://qgis.org/api/) 677 | - [StackOverFlow QGIS questions](https://stackoverflow.com/questions/tagged/qgis) 678 | - [Script by Klas Karlsson](https://raw.githubusercontent.com/klakar/QGIS_resources/master/collections/Geosupportsystem/python/qgis_basemaps.py) 679 | 680 | -------------------------------------------------------------------------------- /docs/16-开发Python插件.md: -------------------------------------------------------------------------------- 1 | # 16 开发Python插件 2 | 3 | 可以用Python编程语言创建插件。与用C ++编写插件相比,由于Python语言的动态特性,这些插件应该更容易编写,理解,维护和分发。 4 | 5 | Python插件与QGIS插件管理器中的C ++插件一起列出。他们在`~/(UserProfile)/python/plugins`和以下路径中搜索: 6 | 7 | - UNIX / Mac上: `(qgis_prefix)/share/qgis/python/plugins` 8 | - Windows: `(qgis_prefix)/python/plugins` 9 | 10 | 有关`~`和`(UserProfile)`的定义,请查看[Core和External插件](https://docs.qgis.org/latest/en/docs/user_manual/plugins/plugins.html#core-and-external-plugins)。 11 | 12 | !!! 提示 13 | 14 | 将QGIS_PLUGINPATH设置为一个存在的目录路径,可以将此路径添加到插件的搜索路径列表中。 15 | 16 | 17 | ## 16.1 构建Python插件 18 | 19 | 创建插件,需要以下步骤: 20 | 21 | 1. *想法*:你想要使用新的QGIS插件做什么。你为什么要这么做?你想解决什么问题?这个问题已经有另一个插件吗? 22 | 2. *创建文件*:一些必要文件(查看[插件文件](#16111)) 23 | 3. *编写代码*:在恰当的文件中写代码 24 | 3. 文档:编写插件文档 25 | 3. 可选:翻译:将插件翻译成不同的语言 26 | 4. *测试*:如果准备就绪,[重新加载插件](#1615) 27 | 5. *发布*:在QGIS仓库中发布你的插件或将你自己的仓库作为个人“GIS武器”的“武器库”。 28 | 29 | ### 16.1.1 准备开始 30 | 31 | 在开始编写新的插件之前,先看看[Python官方插件库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/releasing.html#official-pyqgis-repository)。现有插件的源代码可以帮助你了解更多编程知识。你也可能会发现类似的插件已经存在,你可以扩展它,或者至少在它的基础上开发自己的插件。 32 | 33 | #### 16.1.1.1 插件文件结构 34 | 35 | 开始使用一个新的插件,我们需要设置必要的插件文件。 36 | 37 | 有两种插件模板资源可以帮助你入门: 38 | 39 | - 为了教育目的或者在需要采用极简主义方法时,[最小化插件模板](https://github.com/wonder-sk/qgis-minimal-plugin)提供了创建有效的QGIS Python插件所需的基本文件(框架)。 40 | - 更加完整的插件模板,[插件构建器](https://plugins.qgis.org/plugins/pluginbuilder3/)可以创建多种不同类型的插件模板,包括本地化(翻译)和测试等功能。 41 | 42 | 典型的插件目录包括以下文件: 43 | 44 | - `metadata.txt` - *必须* - 包含插件网站和插件基础结构使用的常规信息,版本、名称和一些其他元数据。 45 | - `__init__.py` - *必须* - 插件的入口。它必须具有 `classFactory()`方法,还可以具有任何其他初始化代码。 46 | - `mainPlugin.py` - *核心代码* - 插件的主要工作代码。包含有关插件操作和主要代码的所有信息。 47 | - `form.ui` - *插件自定义UI* - Qt设计师创建的GUI。 48 | - `form.py` - *编译的GUI* - 将上面描述的form.ui转换为Python代码。 49 | - `resources.qrc` - *可选* - Qt设计师创建的xml文档。包含表单资源的相对路径。 50 | - `resources.py` - *编译的资源文件,可选* - 将上述.qrc文件转换为Python代码。 51 | 52 | !!! warning 警告 53 | 54 | 如果您打算将插件上传到[Python官方插件库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/releasing.html#official-pyqgis-repository),则必须检查插件是否遵循插件[验证](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/releasing.html#official-pyqgis-repository-validation)所必需的一些附加规则 55 | 56 | ### 16.1.2 编写插件代码 57 | 58 | 以下部分显示了应该在上面介绍的每个文件中添加哪些内容。 59 | 60 | #### 16.1.2.1 metadata.txt 61 | 62 | 首先,插件管理器需要检索有关插件的一些基本信息,例如其名称、描述等。文件`metadata.txt`存储此信息。 63 | 64 | !!! 提示 65 | 66 | 所有元数据必须采用UTF-8编码。 67 | 68 | | 元数据名称 | 是否必需 | 描述 | 69 | | :-------------------- | :------: | :----------------------------------------------------------- | 70 | | name | 是 | 插件名称,短字符串 | 71 | | qgisMinimumVersion | 是 | 最小QGIS版本 | 72 | | qgisMaximumVersion | 否 | 最大QGIS版本 | 73 | | description | 是 | 描述插件的简短文本,不支持HTML | 74 | | about | 是 | 较长的文本,详细描述插件,不支持HTML | 75 | | version | 是 | 版本 | 76 | | author | 是 | 作者姓名 | 77 | | email | 是 | 作者的电子邮件,在网站上仅显示给登录的用户,但在插件安装后可在插件管理器中看到 | 78 | | changelog | 否 | 字符串,可以是多行,不支持HTML | 79 | | experimental | 否 | 实验性,布尔值,True或False | 80 | | deprecated | 否 | 弃用,boolean值,True或False,适用于整个插件,而不仅仅适用于上传的版本 | 81 | | tags | 否 | 以逗号分隔的列表,允许在单个标记内使用空格 | 82 | | homepage | 否 | 指向插件主页的有效网址 | 83 | | repository | 是 | 源代码存储库的有效URL | 84 | | tracker | 否 | 故障和错误报告的有效URL | 85 | | icon | 否 | 对于web友好的图像(PNG,JPEG)文件名或相对路径(相对于插件压缩包的文件夹) | 86 | | category | 否 | `Raster`, `Vector`, `Database`, `Mesh` and `Web`(栅格、矢量、数据库和网络) | 87 | | plugin_dependencies | 否 | 类似于PIP的逗号分隔的其他插件列表,使用来自元数据名称字段的插件名称 | 88 | | server | 否 | 布尔值,True或False,确定插件是否具有服务器接口 | 89 | | hasProcessingProvider | 否 | 布尔值,True或False,确定插件是否提供处理算法 | 90 | 91 | 默认情况下,插件放在 **Plugins** 菜单中(我们将在下一节中看到如何为插件添加菜单项),但也可以将它们放入 **Raster** , **Vector** , **Database** ,**Mesh** 和 **Web** 菜单中。 92 | 93 | 输入指定的“category”元数据,可以相应地对插件进行分类。此元数据用于提示用户,并告诉他们可以在哪里(在哪个菜单中)找到该插件。“category”的允许值为:Vector, Raster, Database或者Web。例如,如果你的插件可以从Raster菜单中找到,请将其添加到`metadata.txt`中 94 | 95 | ```ini 96 | category=Raster 97 | ``` 98 | 99 | !!! 提示 100 | 101 | 如果qgisMaximumVersion为空,则在上传到[官方Python插件库](#1643-python)时,它将自动设置为主要版本加上.99(例如:3.99)。 102 | 103 | 104 | metadata.txt示例: 105 | 106 | ```ini 107 | ; 以下是强制性的 108 | 109 | [general] 110 | name=HelloWorld 111 | email=me@example.com 112 | author=Just Me 113 | qgisMinimumVersion=3.0 114 | description=This is an example plugin for greeting the world.\ 115 | Multiline is allowed:\ 116 | lines starting with spaces belong to the same\ 117 | field, in this case to the "description" field.\ 118 | HTML formatting is not allowed. 119 | about=This paragraph can contain a detailed description\ 120 | of the plugin. Multiline is allowed, HTML is not. 121 | version=version 1.2 122 | tracker=http://bugs.itopen.it 123 | repository=http://www.itopen.it/repo 124 | ; 结束强制 125 | 126 | ; 以下是可选的 127 | category=Raster 128 | changelog=The changelog lists the plugin versions\ 129 | and their changes as in the example below:\ 130 | 1.0 - First stable release\ 131 | 0.9 - All features implemented\ 132 | 0.8 - First testing release 133 | 134 | ; 标签采用逗号分隔,标签名称允许使用空格 135 | ; 标签应该是英文的,在创建之前请检查现有标签和同义词 136 | tags=wkt,raster,hello world 137 | 138 | ; 这些元数据可以为空,最终将成为强制性的。 139 | homepage=https://www.itopen.it 140 | icon=icon.png 141 | 142 | ; 实验标志(适用于单一版本) 143 | experimental=True 144 | 145 | ; 弃用标志 (适用于整个插件,不仅适用于上传的版本) 146 | deprecated=False 147 | 148 | ; 如果为空,它将自动设置为主要版本+.99 149 | qgisMaximumVersion=3.99 150 | 151 | ; 从 QGIS 3.8开始,可以指定以逗号分隔指定要安装(或更新)的插件列表 152 | ; 下面示例安装或更新版本1.12的“MyOtherPlugin”和任何版本的“YetAnotherPlugin” 153 | plugin_dependencies=MyOtherPlugin==1.12,YetAnotherPlugin 154 | ``` 155 | 156 | #### 16.1.2.2 \_\_init\_\_.py 157 | 158 | Python包需要此文件。此外,QGIS要求此文件包含一个`classFactory()`函数,该函数在插件被加载到QGIS时调用。它接收[`QgisInterface`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)实例, 并且必须返回`mainplugin.py`中插件类的对象——在我们的例子中它被命名为`TestPlugin`(见下文)。`__init__.py`应该是这样的: 159 | 160 | ```python 161 | def classFactory(iface): 162 | from .mainPlugin import TestPlugin 163 | return TestPlugin(iface) 164 | 165 | # 任何其他初始化 166 | ``` 167 | 168 | #### 16.1.2.3 mainPlugin.py 169 | 170 | 这就是魔法发生的地方,下面就是魔法的例子: 171 | 172 | ```python 173 | from qgis.PyQt.QtGui import * 174 | from qgis.PyQt.QtWidgets import * 175 | 176 | # 从文件resources.py初始化Qt的资源 177 | from . import resources 178 | 179 | 180 | class TestPlugin: 181 | 182 | def __init__(self, iface): 183 | # 保存QGIS interface引用 184 | self.iface = iface 185 | 186 | def initGui(self): 187 | # 创建操作,它将启动插件配置 188 | self.action = QAction(QIcon(":/plugins/testplug/icon.png"), "Test plugin", self.iface.mainWindow()) 189 | self.action.setObjectName("testAction") 190 | self.action.setWhatsThis("Configuration for test plugin") 191 | self.action.setStatusTip("This is status tip") 192 | self.action.triggered.connect(self.run) 193 | 194 | # 添加工具栏按钮和菜单项 195 | self.iface.addToolBarIcon(self.action) 196 | self.iface.addPluginToMenu("&Test plugins", self.action) 197 | 198 | # 连接信号renderComplete——画布渲染完成后发送的信号 199 | self.iface.mapCanvas().renderComplete.connect(self.renderTest) 200 | 201 | def unload(self): 202 | # 删除插件菜单项和图标 203 | self.iface.removePluginMenu("&Test plugins", self.action) 204 | self.iface.removeToolBarIcon(self.action) 205 | 206 | # 断开信号 207 | self.iface.mapCanvas().renderComplete.disconnect(self.renderTest) 208 | 209 | def run(self): 210 | # 创建并显示一个配置对话框或类似的事情 211 | print("TestPlugin: run called!") 212 | 213 | def renderTest(self, painter): 214 | 215 | # 使用painter绘制地图画布 216 | print("TestPlugin: renderTest called!") 217 | ``` 218 | 219 | 主插件源文件中(例如 `mainPlugin.py`)必须存在的插件函数是: 220 | 221 | - `__init__` - >可以访问QGIS界面 222 | - `initGui()` - >加载插件时调用 223 | - `unload()` - >卸载插件时调用 224 | 225 | 在上面的例子中,[`addPluginToMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToMenu)被使用。这会将相应的菜单操作添加到 **Plugins** 菜单中。存在额外的方法将操作(action)添加到不同菜单。以下是这些方法的列表: 226 | 227 | - [`addPluginToRasterMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToRasterMenu) 228 | - [`addPluginToVectorMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToVectorMenu) 229 | - [`addPluginToDatabaseMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToDatabaseMenu) 230 | - [`addPluginToWebMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToWebMenu) 231 | 232 | 它们都具有与[`addPluginToMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.addPluginToMenu)方法相同的语法 。 233 | 234 | 建议将插件菜单添加到其中一个预定义方法,以保持插件条目组织方式的一致性。但是,你可以将自定义菜单组直接添加到菜单栏,如下面示例所示: 235 | 236 | ```python 237 | def initGui(self): 238 | self.menu = QMenu(self.iface.mainWindow()) 239 | self.menu.setObjectName("testMenu") 240 | self.menu.setTitle("MyMenu") 241 | 242 | self.action = QAction(QIcon(":/plugins/testplug/icon.png"), "Test plugin", self.iface.mainWindow()) 243 | self.action.setObjectName("testAction") 244 | self.action.setWhatsThis("Configuration for test plugin") 245 | self.action.setStatusTip("This is status tip") 246 | self.action.triggered.connect(self.run) 247 | self.menu.addAction(self.action) 248 | 249 | menuBar = self.iface.mainWindow().menuBar() 250 | menuBar.insertMenu(self.iface.firstRightStandardMenu().menuAction(), self.menu) 251 | 252 | def unload(self): 253 | self.menu.deleteLater() 254 | ``` 255 | 256 | 不要忘记设置`QAction`和`QMenu` `objectName`插件的特定名称,以便可以自定义。 257 | 258 | 虽然帮助和关于操作也可以添加到你的自定义菜单中,但QGIS主 **帮助** ► **插件** 菜单中有一个简便的地方。这是使用[`pluginHelpMenu()`](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface.pluginHelpMenu)方法完成的。 259 | 260 | ```python 261 | def initGui(self): 262 | 263 | self.help_action = QAction( 264 | QIcon(":/plugins/testplug/icon.png"), 265 | self.tr("Test Plugin..."), 266 | self.iface.mainWindow() 267 | ) 268 | # 添加操作到帮助菜单 269 | self.iface.pluginHelpMenu().addAction(self.help_action) 270 | 271 | self.help_action.triggered.connect(self.show_help) 272 | 273 | @staticmethod 274 | def show_help(): 275 | """打开在线帮助""" 276 | QDesktopServices.openUrl(QUrl('https://docs.qgis.org')) 277 | 278 | def unload(self): 279 | 280 | self.iface.pluginHelpMenu().removeAction(self.help_action) 281 | del self.help_action 282 | ``` 283 | 284 | ### 16.1.3 文档 285 | 286 | 该插件的文档可以编写为HTML帮助文档。`qgis.utils`模块提供了一个函数,`showPluginHelp()`将打开帮助文档浏览器,与其他QGIS帮助文档的方式相同。 287 | 288 | `showPluginHelp()`函数在与调用模块相同的目录中查找帮助文档。它会寻找`index-ll_cc.html`,`index-ll.html`,`index-en.html`,`index-en_us.html`和`index.html`,显示它找到的第一个文档。这`ll_cc`是QGIS语言环境,这允许文档有多个翻译。 289 | 290 | `showPluginHelp()`函数还可以使用参数`packageName`——它显示指定插件的帮助文档,`filename`——可以替换被搜索的文件名中的“index”,section——它是html锚标记的名称,浏览器将定位到该位置。 291 | 292 | ### 16.1.4 翻译 293 | 294 | 通过几个步骤,你可以设置插件本地化的环境,以便根据计算机的区域,插件将以不同语言加载。 295 | 296 | #### 16.1.4.1 软件要求 297 | 298 | 创建和管理所有翻译文件的最简单方法是安装 [Qt Linguist](https://doc.qt.io/qt-5/qtlinguist-index.html)。在基于Debian的GNU / Linux环境中,你可以这样安装它: 299 | 300 | ```shell 301 | sudo apt-get install qttools5-dev-tools 302 | ``` 303 | 304 | #### 16.1.4.2 文件和目录 305 | 306 | 创建插件时,你将在主插件目录中找到该文件夹`i18n`。 307 | 308 | **所有翻译文件都必须放在此目录中。** 309 | 310 | ##### 16.1.4.2.1 .pro文件 311 | 312 | 首先,你应该创建一个`.pro`文件,这是一个可以由 **Qt Linguist** 管理的 *项目* 文件。 313 | 314 | 在此`.pro`文件中,你必须指定要翻译的所有文件和窗体(.ui文件)。此文件用于设置本地化文件和变量。下面是一个项目文件,匹配我们的[示例插件](#16111)的结构 : 315 | 316 | ```ini 317 | FORMS = ../form.ui 318 | SOURCES = ../your_plugin.py 319 | TRANSLATIONS = your_plugin_it.ts 320 | ``` 321 | 322 | 你的插件可能有更复杂的结构,并且可能分布在多个文件中。如果是这种情况,请记住,使用`pylupdate5`读取`.pro`文件并更新可翻译字符串,这不会扩展通配符,因此你需要将每个文件显式放在`.pro`文件中。你的项目文件可能看起来像这样: 323 | 324 | ```ini 325 | FORMS = ../ui/about.ui ../ui/feedback.ui \ 326 | ../ui/main_dialog.ui 327 | SOURCES = ../your_plugin.py ../computation.py \ 328 | ../utils.py 329 | ``` 330 | 331 | 此外,`your_plugin.py`文件是*调用* QGIS工具栏中插件的所有菜单和子菜单的文件,你希望将它们全部翻译。 332 | 333 | 最后,使用*TRANSLATIONS*变量,你可以指定所需的翻译语言。 334 | 335 | !!! 警告 warning 336 | 337 | 确保`ts`文件名称为`your_plugin_` + `language` + `.ts`,否则语言将加载失败。使用两个字母的语言缩写(it对应Italian,de对应German,等等) 338 | 339 | ##### 16.1.4.2.2 .ts文件 340 | 341 | 创建`.pro`完成后,你就可以为插件的语言生成`.ts`文件了。 342 | 343 | 打开终端,转到`your_plugin/i18n`目录并输入: 344 | 345 | ```shell 346 | pylupdate5 your_plugin.pro 347 | ``` 348 | 349 | 你应该可以看到`your_plugin_language.ts`文件。 350 | 351 | 用 **Qt Linguist** 打开`.ts`文件并开始翻译。 352 | 353 | ##### 16.1.4.2.3 .qm文件 354 | 355 | 当你完成插件翻译时(如果某些字符串未完成,将使用这些字符串的源语言),你必须创建`.qm` 文件(将被QGIS使用的`.ts`编译文件)。 356 | 357 | 只需在`your_plugin/i18n`目录中打开终端cd 并输入: 358 | 359 | ```shell 360 | lrelease your_plugin.ts 361 | ``` 362 | 363 | 现在,在`i18n`目录中你将看到`your_plugin.qm`文件。 364 | 365 | #### 16.1.4.3 使用MakeFile进行翻译 366 | 367 | 或者,如果你使用Plugin Builder创建了插件,则可以使用makefile从python代码和Qt对话框中提取消息。在Makefile的开头有一个LOCALES变量: 368 | 369 | ```ini 370 | LOCALES = en 371 | ``` 372 | 373 | 将该语言的缩写添加到此变量中,例如匈牙利语: 374 | 375 | ```ini 376 | LOCALES = en hu 377 | ``` 378 | 379 | 现在,你可以通过以下方式从源生成或更新`hu.ts`文件(以及其中的`en.ts`): 380 | 381 | ```shell 382 | make transup 383 | ``` 384 | 385 | 在此之后,你已在LOCALES变量中更新`.ts`了所有语言的文件。使用 **Qt Linguist** 翻译程序消息。完成翻译后,`.qm`可以通过`transcompile`创建: 386 | 387 | ```shell 388 | make transcompile 389 | ``` 390 | 391 | 你必须在你的插件中分发`.ts`文件。 392 | 393 | #### 16.1.4.4 加载插件 394 | 395 | 要查看插件的翻译,只需打开QGIS,更改语言( **设置** ‣ **选项** ‣ **通用** )并重新启动QGIS。 396 | 397 | 你应该看到你的插件使用正确的语言。 398 | 399 | !!! 警告 warning 400 | 401 | 如果你改变了一些东西(新的UI,新的菜单,等等),你必须重新生成`.ts`和`.qm`文件,因此需要再一次执行以上命令。 402 | 403 | ### 16.1.5 提示和技巧 404 | 405 | #### 16.1.5.1 插件重载 406 | 407 | 在开发插件期间,你经常需要在QGIS中重新加载它以进行测试。使用 **Plugin Reloader** 插件非常容易。你可以在[插件管理器](https://docs.qgis.org/testing/en/docs/user_manual/plugins/plugins.html#plugins)中找到它。 408 | 409 | #### 16.1.5.2 使用 qgis-plugin-ci自动打包、发布和翻译 410 | 411 | [qgis-plugin-ci](https://opengisch.github.io/qgis-plugin-ci/)提供了一个命令行界面,用于在你的计算机上执行 QGIS 插件的自动打包和部署,或使用持续集成(如 [GitHub 工作流](https://docs.github.com/en/actions/using-workflows)或 [Gitlab-CI](https://docs.gitlab.com/ee/ci/) 以及 [Transifex](https://www.transifex.com/) 进行翻译)。 412 | 413 | #### 16.1.5.3 访问插件 414 | 415 | 你可以使用python从QGIS中访问所有已安装插件类,这可以方便调试: 416 | 417 | ```python 418 | my_plugin = qgis.utils.plugins['My Plugin'] 419 | ``` 420 | 421 | #### 16.1.5.4 日志消息 422 | 423 | 插件在[日志消息面板中](https://docs.qgis.org/latest/en/docs/user_manual/introduction/general_tools.html#log-message-panel)有自己的选项卡。 424 | 425 | #### 16.1.5.5 分享你的插件 426 | 427 | QGIS在插件仓库中托管了数百个插件。考虑分享你的插件!它将扩展QGIS,人们将能够从你的代码中学习。可以使用插件管理器在QGIS中找到并安装所有托管的插件。 428 | 429 | 信息和要求:[plugins.qgis.org](https://plugins.qgis.org/)。 430 | 431 | ### 16.1.6 提示和技巧 432 | 433 | #### 16.1.6.1 插件重新加载程序 434 | 435 | 在开发插件过程中,你经常需要重新加载它以进行测试。使用**Plugin Reloader**插件非常容易。你可以在[插件管理器](https://docs.qgis.org/testing/en/docs/user_manual/plugins/plugins.html#plugins)中找到它。 436 | 437 | #### 16.1.6.2 使用qgis插件ci自动打包、发布和翻译 438 | 439 | [qgis-plugin-ci](https://opengisch.github.io/qgis-plugin-ci/) 提供了一个命令行界面,可以在你的计算机上或使用持续集成工具(如 [GitHub 工作流](https://docs.github.com/en/actions/using-workflows)或 [Gitlab-CI](https://docs.gitlab.com/ee/ci/))以及 [Transifex](https://www.transifex.com/) 进行自动打包和部署 QGIS 插件。它允许通过 CLI 或在 CI 操作中发布、翻译、发布或生成 XML 插件存储库文件。 440 | 441 | #### 16.1.6.3 访问插件 442 | 443 | 你可以使用Python在QGIS中访问所有已安装插件的类,这对于调试很有用。 444 | 445 | ```python 446 | my_plugin = qgis.utils.plugins['My Plugin'] 447 | ``` 448 | 449 | #### 16.1.6.4 日志信息 450 | 451 | 插件在[日志消息面板](https://docs.qgis.org/testing/en/docs/user_manual/introduction/general_tools.html#log-message-panel)中有自己的选项卡。 452 | 453 | #### 16.1.6.5 资源文件 454 | 455 | 你可以看到在`initGui()`中我们使用了资源文件中的图标(在我们的案例中是`resources.qrc`) 456 | 457 | ```xml 458 | 459 | 460 | icon.png 461 | 462 | 463 | ``` 464 | 465 | 最好使用不会与其他插件或QGIS的任何部分发生冲突的前缀,否则你可能会得到你不想要的资源。现在你只需要生成一个包含资源的Python文件。它是用 **pyrcc5** 命令完成的: 466 | 467 | ```shell 468 | pyrcc5 -o resources.py resources.qrc 469 | ``` 470 | 471 | !!! 提示 472 | 473 | 在Windows环境中,尝试从CMD或Powershell运行pyrcc5可能会导致错误“Windows无法访问指定的设备,路径,或文件[...]”。最简单的解决方案可能是使用osgeo4wshell,但如果你愿意修改PATH环境变量或显式指定可执行文件的路径,你应该可以在`\bin\pyrcc5.exe`找到它 474 | 475 | 就这些……没什么复杂的:) 476 | 477 | 如果你已正确完成所有操作,则应该能够在插件管理器中查找并加载插件,在点击工具栏图标或相应的菜单项时,可以在控制台中查看到消息。 478 | 479 | 在处理真正的插件时,最好将插件写入另一个(工作)目录并创建一个makefile,它将生成UI和资源文件并将插件安装到QGIS安装中。 480 | 481 | 482 | 483 | ## 16.2 代码片段 484 | 485 | 本节以代码片段为例,讲解插件开发 486 | 487 | ### 16.2.1 如何通过快捷键调用方法 488 | 489 | 在`initGui()`中添加: 490 | 491 | ```python 492 | self.key_action = QAction("Test Plugin", self.iface.mainWindow()) 493 | self.iface.registerMainWindowAction(self.key_action, "Ctrl+I") # 操作被Ctrl+I触发 494 | self.iface.addPluginToMenu("&Test plugins", self.key_action) 495 | self.key_action.triggered.connect(self.key_action_triggered) 496 | ``` 497 | 498 | 在`unload()`中添加: 499 | 500 | ```python 501 | self.iface.unregisterMainWindowAction(self.keyAction) 502 | ``` 503 | 504 | 按下`CTRL+I`时调用方法: 505 | 506 | ```python 507 | def key_action_triggered(self): 508 | QMessageBox.information(self.iface.mainWindow(),"Ok", "You pressed Ctrl+I") 509 | ``` 510 | 511 | 还可以允许用户为提供的操作自定义快捷键。这是通过添加以下内容来完成的: 512 | 513 | ```python 514 | # 在 initGui() 方法中 515 | QgsGui.shortcutsManager().registerAction(self.key_action) 516 | 517 | # 在 unload() 方法中 518 | QgsGui.shortcutsManager().unregisterAction(self.key_action) 519 | ``` 520 | 521 | ### 16.2.2 如何重用QGIS图标 522 | 523 | 因为它们是众所周知的,并且向用户传达了明确的信息,所以有时你可能希望在插件中重用QGIS图标,而不是绘制和设置新图标。使用[`getThemeIcon()`](https://qgis.org/pyqgis/master/core/QgsApplication.html#qgis.core.QgsApplication.getThemeIcon)方法。 524 | 525 | 例如,重用 QGIS 代码存储库中可用的 [mActionFileOpen.svg](https://github.com/qgis/QGIS/blob/master/images/themes/default/mActionFileOpen.svg) 图标: 526 | 527 | ``` 528 | # 例如:在初始化GUI时 529 | self.file_open_action = QAction( 530 | QgsApplication.getThemeIcon("/mActionFileOpen.svg"), 531 | self.tr("Select a File..."), 532 | self.iface.mainWindow() 533 | ) 534 | self.iface.addPluginToMenu("MyPlugin", self.file_open_action) 535 | ``` 536 | 537 | [`iconPath()`](https://qgis.org/pyqgis/master/core/QgsApplication.html#qgis.core.QgsApplication.iconPath)是调用 QGIS 图标的另一种方法。有关调用主题图标的示例,请访问[QGIS嵌入式图像—速查表](https://static.geotribu.fr/toc_nav_ignored/qgis_resources_preview_table/)。 538 | 539 | ### 16.2.3 选项对话框中的插件接口 540 | 541 | 你可以在 **设置** ‣ **选项** 中添加一个自定义插件选项标签。这比为你的插件选项添加一个特定的主菜单条目更可取,因为它将所有的QGIS应用程序设置和插件设置保存在一个单一的地方,便于用户发现和导航。 542 | 543 | 下面的代码片段将为插件的设置添加一个新的空白选项卡,为你填充所有选项和你的插件特定设置做好准备。你可以将下面的类拆分成不同的文件。在这个例子中,我们在mainPlugin.py文件中添加了两个类。 544 | 545 | ```python 546 | class MyPluginOptionsFactory(QgsOptionsWidgetFactory): 547 | 548 | def __init__(self): 549 | super().__init__() 550 | 551 | def icon(self): 552 | return QIcon('icons/my_plugin_icon.svg') 553 | 554 | def createWidget(self, parent): 555 | return ConfigOptionsPage(parent) 556 | 557 | 558 | class ConfigOptionsPage(QgsOptionsPageWidget): 559 | 560 | def __init__(self, parent): 561 | super().__init__(parent) 562 | layout = QHBoxLayout() 563 | layout.setContentsMargins(0, 0, 0, 0) 564 | self.setLayout(layout) 565 | ``` 566 | 567 | 最后我们添加导入和修改`__init__`函数: 568 | 569 | ```python 570 | from qgis.gui import QgsOptionsWidgetFactory, QgsOptionsPageWidget 571 | 572 | 573 | class MyPlugin: 574 | def __init__(self, iface): 575 | """构造函数. 576 | 577 | :param iface: 将传递给该类的接口实例它提供了一个钩子,你可以通过它来操作QGIS运行时应用程序。 578 | :type iface: QgsInterface 579 | """ 580 | # 保存引用 581 | self.iface = iface 582 | 583 | 584 | def initGui(self): 585 | self.options_factory = MyPluginOptionsFactory() 586 | self.options_factory.setTitle(self.tr('My Plugin')) 587 | iface.registerOptionsWidgetFactory(self.options_factory) 588 | 589 | def unload(self): 590 | iface.unregisterOptionsWidgetFactory(self.options_factory) 591 | ``` 592 | 593 | !!! 提示 594 | 595 | **将自定义选项卡添加到图层属性对话框** 596 | 597 | 你可以应用类似的逻辑,使用[QgsMapLayerConfigWidgetFactory](https://qgis.org/pyqgis/master/gui/QgsMapLayerConfigWidgetFactory.html#qgis.gui.QgsMapLayerConfigWidgetFactory)和[QgsMapLayerConfigWidget](https://qgis.org/pyqgis/master/gui/QgsMapLayerConfigWidget.html#qgis.gui.QgsMapLayerConfigWidget)类将插件自定义选项添加到图层属性对话框中。 598 | 599 | ### 16.2.4 在图层树中嵌入图层的自定义小组件 600 | 601 | 除了在图层面板中常见的图层符号元素之外,你还可以添加自己的小部件,以便快速访问一些经常与图层一起使用的操作(设置过滤、选择、样式、使用按钮小部件刷新图层、创建基于时间轴的图层或仅在标签中显示额外的图层信息等)。这些所谓的图层树嵌入式小部件通过图层的属性图例选项卡为各个图层提供。 602 | 603 | 以下代码片段在图例中创建一个下拉菜单,显示图层可用的图层样式,允许快速在不同的图层样式之间切换。 604 | 605 | ```python 606 | class LayerStyleComboBox(QComboBox): 607 | def __init__(self, layer): 608 | QComboBox.__init__(self) 609 | self.layer = layer 610 | for style_name in layer.styleManager().styles(): 611 | self.addItem(style_name) 612 | 613 | idx = self.findText(layer.styleManager().currentStyle()) 614 | if idx != -1: 615 | self.setCurrentIndex(idx) 616 | 617 | self.currentIndexChanged.connect(self.on_current_changed) 618 | 619 | def on_current_changed(self, index): 620 | self.layer.styleManager().setCurrentStyle(self.itemText(index)) 621 | 622 | class LayerStyleWidgetProvider(QgsLayerTreeEmbeddedWidgetProvider): 623 | def __init__(self): 624 | QgsLayerTreeEmbeddedWidgetProvider.__init__(self) 625 | 626 | def id(self): 627 | return "style" 628 | 629 | def name(self): 630 | return "Layer style chooser" 631 | 632 | def createWidget(self, layer, widgetIndex): 633 | return LayerStyleComboBox(layer) 634 | 635 | def supportsLayer(self, layer): 636 | return True # any layer is fine 637 | 638 | provider = LayerStyleWidgetProvider() 639 | QgsGui.layerTreeEmbeddedWidgetRegistry().addProvider(provider) 640 | ``` 641 | 642 | 从给定图层的图例属性选项卡中,将“图层样式选择器”从“可用小部件”拖动到“已用小部件”,以在图层树中启用小部件。嵌入式小部件始终显示在其相关图层节点子项的顶部。 643 | 644 | 如果你想从插件中使用小部件,可以按以下方法添加它们: 645 | 646 | ```python 647 | layer = iface.activeLayer() 648 | counter = int(layer.customProperty("embeddedWidgets/count", 0)) 649 | layer.setCustomProperty("embeddedWidgets/count", counter+1) 650 | layer.setCustomProperty("embeddedWidgets/{}/id".format(counter), "style") 651 | view = self.iface.layerTreeView() 652 | view.layerTreeModel().refreshLayerLegend(view.currentLegendNode()) 653 | view.currentNode().setExpanded(True) 654 | ``` 655 | 656 | ## 16.3 编写和调试插件的IDE设置 657 | 658 | **TODO** 659 | 660 | ## 16.4 发布你的插件 661 | 662 | 一旦你的插件准备好了,并且你认为这个插件可能对某些人有帮助,不要犹豫,把它上传到[官方Python插件仓库](https://docs.qgis.org/testing/en/docs/pyqgis_developer_cookbook/plugins/releasing.html#official-pyqgis-repository)。在该页面上,你还可以找到关于如何准备插件以与插件安装程序良好配合的打包指南。或者,如果你想建立自己的插件库,可以创建一个简单的XML文件,列出插件及其元数据。 663 | 664 | 请特别注意一下建议: 665 | 666 | ### 16.4.1 元数据和名称 667 | 668 | - 避免使用与现有插件过于相似的名称 669 | - 如果你的插件与现有的插件有类似的功能,请在 "关于 "一栏中解释其区别,这样用户就会知道使用哪一个,而不需要安装和测试。 670 | - 避免在插件本身的名称中重复使用"plugin"。 671 | - 使用元数据中的描述字段进行单行描述,使用关于字段进行更详细的说明 672 | - 包括一个代码库、一个错误跟踪器和一个主页;这将极大地提高合作的可能性,并且可以通过现有的网络基础设施(GitHub、GitLab、Bitbucket等)非常容易地完成。 673 | - 谨慎选择标签:避免不具参考价值的标签(如vector),最好选择已经被他人使用的标签(见插件网站)。 674 | - 添加一个合适的图标,不要使用默认的图标;参见QGIS界面,了解要使用的风格建议 675 | 676 | ### 16.4.2 代码和帮助 677 | 678 | - 不要把生成的文件(ui_*.py, resources_rc.py, 生成的帮助文件...)和无用的东西(如.gitignore)包括在版本库中 679 | 680 | - 将插件添加到适当的菜单中(Vector, Raster, Web, Database)。 681 | 682 | - 在适当的时候(执行分析的插件),考虑将插件添加为Processing框架的子插件:这将允许用户批量运行它,将它集成到更复杂的工作流中,并将你从设计界面的负担中解放出来 683 | 684 | - 至少包括最基本的文档,如果对测试和理解有用的话,还包括样本数据。 685 | 686 | ### 16.4.3 官方python插件仓库 687 | 688 | 你可以找到官方python插件仓库:https://plugins.qgis.org/。 689 | 690 | 为了使用官方python插件仓库,你必须从[OSGEO web portal](https://www.osgeo.org/community/getting-started-osgeo/osgeo_userid/)获得OSGEO ID。 691 | 692 | 一旦你上传了你的插件,它将得到一个工作人员的批准,你会得到通知。 693 | 694 | #### 16.4.3.1 权限 695 | 696 | 这些规则已经在官方插件库中实现: 697 | 698 | - 每个注册用户都可以添加一个新的插件 699 | 700 | - 员工用户可以批准或不批准所有的插件版本 701 | 702 | - 拥有特殊权限`plugins.can_approve`的用户可以自动批准他们上传的版本 703 | 704 | - 拥有特殊权限`plugins.can_approve`的用户可以批准其他人上传的版本,只要他们在插件所有者的列表中。 705 | 706 | - 一个特定的插件只能由员工用户和插件所有者删除和编辑。 707 | 708 | - 如果一个没有`plugins.can_approve`权限的用户上传了一个新的版本,该插件的版本会自动取消审批。 709 | 710 | #### 16.4.3.2 信任管理 711 | 712 | 工作人员可以通过前端应用程序设置`plugins.can_approve`权限,向选定的插件创建者授予信任。 713 | 714 | 插件详情视图提供了直接链接,以授予对插件创建者或插件所有者的信任。 715 | 716 | #### 16.4.3.3 验证 717 | 718 | 在上传插件时,插件的元数据会自动从压缩包中导入并进行验证。 719 | 720 | 这里有一些验证规则,当你想在官方仓库上传一个插件时,你应该注意: 721 | 722 | 1. 包含插件的主文件夹的名称必须只包含ASCII字符(A-Z和a-z)、数字和下划线(_)和减号(-),而且不能以数字开头。 723 | 2. `metadata.txt`是必需的 724 | 3. 元数据表中列出的所有必需的元数据都必须存在 725 | 4. 元数据`version`字段必须是唯一的 726 | 727 | #### 16.4.3.4 插件结构 728 | 729 | 按照验证规则,你的插件的压缩包(.zip)必须有一个特定的结构,才能作为一个功能性插件进行验证。由于该插件将被解压在用户的plugins文件夹内,它必须在.zip文件内有自己的目录,以不干扰其他插件。必需的文件有:`metadata.txt` 和 `__init__.py`。但如果能有一个README,当然还有一个代表该插件的图标(resources.qrc),那就更好了。以下是一个plugin.zip的例子,它应该是这样的。 730 | 731 | ``` 732 | plugin.zip 733 | pluginfolder/ 734 | |-- i18n 735 | | |-- translation_file_de.ts 736 | |-- img 737 | | |-- icon.png 738 | | `-- iconsource.svg 739 | |-- __init__.py 740 | |-- Makefile 741 | |-- metadata.txt 742 | |-- more_code.py 743 | |-- main_code.py 744 | |-- README 745 | |-- resources.qrc 746 | |-- resources_rc.py 747 | `-- ui_Qt_user_interface_file.ui 748 | ``` 749 | 750 | -------------------------------------------------------------------------------- /docs/20-QGIS服务器和Python.md: -------------------------------------------------------------------------------- 1 | # 20-QGIS服务器和Python 2 | 3 | ## 20.1 介绍 4 | 5 | 要了解有关QGIS服务器的更多信息,请阅读[QGIS服务器指南/手册](https://docs.qgis.org/testing/en/docs/server_manual/index.html#qgis-server-manual)。 6 | 7 | QGIS服务器是三个不同的东西: 8 | 9 | 1. QGIS服务器库:一个为创建OGC网络服务提供API的库 10 | 2. QGIS服务器FCGI:一个FCGI二进制应用程序`qgis_mapserv.fcgi`,与网络服务器一起实现一套OGC服务(WMS、WFS、WCS等)和OGC APIs(WFS3/OAPIF)。 11 | 3. QGIS开发服务器:一个开发服务器二进制应用程序`qgis_mapserver`,实现了一套OGC服务(WMS、WFS、WCS等)和OGC APIs(WFS3/OAPIF)。 12 | 13 | 本章的重点是第一个话题,通过解释QGIS服务器API的用法来说明如何使用Python来扩展、增强或定制服务器行为,或如何使用QGIS服务器API将QGIS服务器嵌入到另一个应用程序。 14 | 15 | 你可以通过一些不同的方式来改变QGIS服务器的行为或扩展其功能,以提供新的定制服务或API,一下是你可能面临的主要情况: 16 | 17 | - 融合 → 从另一个Python应用程序中使用QGIS服务器API 18 | - 独立 → 以独立的WSGI/HTTP服务方式运行QGIS服务器 19 | - 过滤 → 使用过滤插件增强/定制QGIS服务器 20 | - 服务 → 添加一个新的*服务* 21 | - OGC APIs → 添加一个新的*OGC API* 22 | 23 | 嵌入和独立的应用程序需要直接从另一个Python脚本或应用程序中使用QGIS服务器的Python API。其余的选项更适合于当你想在标准的QGIS服务器二进制应用程序(FCGI或开发服务器)中添加自定义功能时:在这种情况下,你需要为服务器应用程序编写一个Python插件,并注册你的自定义过滤器、服务或API。 24 | 25 | ## 20.2 服务器API基础 26 | 27 | 一个典型的QGIS服务器应用程序所涉及的基本类是: 28 | 29 | - [`QgsServer`](https://qgis.org/pyqgis/master/server/QgsServer.html#qgis.server.QgsServer) 服务实例 (通常在整个应用程序的生命周期中只有一个实例) 30 | - [`QgsServerRequest`](https://qgis.org/pyqgis/master/server/QgsServerRequest.html#qgis.server.QgsServerRequest) 请求对象(通常在每次请求时重新创建) 31 | - [`QgsServer.handleRequest(request, response)`](https://qgis.org/pyqgis/master/server/QgsServer.html#qgis.server.QgsServer.handleRequest) 处理请求并响应 32 | 33 | QGIS服务器FCGI或开发服务器的工作流程可以概括为以下几点: 34 | 35 | ``` 36 | initialize the QgsApplication 37 | create the QgsServer 38 | the main server loop waits forever for client requests: 39 | for each incoming request: 40 | create a QgsServerRequest request 41 | create a QgsServerResponse response 42 | call QgsServer.handleRequest(request, response) 43 | filter plugins may be executed 44 | send the output to the client 45 | ``` 46 | 47 | 在[`QgsServer.handleRequest(request, response)`](https://qgis.org/pyqgis/master/server/QgsServer.html#qgis.server.QgsServer.handleRequest)方法中,过滤器插件的回调函数被调用,[`QgsServerRequest`](https://qgis.org/pyqgis/master/server/QgsServerRequest.html#qgis.server.QgsServerRequest)和[`QgsServerResponse`](https://qgis.org/pyqgis/master/server/QgsServerResponse.html#qgis.server.QgsServerResponse)通过[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)类被提供给插件。 48 | 49 | !!! 警告 warning 50 | 51 | QGIS服务器类不是线程安全的,在构建基于QGIS服务器API的可扩展应用程序时,你应该始终使用多进程模型或容器。 52 | 53 | ## 20.3 独立或嵌入 54 | 55 | 对于独立的服务器应用或嵌入,你需要直接使用上述的服务器类,将它们包装成一个Web服务器实现,管理所有与客户端的HTTP协议交互。 56 | 57 | 这里有一个关于QGIS服务器API应用的最小例子(没有HTTP部分): 58 | 59 | ```python 60 | from qgis.core import QgsApplication 61 | from qgis.server import * 62 | app = QgsApplication([], False) 63 | 64 | # 创建服务器实例,它可能是一个单一的实例,在多个请求中重复使用 65 | server = QgsServer() 66 | 67 | # 通过指定完整的URL和一个可选的主体来创建请求(例如POST请求) 68 | request = QgsBufferServerRequest( 69 | 'http://localhost:8081/?MAP=/qgis-server/projects/helloworld.qgs' + 70 | '&SERVICE=WMS&REQUEST=GetCapabilities') 71 | 72 | # 创建响应对象 73 | response = QgsBufferServerResponse() 74 | 75 | # 处理请求 76 | server.handleRequest(request, response) 77 | 78 | print(response.headers()) 79 | print(response.body().data().decode('utf8')) 80 | 81 | app.exitQgis() 82 | ``` 83 | 84 | 这里有一个完整的独立应用实例,它是为QGIS源代码库的持续集成测试而开发的,它展示了一系列不同的插件过滤器和认证方案(不意味着可用于生产环境,因为它们只是为测试目的而开发的,但对于学习来说仍然很有趣):https://github.com/qgis/QGIS/blob/master/tests/src/python/qgis_wrapped_server.py 85 | 86 | ## 20.4 服务器插件 87 | 88 | 服务器python插件在QGIS服务器应用程序启动时被加载一次,可用于注册过滤器、服务或API。 89 | 90 | 服务器插件的结构与桌面版的插件非常相似,一个[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)对象被提供给插件,插件可以通过使用服务器接口暴露的方法将一个或多个自定义过滤器、服务或API注册到相应的注册表。 91 | 92 | ### 20.4.1 服务器过滤插件 93 | 94 | 过滤器有三种不同的类型,它们可以通过子类化下面的一个类并调用[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)的相应方法来实例化。 95 | 96 | | 过滤器类型 | 基类 | QgsServerInterface 注册 | 97 | | -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 98 | | I/O | [`QgsServerFilter`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter) | [`registerFilter()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.registerFilter) | 99 | | Access Control | [`QgsAccessControlFilter`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter) | [`registerAccessControl()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.registerAccessControl) | 100 | | Cache | [`QgsServerCacheFilter`](https://qgis.org/pyqgis/master/server/QgsServerCacheFilter.html#qgis.server.QgsServerCacheFilter) | [`registerServerCache()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.registerServerCache) | 101 | 102 | #### 20.4.1.1 I/O过滤器 103 | 104 | I/O过滤器可以修改核心服务(WMS、WFS等)的服务器输入和输出(请求和响应),允许对服务工作流进行任何形式的操作。例如,可以限制对选定图层的访问,向XML响应注入XSL样式表,向生成的WMS图像添加水印等等。 105 | 106 | 从这一点来看,你可能会发现快速浏览一下[服务器插件的API文档](https://qgis.org/pyqgis/master/server)很有用。 107 | 108 | 每一个插件应该至少实现以下三个回调函数: 109 | 110 | - [`onRequestReady()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onRequestReady) 111 | - [`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete) 112 | - [`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse) 113 | 114 | 所有的过滤器都可以访问请求/响应对象([`QgsRequestHandler`](https://qgis.org/pyqgis/master/server/QgsRequestHandler.html#qgis.server.QgsRequestHandler)),并且可以操作它的所有属性(输入/输出)和引发异常(同时以一种相当特别的方式,我们将在下面看到)。 115 | 116 | 所有这些方法都返回一个布尔值,指示调用是否应传播到后续过滤器。如果其中一个方法返回False,则传播链停止,否则调用将传播到下一个过滤器。 117 | 118 | 下面是显示服务器如何处理一个典型请求以及何时调用过滤器回调函数的伪代码。 119 | 120 | ``` 121 | for each incoming request: 122 | create GET/POST request handler 123 | pass request to an instance of QgsServerInterface 124 | call onRequestReady filters 125 | 126 | if there is not a response: 127 | if SERVICE is WMS/WFS/WCS: 128 | create WMS/WFS/WCS service 129 | call service’s executeRequest 130 | possibly call onSendResponse for each chunk of bytes 131 | sent to the client by a streaming services (WFS) 132 | call onResponseComplete 133 | request handler sends the response to the client 134 | ``` 135 | 136 | 下面几个段落详细描述了可用的回调函数。 137 | 138 | ##### 20.4.1.1.1 请求就绪 139 | 140 | 当请求准备就绪时,将被调用:传入的URL和数据已经被解析,在进入核心服务(WMS,WFS等)开关之前,这是你可以操作输入和执行动作的地方: 141 | 142 | - 认证授权 143 | - 重定向 144 | - 添加删除某些参数 (例如类型名称) 145 | - 抛出异常 146 | 147 | 你甚至可以通过改变 **SERVICE** 参数来完全替代一个核心服务,从而完全绕过核心服务(不过这并没有什么意义)。 148 | 149 | ##### 20.4.1.1.2 发送响应 150 | 151 | 无论何时从响应缓冲区刷新任何部分输出,都会调用该函数(例如 **FCGI** 标准输出被使用),并从缓冲区刷新到客户端。当大量内容被流式传输(比如WFS GetFeature)时,就会发生这种情况。在这种情况下,[`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse)可能会被多次调用。 152 | 153 | 请注意,如果响应没有流式传输,则根本不会调用[`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse)。 154 | 155 | 在所有情况下,调用[`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete)后,最后一个(或唯一的)块将被发送到客户端。 156 | 157 | 返回`False`将防止向客户端刷新数据。当插件希望从响应中收集所有块,并在[`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete)中检查或更改响应时,这是可取的。 158 | 159 | ##### 20.4.1.1.3 响应完成 160 | 161 | 当核心服务(如果被击中的话)完成它们的过程,并且请求准备好被发送到客户端时,将被调用一次。如上所述,通常在[`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse)之前调用,除了流媒体服务(或其他插件过滤器)可能在之前调用[`sendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.sendResponse)。 162 | 163 | [`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete)是提供新服务实现(WPS或自定义服务)和对来自核心服务的输出进行直接操作的理想场所(例如,在WMS图像上添加水印)。 164 | 165 | 请注意,返回`False`将阻止下一个插件执行[`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete),但在任何情况下,都会阻止将响应发送到客户端。 166 | 167 | #### 20.4.1.2 从插件引发异常 168 | 169 | 在这个问题上还有一些工作要做:目前的实现可以通过将[`QgsRequestHandler`](https://qgis.org/pyqgis/master/server/QgsRequestHandler.html#qgis.server.QgsRequestHandler)属性设置为QgsMapServiceException的一个实例来区分已处理和未处理的异常,这样,主要的C++代码可以捕获已处理的Python异常而忽略未处理的异常(或者更好的是:记录日志)。 170 | 171 | 这种方法基本上是可行的,但它不是很 "pythonic":一个更好的方法是在python代码中引发异常,并看到它们进入到C++循环中被处理。 172 | 173 | #### 20.4.1.3 编写一个服务器插件 174 | 175 | 服务器插件是一个标准的 QGIS Python 插件,如[16-开发Python插件](16-开发Python插件.md)中所述,它只是提供了一个额外的(或替代的)接口:典型的 QGIS 桌面插件通过 [`QgisInterface `](https://qgis.org/pyqgis/master/gui/QgisInterface.html#qgis.gui.QgisInterface)实例访问 QGIS 应用程序,而服务器插件只有在 QGIS Server 应用程序上下文中执行时才能访问。 176 | 177 | 为了让QGIS Server知道一个插件有一个服务器接口,需要一个特殊的元数据条目(在`metadata.txt`中)。 178 | 179 | ```ini 180 | server=True 181 | ``` 182 | 183 | !!! 重要的 important 184 | 185 | 只有设置了`server=True`元数据的插件才能被QGIS Server加载和执行。 186 | 187 | 这里讨论的 [qgis3-server-vagrant](https://github.com/elpaso/qgis3-server-vagrant/tree/master/resources/web/plugins) 示例插件(以及更多插件)可在 Github 上找到,一些服务器插件也发布在官方的 [QGIS 插件仓库](https://plugins.qgis.org/plugins/server)中。 188 | 189 | #### 20.4.1.4 插件文件 190 | 191 | 下面是我们的示例服务器插件的目录结构。 192 | 193 | ``` 194 | PYTHON_PLUGINS_PATH/ 195 | HelloServer/ 196 | __init__.py --> *required* 197 | HelloServer.py --> *required* 198 | metadata.txt --> *required* 199 | ``` 200 | 201 | ##### 20.4.1.4.1 _\_init\_\_.py 202 | 203 | 这个文件是Python的导入系统所要求的。此外,QGIS Server要求该文件包含一个`serverClassFactory()`函数,当服务器启动时,插件被加载到QGIS Server中时,该函数将被调用。它接收对[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)实例的引用,并必须返回你的插件类的实例。以下是插件示例 `__init__.py `的样子: 204 | 205 | ```python 206 | def serverClassFactory(serverIface): 207 | from .HelloServer import HelloServerServer 208 | return HelloServerServer(serverIface) 209 | ``` 210 | 211 | ##### 20.4.1.4.2 HelloServer.py 212 | 213 | 这就是魔法发生的地方,这就是魔法的模样。(例如:`HelloServer.py`) 214 | 215 | 一个服务器插件通常由一个或多个回调函数组成,被打包到[`QgsServerFilter`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter)的实例中。 216 | 217 | 每个[`QgsServerFilter`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter)都实现了一个或多个以下的回调函数: 218 | 219 | - [`onRequestReady()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onRequestReady) 220 | - [`onResponseComplete()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onResponseComplete) 221 | - [`onSendResponse()`](https://qgis.org/pyqgis/master/server/QgsServerFilter.html#qgis.server.QgsServerFilter.onSendResponse) 222 | 223 | 下面的例子实现了一个最小的过滤器,当 **SERVICE** 参数等于 " **HELLO** "时,打印出*HelloServer*! 224 | 225 | ```python 226 | class HelloFilter(QgsServerFilter): 227 | 228 | def __init__(self, serverIface): 229 | super().__init__(serverIface) 230 | 231 | def onRequestReady(self) -> bool: 232 | QgsMessageLog.logMessage("HelloFilter.onRequestReady") 233 | return True 234 | 235 | def onSendResponse(self) -> bool: 236 | QgsMessageLog.logMessage("HelloFilter.onSendResponse") 237 | return True 238 | 239 | def onResponseComplete(self) -> bool: 240 | QgsMessageLog.logMessage("HelloFilter.onResponseComplete") 241 | request = self.serverInterface().requestHandler() 242 | params = request.parameterMap() 243 | if params.get('SERVICE', '').upper() == 'HELLO': 244 | request.clear() 245 | request.setResponseHeader('Content-type', 'text/plain') 246 | # 注意内容类型是"bytes" 247 | request.appendBody(b'HelloServer!') 248 | return True 249 | ``` 250 | 251 | 过滤器必须被注册到 **serverIface** 中,如下例所示: 252 | 253 | ```python 254 | class HelloServerServer: 255 | def __init__(self, serverIface): 256 | serverIface.registerFilter(HelloFilter(serverIface), 100) 257 | ``` 258 | 259 | [`registerFilter()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.registerFilter)的第二个参数设置了一个优先级,定义了同名回调函数的顺序(优先级低的回调先被调用)。 260 | 261 | 通过使用这三个回调函数,插件可以以许多不同的方式操纵服务器的输入输出。在每个时刻,插件实例都可以通过[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)访问[`QgsRequestHandler`](https://qgis.org/pyqgis/master/server/QgsRequestHandler.html#qgis.server.QgsRequestHandler)。[`QgsRequestHandler`](https://qgis.org/pyqgis/master/server/QgsRequestHandler.html#qgis.server.QgsRequestHandler)类有很多方法,可以用来在进入服务器的核心处理之前(通过使用`requestReady()`)或在请求被核心服务处理之后(通过使用`sendResponse()`)改变输入参数。 262 | 263 | 下面的例子涵盖了一些常见的使用案例。 264 | 265 | ##### 20.4.1.4.3 修改输入 266 | 267 | 示例插件包含一个改变来自查询字符串的输入参数的测试例子,在这个例子中,一个新的参数被注入到(已经解析过的)`parameterMap`中,然后这个参数被核心服务(WMS等)看到,在核心服务处理结束时,我们检查这个参数是否仍然存在: 268 | 269 | ```python 270 | class ParamsFilter(QgsServerFilter): 271 | 272 | def __init__(self, serverIface): 273 | super(ParamsFilter, self).__init__(serverIface) 274 | 275 | def onRequestReady(self) -> bool: 276 | request = self.serverInterface().requestHandler() 277 | params = request.parameterMap( ) 278 | request.setParameter('TEST_NEW_PARAM', 'ParamsFilter') 279 | return True 280 | 281 | def onResponseComplete(self) -> bool: 282 | request = self.serverInterface().requestHandler() 283 | params = request.parameterMap( ) 284 | if params.get('TEST_NEW_PARAM') == 'ParamsFilter': 285 | QgsMessageLog.logMessage("SUCCESS - ParamsFilter.onResponseComplete") 286 | else: 287 | QgsMessageLog.logMessage("FAIL - ParamsFilter.onResponseComplete") 288 | return True 289 | ``` 290 | 291 | 这是日志文件中内容的摘录: 292 | 293 | ```text hl_lines="6" 294 | src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloServerServer - loading filter ParamsFilter 295 | src/core/qgsmessagelog.cpp: 45: (logMessage) [1ms] 2014-12-12T12:39:29 Server[0] Server plugin HelloServer loaded! 296 | src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 Server[0] Server python plugins loaded 297 | src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO into the parameter map 298 | src/mapserver/qgsserverfilter.cpp: 42: (onRequestReady) [0ms] QgsServerFilter plugin default onRequestReady called 299 | src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.onResponseComplete 300 | ``` 301 | 302 | 在突出显示的一行,"SUCCESS "字符串表示该插件通过了测试。 303 | 304 | 同样的技术可以被利用来代替核心服务:例如,你可以跳过 **WFS SERVICE** 请求或任何其他核心请求,只需将 **SERVICE** 参数改为不同的参数,核心服务就会被跳过。然后,你可以将你的自定义结果注入到输出中,并将其发送给客户端(这将在下面解释)。 305 | 306 | !!! 提示 infomation 307 | 308 | 如果你真的想实现一个自定义的服务,建议将[`QgsService`](https://qgis.org/pyqgis/master/server/QgsService.html#qgis.server.QgsService)子类化,并通过调用其[`registerService(service)`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.serviceRegistry)方法在[`registerFilter()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.serviceRegistry)方法上注册你的服务。 309 | 310 | ##### 20.4.1.4.4 修改或替换输出 311 | 312 | 水印过滤器的例子显示了如何用一个新的图像替换WMS的输出,该图像是通过在WMS核心服务生成的WMS图像上添加一个水印图像而获得: 313 | 314 | ```python 315 | from qgis.server import * 316 | from qgis.PyQt.QtCore import * 317 | from qgis.PyQt.QtGui import * 318 | 319 | class WatermarkFilter(QgsServerFilter): 320 | 321 | def __init__(self, serverIface): 322 | super().__init__(serverIface) 323 | 324 | def responseComplete(self): 325 | request = self.serverInterface().requestHandler() 326 | params = request.parameterMap( ) 327 | # 一些检查 328 | if (params.get('SERVICE').upper() == 'WMS' \ 329 | and params.get('REQUEST').upper() == 'GETMAP' \ 330 | and not request.exceptionRaised() ): 331 | QgsMessageLog.logMessage("WatermarkFilter.responseComplete: image ready %s" % request.parameter("FORMAT")) 332 | # 获取图像 333 | img = QImage() 334 | img.loadFromData(request.body()) 335 | # 添加水印 336 | watermark = QImage(os.path.join(os.path.dirname(__file__), 'media/watermark.png')) 337 | p = QPainter(img) 338 | p.drawImage(QRect( 20, 20, 40, 40), watermark) 339 | p.end() 340 | ba = QByteArray() 341 | buffer = QBuffer(ba) 342 | buffer.open(QIODevice.WriteOnly) 343 | img.save(buffer, "PNG" if "png" in request.parameter("FORMAT") else "JPG") 344 | # 设置数据体 345 | request.clearBody() 346 | request.appendBody(ba) 347 | ``` 348 | 349 | 在这个例子中,**SERVICE** 参数值被检查,如果传入的请求是一个 **WMS GETMAP** ,并且没有被先前执行的插件或核心服务(在这个例子中是WMS)设置过异常,那么WMS生成的图像就会从输出缓冲区中被检索出来,并且添加水印图像。最后一步是清除输出缓冲区,用新生成的图像替换它。请注意,在现实世界中,我们还应该检查所要求的图像类型,而不是只支持PNG或JPG。 350 | 351 | #### 20.4.1.5 访问控制过滤器 352 | 353 | 访问控制过滤器为开发者提供了对哪些层、要素和属性可以被访问的细粒度控制,以下回调函数可以在访问控制过滤器中实现: 354 | 355 | - [`layerFilterExpression(layer)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.layerFilterExpression) 356 | - [`layerFilterSubsetString(layer)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.layerFilterSubsetString) 357 | - [`layerPermissions(layer)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.layerPermissions) 358 | - [`authorizedLayerAttributes(layer, attributes)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.authorizedLayerAttributes) 359 | - [`allowToEdit(layer, feature)`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.allowToEdit) 360 | - [`cacheKey()`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.cacheKey) 361 | 362 | ##### 20.4.1.5.1. 插件文件 363 | 364 | 下面是我们的示例服务器插件的目录结构: 365 | 366 | ``` 367 | PYTHON_PLUGINS_PATH/ 368 | MyAccessControl/ 369 | __init__.py --> *required* 370 | AccessControl.py --> *required* 371 | metadata.txt --> *required* 372 | ``` 373 | 374 | ##### 20.4.1.5.2 _\_init\_\_.py 375 | 376 | 这个文件是Python的导入系统所要求的。对于所有的QGIS服务器插件来说,这个文件包含一个`serverClassFactory()`函数,当插件在启动时被加载到QGIS服务器中时,它将被调用。它接收一个对[`QgsServerInterface`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface)实例的引用,并必须返回一个你的插件类的实例。以下是插件实例 `__init__.py`的样子: 377 | 378 | ```python 379 | def serverClassFactory(serverIface): 380 | from MyAccessControl.AccessControl import AccessControlServer 381 | return AccessControlServer(serverIface) 382 | ``` 383 | 384 | ##### 20.4.1.5.3. AccessControl.py 385 | 386 | ```python 387 | class AccessControlFilter(QgsAccessControlFilter): 388 | 389 | def __init__(self, server_iface): 390 | super().__init__(server_iface) 391 | 392 | def layerFilterExpression(self, layer): 393 | """ 返回一个额外的表达式过滤器 """ 394 | return super().layerFilterExpression(layer) 395 | 396 | def layerFilterSubsetString(self, layer): 397 | """ 返回一个额外的子集字符串(通常是SQL)过滤器 """ 398 | return super().layerFilterSubsetString(layer) 399 | 400 | def layerPermissions(self, layer): 401 | """ 返回该层的权限 """ 402 | return super().layerPermissions(layer) 403 | 404 | def authorizedLayerAttributes(self, layer, attributes): 405 | """ 返回授权的图层属性 """ 406 | return super().authorizedLayerAttributes(layer, attributes) 407 | 408 | def allowToEdit(self, layer, feature): 409 | """ 我们是否被授权修改以下几何图形 """ 410 | return super().allowToEdit(layer, feature) 411 | 412 | def cacheKey(self): 413 | return super().cacheKey() 414 | 415 | class AccessControlServer: 416 | 417 | def __init__(self, serverIface): 418 | """ 注册访问控制过滤器 """ 419 | serverIface.registerAccessControl(AccessControlFilter(serverIface), 100) 420 | ``` 421 | 422 | 这个例子为每个人提供了一个完整的访问权限。 423 | 424 | 插件的作用是知道谁在登录。 425 | 426 | 在所有这些方法中,我们都有一个图层的参数,以便能够定制每个图层的限制。 427 | 428 | ##### 20.4.1.5.4. layerFilterExpression 429 | 430 | 用于添加一个表达式来限制结果。 431 | 432 | 例如:限制要素的属性`role`等于`user`。 433 | 434 | ```python 435 | def layerFilterExpression(self, layer): 436 | return "$role = 'user'" 437 | ``` 438 | 439 | ##### 20.4.1.5.5. layerFilterSubsetString 440 | 441 | 与前者相同,但使用`SubsetString`(在数据库中执行)。 442 | 443 | ```python 444 | def layerFilterSubsetString(self, layer): 445 | return "role = 'user'" 446 | ``` 447 | 448 | 限制要素的属性`role`等于`“user”`。 449 | 450 | ##### 20.4.1.5.6. layerPermissions 451 | 452 | 限制访问图层。 453 | 454 | 返回一个[`LayerPermissions()`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.layerPermissions)对象,它有以下属性: 455 | 456 | - [`canRead`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.LayerPermissions.canRead)可以在`GetCapabilities`中看到它并有读取权限。 457 | - [`canInsert`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.LayerPermissions.canInsert)能够插入一个新的要素。 458 | - [`canUpdate`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.LayerPermissions.canUpdate)能够更新一个要素。 459 | - [`canDelete`](https://qgis.org/pyqgis/master/server/QgsAccessControlFilter.html#qgis.server.QgsAccessControlFilter.LayerPermissions.canDelete)能够删除一个要素。 460 | 461 | 例如: 462 | 463 | ```python 464 | def layerPermissions(self, layer): 465 | rights = QgsAccessControlFilter.LayerPermissions() 466 | rights.canRead = True 467 | rights.canInsert = rights.canUpdate = rights.canDelete = False 468 | return rights 469 | ``` 470 | 471 | 限制每一个人只读访问。 472 | 473 | ##### 20.4.1.5.7. authorizedLayerAttributes 474 | 475 | 用于限制一个特定的属性子集的可见性。 476 | 477 | 参数 attribute 返回当前的可见属性集。 478 | 479 | 例如: 480 | 481 | ```python 482 | def authorizedLayerAttributes(self, layer, attributes): 483 | return [a for a in attributes if a != "role"] 484 | ``` 485 | 486 | 隐藏‘role’属性。 487 | 488 | ##### 20.4.1.5.8. allowToEdit 489 | 490 | 这被用来限制对一个属性子集的编辑。 491 | 492 | 它在`WFS-Transaction`协议中使用。 493 | 494 | 例如: 495 | 496 | ```python 497 | def allowToEdit(self, layer, feature): 498 | return feature.attribute('role') == 'user' 499 | ``` 500 | 501 | 只能够编辑具有‘role’属性的要素。 502 | 503 | ##### 20.4.1.5.9. cacheKey 504 | 505 | QGIS服务器会对能力进行缓存,如果要对每个角色进行缓存,你可以在此方法中返回角色。或者返回`None`以完全禁用缓存。 506 | 507 | ### 20.4.2 自定义服务 508 | 509 | 在QGIS服务器中,WMS、WFS和WCS等核心服务是作为[`QgsService`](https://qgis.org/pyqgis/master/server/QgsService.html#qgis.server.QgsService)的子类实现的。 510 | 511 | 为了实现一个新的服务,当查询字符串参数`SERVICE`与服务名称相匹配时将被执行,你可以实现你自己的[`QgsService`](https://qgis.org/pyqgis/master/server/QgsService.html#qgis.server.QgsService),并通过调用[`registerService(service)`](https://qgis.org/pyqgis/master/server/QgsServiceRegistry.html#qgis.server.QgsServiceRegistry.registerService)在[`serviceRegistry()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.serviceRegistry)上注册你的服务。 512 | 513 | 下面是一个名为CUSTOM的自定义服务的例子: 514 | 515 | ```python 516 | from qgis.server import QgsService 517 | from qgis.core import QgsMessageLog 518 | 519 | class CustomServiceService(QgsService): 520 | 521 | def __init__(self): 522 | QgsService.__init__(self) 523 | 524 | def name(self): 525 | return "CUSTOM" 526 | 527 | def version(self): 528 | return "1.0.0" 529 | 530 | def executeRequest(self, request, response, project): 531 | response.setStatusCode(200) 532 | QgsMessageLog.logMessage('Custom service executeRequest') 533 | response.write("Custom service executeRequest") 534 | 535 | 536 | class CustomService(): 537 | 538 | def __init__(self, serverIface): 539 | serverIface.serviceRegistry().registerService(CustomServiceService()) 540 | ``` 541 | 542 | ### 20.4.3. 自定义APIs 543 | 544 | 在QGIS Server中,诸如OAPIF(又称WFS3)等核心OGC APIs被实现为[`QgsServerOgcApiHandler`](https://qgis.org/pyqgis/master/server/QgsServerOgcApiHandler.html#qgis.server.QgsServerOgcApiHandler)子类的集合,这些子类被注册到[`QgsServerOgcApi`](https://qgis.org/pyqgis/master/server/QgsServerOgcApi.html#qgis.server.QgsServerOgcApi)(或其父类[`QgsServerApi`](https://qgis.org/pyqgis/master/server/QgsServerApi.html#qgis.server.QgsServerApi))的实例中。 545 | 546 | 要实现一个新的API,当url路径与某个URL相匹配时就会被执行,你可以实现你自己的[`QgsServerOgcApiHandler`](https://qgis.org/pyqgis/master/server/QgsServerOgcApiHandler.html#qgis.server.QgsServerOgcApiHandler)实例,将它们添加到[`QgsServerOgcApi`](https://qgis.org/pyqgis/master/server/QgsServerOgcApi.html#qgis.server.QgsServerOgcApi)中,并通过调用其[`registerApi(api)`](https://qgis.org/pyqgis/master/server/QgsServiceRegistry.html#qgis.server.QgsServiceRegistry.registerApi)在[`serviceRegistry()`](https://qgis.org/pyqgis/master/server/QgsServerInterface.html#qgis.server.QgsServerInterface.serviceRegistry)上注册该API。 547 | 548 | 下面是一个自定义API的例子,当URL包含`/customapi`时将被执行: 549 | 550 | ```python 551 | import json 552 | import os 553 | 554 | from qgis.PyQt.QtCore import QBuffer, QIODevice, QTextStream, QRegularExpression 555 | from qgis.server import ( 556 | QgsServiceRegistry, 557 | QgsService, 558 | QgsServerFilter, 559 | QgsServerOgcApi, 560 | QgsServerQueryStringParameter, 561 | QgsServerOgcApiHandler, 562 | ) 563 | 564 | from qgis.core import ( 565 | QgsMessageLog, 566 | QgsJsonExporter, 567 | QgsCircle, 568 | QgsFeature, 569 | QgsPoint, 570 | QgsGeometry, 571 | ) 572 | 573 | 574 | class CustomApiHandler(QgsServerOgcApiHandler): 575 | 576 | def __init__(self): 577 | super(CustomApiHandler, self).__init__() 578 | self.setContentTypes([QgsServerOgcApi.HTML, QgsServerOgcApi.JSON]) 579 | 580 | def path(self): 581 | return QRegularExpression("/customapi") 582 | 583 | def operationId(self): 584 | return "CustomApiXYCircle" 585 | 586 | def summary(self): 587 | return "Creates a circle around a point" 588 | 589 | def description(self): 590 | return "Creates a circle around a point" 591 | 592 | def linkTitle(self): 593 | return "Custom Api XY Circle" 594 | 595 | def linkType(self): 596 | return QgsServerOgcApi.data 597 | 598 | def handleRequest(self, context): 599 | """Simple Circle""" 600 | 601 | values = self.values(context) 602 | x = values['x'] 603 | y = values['y'] 604 | r = values['r'] 605 | f = QgsFeature() 606 | f.setAttributes([x, y, r]) 607 | f.setGeometry(QgsCircle(QgsPoint(x, y), r).toCircularString()) 608 | exporter = QgsJsonExporter() 609 | self.write(json.loads(exporter.exportFeature(f)), context) 610 | 611 | def templatePath(self, context): 612 | # 模板路径用于提供HTML内容 613 | return os.path.join(os.path.dirname(__file__), 'circle.html') 614 | 615 | def parameters(self, context): 616 | return [QgsServerQueryStringParameter('x', True, QgsServerQueryStringParameter.Type.Double, 'X coordinate'), 617 | QgsServerQueryStringParameter( 618 | 'y', True, QgsServerQueryStringParameter.Type.Double, 'Y coordinate'), 619 | QgsServerQueryStringParameter('r', True, QgsServerQueryStringParameter.Type.Double, 'radius')] 620 | 621 | 622 | class CustomApi(): 623 | 624 | def __init__(self, serverIface): 625 | api = QgsServerOgcApi(serverIface, '/customapi', 626 | 'custom api', 'a custom api', '1.1') 627 | handler = CustomApiHandler() 628 | api.registerHandler(handler) 629 | serverIface.serviceRegistry().registerApi(api) 630 | ``` 631 | 632 | --------------------------------------------------------------------------------