0%

从零开始的文档网站搭建——以Tinker Docs为例

无论是要搭建个人博客还是团队知识库,一个文档网站都是必不可少的。一个优秀的文档网站能让我们方便快捷地上传文档资料,清晰地组织文档架构,同时支持多语言、链接跳转、界面个性化等多种多样的功能。本文将以 Tinker Docs 的搭建过程为例,介绍如何基于 DocusaurusGithub Pages 完成一个基本的文档网站的部署。

一、静态网站生成器

想要搭建文档网站,首先的问题就是——我要如何方便快捷地发布我的文档?毕竟,文档网站的核心是文档的内容产出,并不需要多么炫酷的前端界面。倘若对于我的每一份文档,我都需要为它写一套前端代码,来显示和组织其中的各个元素,这显然有点太过折磨人了。

为此,静态网站生成器应运而生。通过静态网站生成器,我们可以轻松地通过 Markdown 或其他轻量级标记语言来编写文档并发布至网站中,从而大大减小了文档网站的发布工作量。

想要理解静态网站生成器,我们首先需要知道什么是静态网站。静态网站是由静态网页组成的网站,这些网页是预先编写好的,存储在服务器上的文件,其中的每个文件都对应一个网页。当用户请求一个网页时,服务器会将对应的文件发送给用户的浏览器,将这一页面呈现出来。静态网页的内容在服务器上是固定的,不会因为用户的请求而改变。与静态网站相对的就是动态网站。动态网站的内容会根据用户的请求实时生成。当用户请求一个网页时,服务器会运行一些脚本,可能会查询数据库,然后生成对应的网页发送给用户。动态网站的内容会根据用户的请求、用户的输入、当前的时间等因素进行变化。

文档网站通常都属于静态网站。文档网站的主要功能是静态文档的呈现,对于动态交互等功能需求较少。这一特点使得静态网站生成器能很方便地将我们的文本文件转化为静态网站上的页面。

静态网站生成器的工作流程通常如下:

  1. 用户在文本文件中编写内容,可能使用 Markdown 或其他轻量级标记语言。
  2. 用户可能会使用一些模板,这些模板定义了网页的布局和样式。
  3. 用户运行静态网站生成器,生成器读取文本文件和模板,生成静态 HTML 文件。
  4. 生成的 HTML 文件可以直接部署到任何能够提供静态内容的 web 服务器上。

二、Docusaurus 简介

概览

市面上常用的静态网页生成器有很多,包括 Hexo(常用于搭建个人博客)、MkDocs(常用于搭建技术文档)、VuePress(由 Vue.js 支持的静态网站生成工具)等。Tinker Docs 采用了 Docusaurus 作为静态网页生成器。

Docusaurus是一个由Facebook开发的开源静态网页生成器,支持编写技术文档、博客等多种功能。Docusaurus 对 React 和 Markdown 均有着较好的支持,这也使得它在 Markdown 的基础上能提供相当丰富的显示功能。

以下,我们将介绍 Docusaurus 的基本使用流程。

项目创建

我们可以使用npx来创建一个 Docusaurus 项目。(要求 Node.js 版本为 18.0 或更高版本,可以通过运行 node -v 进行检查)。使用npx的初始化命令(推荐使用):

1
npx create-docusaurus@latest <website_name> classic

也可以选择其它包管理器进行初始化。

使用npm的初始化命令:

1
npm init docusaurus

使用yarn的初始化命令:

1
yarn create docusaurus

添加页面

Docusaurus 的典型项目结构如下(以项目my-website为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
my-website
├── blog
│ ├── 2019-05-28-hola.md
│ ├── 2019-05-29-hello-world.md
│ └── 2020-05-30-welcome.md
├── docs
│ ├── doc1.md
│ ├── doc2.md
│ ├── doc3.md
│ ├── subfold
│ │ ├── _category.json
│ │ └── subdoc.md
│ └── mdx.md
├── src
│ ├── css
│ │ └── custom.css
│ └── pages
│ ├── styles.module.css
│ └── index.js
├── static
│ └── img
├── docusaurus.config.js
├── package.json
├── README.md
├── sidebars.js
└── yarn.lock
  • /blog/ - 包含博客的 Markdown 文件。
  • /docs/ - 包含文档的 Markdown 文件。可通过自定义 sidebars.js 改变/docs/中的文档在侧边栏的顺序。
  • /src/ - 非文档文件,例如页面和自定义 React 组件。不必严格将非文档文件放在这里,但是将它们放在集中目录下可以更方便进行处理。
    • /src/pages - 此目录中的任何 JSX/TSX/MDX 文件都将转换为网站页面。
  • /static/ - 静态目录。这里的任何内容都将被复制到最终 build 目录的根目录中。
  • /docusaurus.config.js - 包含站点配置的配置文件。
  • /package.json - Docusaurus 网站是一个 React 应用程序,您可以在其中安装和使用需要的 npm 软件包。
  • /sidebars.js - 用来指定侧边栏中文档的顺序

如果要添加文档,只需向/blog//docs/中添加相应的 Markdown 或 React 文件,即可在网站中增添相应的页面。

如需对文档进行分类,只需在/docs/文件夹下新建子文件夹(如项目结构中的/subfold/),便可在网站中起到相应的文档归类作用。在子文件夹中可添加文件_category_.json,用以起到对整个子文件夹页面的组织、介绍作用。

Tinker Docs 中的/docs/Hardware/_category_.json文件:

1
2
3
4
5
6
7
8
{
"label": "硬件",
"position": 2,
"link": {
"type": "generated-index",
"description": "电路、机械结构设计相关知识"
}
}

对应的子文件夹介绍页面:硬件 | Tinker Docs

本地预览

当完成文档编写后,在正式向网站发布之前,我们可以通过运行本地开发服务器来预览自己的网站,并实时进行更改。

我们可通过如下命令进行网站预览:

使用npm的命令:

1
2
cd <website_name>
npm run start

使用yarn的命令:

1
2
cd <website_name>
yarn run start

这样,一个浏览器窗口将会在http://localhost:3000/<website_name>打开,我们可预览自己的网站,并进行实时的更改。

构建网站

生成静态网站的最后一步就是对网站进行构建。构建网站的过程可以将我们的 Markdown 文档转化为html文件,从而生成我们需要的静态网页。

我们可通过如下命令进行网站构建:

使用npm的命令:

1
npm run build

使用yarn的命令:

1
yarn build

构建后的内容将在/build中生成,其中包含了由我们的 Markdown 文档转化而来的html文件。将/build文件夹中的内容复制到静态网站托管服务,从而实现网站部署。这也就进入了我们的下一章节——如何部署我们的网站。

三、基于 Github Pages 的网站部署

本地测试

当完成网站的构建后,在将网站部署到生产环境之前,我们可以先在本地测试构建后的网站是否正常。

我们可通过如下命令进行网站构建:

使用npm的命令:

1
npm run serve

使用yarn的命令:

1
yarn serve

如果运行正常,我们构建后的网站将会在http://localhost:3000/<website_name>打开,我们可检查自己的网站构建是否正常。

这里的npm run serve与上文的npm run start的区别是:npm run serve 呈现出的是在网站构建完成后/build文件夹中的静态页面,不支持实时的更改和预览;npm run start则是基于整个项目的网站预览,由项目文件夹生成,可以进行实时的更改和预览。

网站托管

在这之后,我们可以着手进行我们的网站托管。常见的托管方式包括以下两种:

  1. 使用 Apache2, Nginx 等 HTTP 服务器进行自托管。
  2. 基于 Jamstack 提供商进行托管(如 Netlify, Vercel, Github Pages 等)。

以下将主要介绍如何基于 Github Pages 完成网站的托管。这是一种简便、免费的网站部署方式,且可以非常方便地应用于其他 Github 项目。

通常,使用 Github Pages 发布网站的过程涉及两个仓库(或是两个分支):包含源文件的分支,称为源分支;以及包含网站项目构建完成后的文件(也就是/build/)中的内容,准备通过 Github Pages 进行部署的分支,称为部署分支。

每个 GitHub 仓库都和一个 GitHub Pages 服务关联。其站点域名与仓库名关联如下:

  • 对于组织名或个人名是my-org的账号,如果其有一个名字为my-org.github.io的仓库,那个这个仓库经过 Github Pages 部署后的站点域名为 https://my-org.github.io/
  • 对于组织名或个人名是my-org的账号,如果其有一个名字不为my-org.github.io的仓库my-project,那个这个仓库经过 Github Pages 部署后的站点域名为https://my-org.github.io/my-project/

也就是说,组织或个人的每个仓库都可通过 Github Pages 进行部署,不过只有名为my-org.github.io的指定仓库经过 Github Pages 部署后可以作为网站的根目录my-org.github.io,其余名称的网站都只能作为my-org.github.io的子目录。

GitHub Pages会从默认分支(通常是master / main)或gh-pages分支中获取准备部署的文件(即docusaurus build的输出),该分支即为部署分支。Github Pages 也可选择发布该分支中的/(根目录)或是/docs/。部署 Docusaurus 网站时,我们一般选择发布根目录。 tinkerdocs 仓库将main分支作为了源分支,将gh-pages分支作为了部署分支。其具体选项可在 tinkerdocs->Settings->Pages 中查看与配置。

项目配置

在这之后,我们需要在docusaurus.config.js中完成一些与项目部署和 Github Pages 有关的配置,Tinker Docs 中与网站部署有关的配置具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const config = {
// Set the production url of your site here
url: 'https://tinkerfuroc.github.io/',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/tinkerdocs/',

// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'tinkerfuroc', // Usually your GitHub org/user name.
projectName: 'Tinker_docs', // Usually your repo name.
trailingSlash: false,
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
...
}

其中有一些需要注意的配置:

属性 介绍
url 网站的url。对于部署在 https://my-org.com/my-project/ 的站点, urlhttps://my-org.com/
baseUrl 项目的基本 URL,末尾带有斜杠。对于部署在 https://my-org.com/my-project/ 的站点, baseUrl/my-project/
organizationName 拥有网站仓库的 GitHub 个人或组织。
projectName Github 上网站仓库的名称。
deploymentBranch 部署分支的名称。对于projectName 不以 .github.io 结尾的 GitHub Pages 存储库,它默认为 gh-pages 。否则,它需要明确指定。

网站部署

最后,我们就可以进入激动人心的网站部署环节了。Docusaurus 为我们提供了 deploy 命令,能自动完成构建项目、推送至仓库、发布网站等任务。

使用bash的命令:

1
GIT_USER=<GITHUB_USERNAME> yarn deploy

使用 Windows cmd的命令:

1
cmd /C "set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy"

使用 Windows powershell的命令:

1
cmd /C 'set "GIT_USER=<GITHUB_USERNAME>" && yarn deploy'

至此,我们就已完成了部署网站的全部过程!在浏览器中输入你的网址,你应该就能看见你的网站了。

Tinker_Docs

使用 Github Actions 自动化部署

如果每一次要更新网站,我们都得在本地用命令行进行部署,这显然有些过于繁琐了,且不利于多人协作。有没有办法可以让我们实现一旦网站仓库有更新,就能立刻把新的网站发布出来呢?GitHub Actions 就是一个很好的选择。

GitHub Actions 是一种持续集成和持续交付 (CI/CD) 平台,可用于自动执行生成、测试和部署管道。 您可以创建工作流程来构建和测试存储库的每个拉取请求,或将合并的拉取请求部署到生产环境。

要使用 Github Actions ,我们需要撰写相应的工作流文件(.yaml.yml格式),来让 Github Actions 为项目提供相应的工作流程。幸运的是,Docusaurus 已经为我们写好了相应的文件:

.github/workflows/deploy.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
name: Deploy to GitHub Pages

on:
push:
branches:
- main
# Review gh actions docs if you want to further define triggers, paths, etc
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on

jobs:
build:
name: Build Docusaurus
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18
cache: yarn

- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build website
run: yarn build

- name: Upload Build Artifact
uses: actions/upload-pages-artifact@v3
with:
path: build

deploy:
name: Deploy to GitHub Pages
needs: build

# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source

# Deploy to the github-pages environment
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

.github/workflows/test-deploy.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: Test deployment

on:
pull_request:
branches:
- main
# Review gh actions docs if you want to further define triggers, paths, etc
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on

jobs:
test-deploy:
name: Test deployment
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 18
cache: yarn

- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Test build website
run: yarn build

这两个工作流文件可实现以下功能:

  1. 当向 main 发出新的pull request时,有一个 Action 会检测网站是否能够成功构建,而不会真正进行部署。这一工作流被称为 test-deploy
  2. pull request被合并到 main 分支或有人直接pushmain 分支时,网站将被构建并部署到 GitHub Pages。该作业将被称为 deploy

通过将这两个文件放入项目中的.github/workflows/文件夹并上传至 Github 仓库中,我们即可实现基于 Github Actions 的网站自动化部署。


限于篇幅原因,本文对于 Docusaurus 和 Github Pages 的介绍并未深入展开,感兴趣的同学可阅读官方文档(DocusaurusGithub Pages)进一步学习。

Tinker Docs 在搭建过程中较多地参考了 EESAST Docs | 清华大学电子工程系学生科协 ,在此向 EESAST 各位开发人员表达诚挚谢意。

LiDAR: Light Detection and Ranging

Radar: Radio Detection and Ranging

Lidar工作原理

LiDAR physics principles

两组关键量:

  1. 激光的方向
  2. 测得的距离

测量与计算方式

  1. 飞行时间法(TOF, Pulsed Time of Flight)

    • 单个光脉冲

    img

  2. 调幅连续波(AMCW, amplitude modulated continuous waveform laser )

    • 连续光波,检测相位差

    • 可认为属于TOF

    img

  3. 调频连续波(FMCW, frequency modulated continuous wave)

    img

  • 发射一束连续的光束,频率随时间稳定地发生变化。由于源光束的频率在不断变化,光束传输距离的差异会导致频率的差异,将回波信号与本振信号混频并经低通滤波后,得到的差频信号是光束往返时间的函数。
  • 通过频率差值算出与物体的距离
  • 通过多普勒效应算出物体的速度
  • 毫米波雷达的主要原理

Lidar功能特点

优势

  1. 数据采集速度快且准确度高
  2. 得益于主动照明传感器,LiDAR 技术可以在白天或黑暗中使用
  3. 可用于收集有关无法到达的地方的数据
  4. 便于收集大片地形数据(机载雷达)

劣势

  1. 如果环境有雾或烟雾或透明障碍物,如大雨、低空云层,可能会干扰雷达的使用
  2. 分析收集的大量数据可能会消耗时间和资源
  3. 某些激光雷达使用的强大激光束可能会损害人眼
  4. 难以穿透高密度物质

Lidar类别

结构

机械式

Velodyne Says It's Got a "Breakthrough" in Solid State Lidar Design - IEEE  Spectrum

机械式Lidar

混合固态式(较主流)
  1. MEMS(Microelectromechanical Systems,微机电系统)

img

  1. 二维转镜

    1. 二维扫描

img

  1. 一维扫描

img

固态式(未来发展)
  1. 相控阵(OPA)

    在相控阵中,来自发射器的功率通过称为移相器的设备馈送到辐射元件,该设备由计算机系统控制,可以电子方式改变相位或信号延迟,从而将无线电波束转向不同的方向。

    img

    一束光经过光分束器分为多路光信号。在各路光信号附加相位差之后(以各路光信号赋予均匀的相位差为例,第二个波导与第一个波导的相位差为ΔϕB,第三个波导与第一个波导的相位差为2ΔϕB,以此类推),此时的等相位面不再垂直于波导方向,而是有了一定的偏转,满足等相位关系的波束会相干相长,不满足等相位条件的光束就会相互抵消,故光束的指向总是垂直于等相位面。

img

  1. 泛光面阵(FLASH)

    Flash 激光雷达在短时间直接发射出一大片覆盖探测区域的激光,再以高度灵敏的接收器,来完成对环境周围图像的绘制。

img

维度

  • 1D
img
  • 2D

    一道旋转激光

img
  • 3D

    多道旋转激光

img

使用场景

  • 室内
  • 室外
    • 阳光产生的干扰
    • 雨雪对透光度的影响
    • 室外温度影响

Lidar参数

波长

常见车载雷达使用波长:

  • 905nm

    • 器件相对成熟,成本较低
    • 考虑到人眼安全要求,激光功率受到明显限制
    • 太阳光中存在较多近红外背景光,传感器信噪比物理上受限,最大探测距离限制在150米左右
    • 穿透力更强,能够应对复杂环境情况
  • 1550nm

    • 同等功率的1550 nm激光人眼安全性提高40倍
    • 背景光干扰问题相对较小,可以实现远距离探测
    • 探测器只对自身发射的激光回波响应,信噪比远高于905nm-ToF激光雷达,最大探测距离可以达到1000米以上
    • 1550 nm配合调频连续波(FMCW)的技术,不仅可以检测距离,同时可以利用多普勒频移来测量物体的速度

    img

测距能力

在车载lidar的表征上经常会通过10%反射率板标注最远测距距离,表达形式如XXm@10% reflectivity的测距能力。测距距离越长表明在系统中接触到物体的时间越早,留给系统判断和决策的时间越长

反射率(reflectivity),是材料表面反射辐射能的有效性。它是在边界处反射电磁功率与入射电磁功率的比值。

勒克斯(Lux,通常简写为lx)是一个标识照度的国际单位制单位,1流明每平方米面积,就是1勒克斯。

测距精度

测距精度主要表征lidar测量物体时数据的一致性。精度越高表示误差越小,每一次获取到的数值更为一致。

例如:<2cm (1σ@20m)即表示在20m测量位置,在1σ(标准差)的概率下,每次测量的误差值小于2cm。

视场角(FOV, Field of View)

分为水平视场角,垂直视场角。表示lidar前方能扫描到的区域。视场角越大则扫描范围越大。

角分辨率

两点之间的角度间隔(水平与竖直)。间隔越小对物体细节表现越细致。若有开发ROI功能,角分辨率会更小。

帧率

lidar每秒传递的帧数,每一帧都是完整的一轮扫描结果,以Hz为单位,通常为5、10、20Hz

点频

lidar扫描1s的总探测点数(点/秒)

$单帧点数 * 帧率数值 = 点频$

盲区

考虑雷达盒体内的光束探测反馈(玻璃片前导信号及其他反馈信号)可能会使得近距离的探测精度大幅度下降使得数据没有意义,所以会存在直接舍去近距离点云的部分数据,这部分会被划定为盲区。

  • 补盲雷达:专门负责检测近处物体

[2] 若目标物体距离 0.1 m 至 0.2 m,激光探测测距仪可探测并输出点云数据,但由于无法保证探测精度,此数据仅供参考。

人眼安全级别(Laser class)

几乎所有激光雷达扫描仪都属于激光等级1,这是最安全的级别。它表示扫描仪是完全无害的,你可以在眼睛没有任何保护措施的情况下直视它,而不会受到任何负面影响。

防护等级

IP等级的第一位数字代表其抗尘性,第二个数字是其抗液性 (IP67)

功率

雷达发热情况

IMU

使用IMU更新Lidar的朝向

Tinker使用的Lidar——Livox Mid 360

业界首款 360° 混合固态激光雷达

¥3999

觅道  Mid-360

image-20230924124405603

image-20230921181441600

LiDAR数据处理

PointCloud

sensor_msgs/PointCloud

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# This message holds a collection of 3d points, plus optional additional
# information about each point.

# Time of sensor data acquisition, coordinate frame ID.
Header header

# Array of 3d points. Each Point32 should be interpreted as a 3d point
# in the frame given in the header.
geometry_msgs/Point32[] points

# Each channel should have the same number of elements as points array,
# and the data in each channel should correspond 1:1 with each point.
# Channel names in common practice are listed in ChannelFloat32.msg.
ChannelFloat32[] channels

sensor_msgs/ChannelFloat32

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# This message is used by the PointCloud message to hold optional data
# associated with each point in the cloud. The length of the values
# array should be the same as the length of the points array in the
# PointCloud, and each value should be associated with the corresponding
# point.

# Channel names in existing practice include:
# "u", "v" - row and column (respectively) in the left stereo image.
# This is opposite to usual conventions but remains for
# historical reasons. The newer PointCloud2 message has no
# such problem.
# "rgb" - For point clouds produced by color stereo cameras. uint8
# (R,G,B) values packed into the least significant 24 bits,
# in order.
# "intensity" - laser or pixel intensity.
# "distance"

# The channel name should give semantics of the channel (e.g.
# "intensity" instead of "value").
string name

# The values array should be 1-1 with the elements of the associated
# PointCloud.
float32[] values

典型应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//将数据通过消息发送出去
sensor_msgs::PointCloud cloud;
cloud.header.stamp = ros::Time::now();
cloud.header.frame_id = "sensor_frame";//帧id
cloud.points.resize(LINE*CIRCLEPT);
cloud.channels.resize(2);//设置增加通道数
cloud.channels[0].name = "intensity";//增加反射强度信道,并设置其大小,使与点云数量相匹配
cloud.channels[0].values.resize(LINE*CIRCLEPT);
cloud.channels[1].name = "distance";//增加距离信道,并设置其大小,使与点云数量相匹配
cloud.channels[1].values.resize(LINE*CIRCLEPT);
int i=0;
for (int l = 0; l < LINE; l++)
for (int c = 0; c < CIRCLEPT; c++)
{
cloud.points[i].x = mdecoder.mpointcloud[l][c].x;
cloud.points[i].y = mdecoder.mpointcloud[l][c].y;
cloud.points[i].z = mdecoder.mpointcloud[l][c].z;
cloud.channels[0].values[i] = mdecoder.mpointcloud[l][c].r;//设置反射强度
cloud.channels[1].values[i] = mdecoder.mpointcloud[l][c].d;
i++;
}
cloud_pub.publish(cloud);

PointCloud2

sensor_msgs/PointCloud2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# This message holds a collection of N-dimensional points, which may
# contain additional information such as normals, intensity, etc. The
# point data is stored as a binary blob, its layout described by the
# contents of the "fields" array.

# The point cloud data may be organized 2d (image-like) or 1d
# (unordered). Point clouds organized as 2d images may be produced by
# camera depth sensors such as stereo or time-of-flight.

# Time of sensor data acquisition, and the coordinate frame ID (for 3d
# points).
Header header

# 2D structure of the point cloud. If the cloud is unordered, height is
# 1 and width is the length of the point cloud.
uint32 height # height != 1: 有组织的二维点云,通过了解相邻点(例如像素)之间的关系,最近邻操作更加高效
uint32 width # point的行数,若height = 1则width为所有点的数量

# Describes the channels and their layout in the binary data blob.
PointField[] fields

bool is_bigendian # Is this data bigendian?
uint32 point_step # Length of a point in bytes
uint32 row_step # Length of a row in bytes
# row_step = width * point_step 存所有数据(一个数组)的字节
uint8[] data # Actual point data, size is (row_step*height)

bool is_dense # True if there are no invalid points

big endian & small endian

img

sensor_msgs/PointField

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# This message holds the description of one point entry in the
# PointCloud2 message format.
uint8 INT8 = 1
uint8 UINT8 = 2
uint8 INT16 = 3
uint8 UINT16 = 4
uint8 INT32 = 5
uint8 UINT32 = 6
uint8 FLOAT32 = 7
uint8 FLOAT64 = 8

string name # Name of field
uint32 offset # Offset from start of point struct
uint8 datatype # Datatype enumeration, see above
uint32 count # How many elements in the field

典型应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
header: 
seq: 1071
stamp:
secs: 1521699326
nsecs: 676390000
frame_id: "velodyne"
height: 1
width: 66811
fields:
-
name: "x"
offset: 0
datatype: 7
count: 1
-
name: "y"
offset: 4
datatype: 7
count: 1
-
name: "z"
offset: 8
datatype: 7
count: 1
-
name: "intensity"
offset: 16
datatype: 7
count: 1
-
name: "ring"
offset: 20
datatype: 4
count: 1
is_bigendian: False
point_step: 32
// FLOAT32(4) + FLOAT32(4) + FLOAT32(4) + FLOAT32(4) + UINT16(2) + EMPTY(10) = 20
row_step: 2137952 // = 66811 * 32
data: [235, 171, 54, 190, 53, 107, 250, ...

offset:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ~31
x x x x y y y y z z z z - - - - i i i i r r r r -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <sensor_msgs/PointCloud2.h>
#include <sensor_msgs/PointField.h>
#include <sensor_msgs/point_cloud2_iterator.h>

sensor_msgs::PointCloud2 pcl_msg;

//Modifier to describe what the fields are.
sensor_msgs::PointCloud2Modifier modifier(pcl_msg);

modifier.setPointCloud2Fields(4,
"x", 1, sensor_msgs::PointField::FLOAT32,
"y", 1, sensor_msgs::PointField::FLOAT32,
"z", 1, sensor_msgs::PointField::FLOAT32,
"intensity", 1, sensor_msgs::PointField::FLOAT32);

//Msg header
pcl_msg.header = std_msgs::Header();
pcl_msg.header.stamp = ros::Time::now();
pcl_msg.header.frame_id = "frame";

pcl_msg.height = height;
pcl_msg.width = width;
pcl_msg.is_dense = true;

//Total number of bytes per point
pcl_msg.point_step = 16;
pcl_msg.row_step = pcl_msg.point_step * pcl_msg.width;
pcl_msg.data.resize(pcl_msg.row_step);

//Iterators for PointCloud msg
sensor_msgs::PointCloud2Iterator<float> iterX(pcl_msg, "x");
sensor_msgs::PointCloud2Iterator<float> iterY(pcl_msg, "y");
sensor_msgs::PointCloud2Iterator<float> iterZ(pcl_msg, "z");
sensor_msgs::PointCloud2Iterator<float> iterIntensity(pcl_msg, "intensity");

//iterate over the message and populate the fields.
for (int i = 0; i < width; i++)
{
*iterX = //Your x data
*iterY = //Your y data
*iterZ = //Your z data
*iterIntensity = //Your intensity data

// Increment the iterators
++iterX;
++iterY;
++iterZ;
++iterIntensity;
}

环境配置

感兴趣的同学可在课前按照这一教程 练习:创建第一个 .NET MAUI 应用 - Training | Microsoft Learn 配置环境,以跑出如下的Hello World界面为成功。

本节课内容将主要针对于Windows平台下的开发,暂时无需配置其他平台的环境。

image-20240128221948128

MAUI简介

什么是MAUI

MAUI,全名Multi-platform Application User Interface,是微软提出的一个跨平台框架,用于使用 C# 和 XAML 创建本机移动和桌面应用。

使用 .NET MAUI,可从单个共享代码库开发可在 Android、iOS、macOS 和 Windows 上运行的应用。

可以针对以下平台编写 .NET Multi-platform App UI (.NET MAUI) 应用:

  • Android 5.0 (API 21) 或更高版本。
  • iOS 11 或更高版本,使用最新版本的 Xcode。
  • macOS 10.15 或更高版本,使用 Mac Catalyst。
  • Windows 11 和 Windows 10 版本 1809 或更高版本,使用 Windows UI 库 (WinUI) 3。

MAUI开发平台

  • Windows + Visual Studio,强烈推荐

  • macOS + Visual Studio for Mac(Visual Studio for Mac 计划于 2024 年 8 月 31 日停用)

  • Windows/macOS/Linux + Vscode with .NET MAUI Extension(预览版)

    此扩展仍为早期预览版,因此存在许多已知限制:

    • XAML 编辑功能非常轻量 - 你可以获得基本的语法突出显示和自动完成功能。 我们正在探索如何在将来的版本中改进 XAML 体验。
    • 目前,无法切换 IntelliSense 的目标框架(它仅针对 .csproj 文件中列出的第一个目标框架显示语法突出显示)。 此功能正在开发中。
    • 目前不支持 XAML 和 .NET 热重载。
    • 此扩展尚未使用最新的 iOS 和 Xcode beta 版本进行全面测试。
你的操作系统 受支持的目标平台
Windows Windows、Android
macOS Android、iOS、macOS
Linux Android

MAUI基础教程

MAUI文件结构

image-20240125175530880

  • MauiWinterTutorial:csproj文件,用于整个项目的配置

  • Platforms:针对不同平台所需的文件资源和配置

  • Resources:图标、图片、字体等项目资源文件

  • MauiProgram.cs:这是启动应用的代码文件。 此文件中的代码充当应用的跨平台入口点,用于配置和启动应用。 模板启动代码指向由 App.xaml 文件定义的 App 类。

    所有 XAML 文件通常都包含两个文件,即 .xaml 文件本身,以及一个相应的代码文件,该文件是“解决方案资源管理器”中的 .xaml 文件的子项。 .xaml 文件包含 XAML 标记,代码文件包含用户创建的用于与 XAML 标记交互的代码。

  • App.xaml 和 App.xaml.cs:App.xaml 文件包含应用范围的 XAML 资源,例如颜色、样式或模板。 App.xaml.cs 文件通常包含用于实例化 Shell 应用程序的代码。 在此项目中,它指向 AppShell 类。

  • AppShell.xaml 和 AppShell.xaml.cs:此文件定义 AppShell 类,该类用于定义应用的视觉对象层次结构(页面导航)。

    Screenshot of a Shell flyout.

  • MainPage.xaml 和 MainPage.xaml.cs:这是应用显示的启动页。 MainPage.xaml 文件定义页面的 UI(用户界面)。 MainPage.xaml.cs 包含 XAML 的代码隐藏,如按钮单击事件的代码。

项目启动流程:

MauiProgram.cs –> App –> AppShell –> MainPage

XAML概述

eXtensible Application Markup Language(XAML,可扩展的应用程序标记语言)是基于 XML 的语言,可替代编程代码执行对象的实例化和初始化并按父子层次结构来组织这些对象。

XAML 使开发人员能够使用标记而不是代码定义 .NET Multi-platform App UI (.NET MAUI) 应用中的用户界面。 XAML 在 .NET MAUI 应用中不是必需的(XAML实现的UI设计功能均可通过等效的C#代码实现),但建议使用它来开发 UI,因为它通常更简洁、更直观、更连贯,并且具有工具支持。 XAML 也非常适合与模型-视图-视图模型 (MVVM) 模式一起使用,其中 XAML 定义视图,而视图通过基于 XAML 的数据绑定链接到视图模型代码。

属性设置

1
2
3
4
5
<Label Text="Hello, XAML!"
VerticalOptions="Center"
FontAttributes="Bold"
FontSize="18"
TextColor="Aqua" />
1
2
3
4
5
6
7
8
<Label Text="Hello, XAML!"
VerticalOptions="Center"
FontAttributes="Bold"
FontSize="18">
<Label.TextColor>
Aqua
</Label.TextColor>
</Label>

这两个指定 TextColor 属性的示例在功能上是等效的。

在这里,Label是一个对象元素。 它是一个以 XML 元素表示的 .NET MAUI 对象。

可以等效认为一个XAML标签就代表了一个对象(类),对象之间成并列或包含关系,标签内部包含了这个对象的属性及其他信息。

命名空间

通常而言,不会通过 XAML 来定义命名空间,而只是在 XAML 中声明来自C#程序或其他程序中的命名空间。

XAML 使用 xmlns XML 特性进行命名空间声明。 XAML 文件的根元素中始终有两个 XAML 命名空间声明。

  • 第一个定义默认命名空间:

    1
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"

    默认命名空间指定 XAML 文件中定义的不带前缀的元素引用 .NET Multi-platform App UI (.NET MAUI) 类,例如 ContentPage、Label 和 Button。

  • 第二个命名空间声明使用 x 前缀:

    1
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

    XAML 使用前缀声明非默认命名空间,并在引用命名空间中的类型时使用前缀。 x 命名空间声明指定在 XAML 中定义的带有 x 前缀的元素用于 XAML固有的元素和属性。

    下表概述了 .NET MAUI 支持的 x 构造:

    构造 描述
    x:Arguments 指定非默认构造函数或工厂方法对象声明的构造函数参数。
    x:Class 指定 XAML 中定义的类的命名空间和类名。 类名必须与代码隐藏文件的类名匹配。 请注意,此构造只能出现在 XAML 文件的根元素中。
    x:ClassModifier 指定程序集中生成的类的访问级别。
    x:DataType 指定 XAML 元素及其子元素将绑定到的对象的类型。
    x:FactoryMethod 指定可用于初始化对象的工厂方法。
    x:FieldModifier 指定命名 XAML 元素的生成字段的访问级别。
    x:Key 为 ResourceDictionary 中的每个资源指定一个唯一的用户定义密钥。 密钥的值用于检索 XAML 资源,通常用作 StaticResource标记扩展的参数。
    x:Name 指定 XAML 元素的运行时对象名称。 设置 x:Name 类似于在代码中声明变量。
    x:TypeArguments 指定泛型类型构造函数的泛型类型参数。
    1
    2
    3
    4
    5
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Class="MauiWinterTutorial.MainPage">
    ...
    </ContentPage>

可以通过使用前缀声明 XAML 命名空间,在 XAML 中引用类型,命名空间声明指定公共语言运行时 (CLR) 命名空间名称以及可选的程序集名称。 这通过在命名空间声明中定义以下关键字的值来实现:

  • clr-namespace:using: - 在程序集(程序集内部)中声明的 CLR 命名空间,此程序集包含要作为 XAML 元素公开的类型。 此关键字是必需的。
  • assembly= - 包含引用的 CLR 命名空间的程序集(外部程序集)。 该值是程序集的名称,不带文件扩展名。 程序集的路径应在包含将引用程序集的 XAML 文件的项目中作为引用建立。 如果 clr-namespace 值与引用类型的应用代码位于同一程序集中,则可以省略此关键字。

在程序集中声明命名空间:

1
2
3
4
5
6
7
<ContentPage ... xmlns:local="clr-namespace:MyMauiApp">
...
</ContentPage>
<!-- 等价于 -->
<ContentPage ... xmlns:local="using:MyMauiApp">
...
</ContentPage>

在以下例子中,我们可看到如何在 XAML 中引用在同程序集下的 C# 文件中定义的命名空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Monkey.cs
namespace MonkeyFinder.Model;

public class Monkey
{

public string Name { get; set; }
public string Location { get; set; }
public string Details { get; set; }
public string Image { get; set; }
public int Population { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
}
...
1
2
3
4
5
6
7
<!-- MainPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MonkeyFinder.View.MainPage"
xmlns:model="clr-namespace:MonkeyFinder.Model">
...
</ContentPage>

引用其他程序集中的命名空间:

1
2
3
<ContentPage ... xmlns:custom="clr-namespace:CustomNamespace;assembly=CustomAssembly">
...
</ContentPage>

在这里,custom是自行定义的前缀,CustomNamespace是要引用的命名空间的名称,CustomAssembly是包含这个命名空间的程序集的名称。在之后可以使用custom这个前缀来引用这个命名空间中的类型,例如<custom:CustomType />

标记拓展

将在数据绑定一节详细展开

XAML热重载

XAML 热重载是一项 Visual Studio 功能,可用于查看正在运行的应用中 XAML 更改的结果,而无需重新生成项目。 如果没有 XAML 热重载,每次要查看 XAML 更改结果时,都需要生成和部署应用。

当 .NET MAUI 应用连接了调试器并在调试配置下运行时,XAML 热重载会分析 XAML 编辑内容,并将相关更改发送到正在运行的应用。 由于它不会为整个页面重新创建 UI,因此会保留 UI 状态,并更新受编辑影响的控件上被更改的属性。 此外,可保持导航状态和数据,这样既可快速迭代 UI,又不影响在应用中的即时位置。 因此,可减少重新生成和部署应用以验证 UI 更改的时间。

MAUI页面设计

视图

  • Label

    Label可用于显示带有修饰的文本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!-- 创建标签 -->
    <Label Text="Hello world" />

    <!-- 设置颜色 -->
    <Label TextColor="#77d065"
    Text="This is a green label." />

    <Label TextColor="Green"
    Text="This is a green label." />

    <!-- 添加换行 -->
    <Label Text="First line &#10; Second line" />

    <Label>
    <Label.Text>
    First line
    Second line
    </Label.Text>
    </Label>

    Label 具有 FormattedText 属性,该属性允许在同一视图中显示具有多个字体和颜色的文本。 FormattedText 属性是 FormattedString 类型,由一个或多个 Span 实例组成,通过 Spans 属性设置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 使用格式化文本 -->
    <Label LineBreakMode="WordWrap">
    <Label.FormattedText>
    <FormattedString>
    <Span Text="Red Bold, " TextColor="Red" FontAttributes="Bold" />
    <Span Text="default, " FontFamily="OpenSansSemiBold" FontSize="14" />
    <Span Text="default, " FontFamily="OpenSansRegular" FontSize="14"
    TextDecorations="Underline" />
    <Span Text="italic small." FontAttributes="Italic" FontSize="12" />
    </FormattedString>
    </Label.FormattedText>
    </Label>
  • BoxView

    BoxView 可绘制具有指定宽度、高度和颜色的简单矩形或正方形。

    1
    2
    3
    4
    5
    6
    <BoxView Color="CornflowerBlue"
    CornerRadius="10"
    WidthRequest="160"
    HeightRequest="160"
    VerticalOptions="Center"
    HorizontalOptions="Center" />
  • Border

    要绘制边框,可创建一个 Border 对象并设置其属性以定义外观,然后将其子级设置为要添加边框的控件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <Border Stroke="#C49B33"
    StrokeThickness="4"
    StrokeShape="RoundRectangle 40,0,0,40"
    Background="#2B0B98"
    Padding="16,8"
    HorizontalOptions="Center">
    <Label Text=".NET MAUI"
    TextColor="White"
    FontSize="18"
    FontAttributes="Bold" />
    </Border>
  • Image

    Image 显示可从本地文件、URI 或流加载的图像。 支持动画 GIF 等标准平台图像格式,还支持本地可缩放矢量图形 (SVG) 文件

    右键Resources/Images文件夹,选择添加-现有项-右下角文件类型选择所有文件-选择一个图片文件(文件名需小写)

    image-20240128140235212 image-20240128140154963
    1
    2
    3
    4
    5
    <!-- 使用maui自带的dotnet_bot图片 -->
    <Image Source="dotnet_bot.png" />

    <!-- 把Source改为自己的图片 -->
    <Image Source="eesastlogo.png" WidthRequest="100" HeightRequest="100"/>

    控制图像缩放

    Aspect 属性确定如何缩放图像以适合显示区域,并应设置为 Aspect 枚举的其中一个成员:

    • AspectFit - 对图像上下加框(如需要),以使整个图像适合显示区域,并根据图像的宽度或高度,在顶部/底部或侧边添加空白区域。
    • AspectFill - 剪裁图像,使其填充显示区域,同时保持纵横比。
    • Fill - 拉伸图像,以完全、准确填充显示区域。 这可能会导致图像失真。
    • Center - 将图像在显示区域居中显示,同时保持纵横比。
    1
    2
    <!-- 控制图像缩放 -->
    <Image Source="eesastlogo.png" HeightRequest="100" WidthRequest="200" Aspect="Fill" />
  • Button

    Button 可显示文本并响应指示应用执行任务的点击或单击操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <Button Text="Click The Button"
    VerticalOptions="Center"
    HorizontalOptions="Center"
    Clicked="OnButtonClicked" />
    <Label x:Name="label"
    Text="Click Counter"
    FontSize="18"
    VerticalOptions="Center"
    HorizontalOptions="Center" />

    同时,在MainPage.xaml.csMainPage类中添加以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int count = 0;

    private void OnButtonClicked(object sender, EventArgs e)
    {
    count++;
    if (count == 1)
    {
    label.Text = "Have Clicked " + count.ToString() + " Time";
    }
    else
    {
    label.Text = "Have Clicked " + count.ToString() + " Times";
    }
    }
  • ListView

    将在数据拓展一节详细展开

布局

布局类允许在应用程序中排列 UI 控件并对其进行分组。 选择布局类需要了解布局如何定位其子元素,以及布局如何调整其子元素的大小。 此外,可能还需要嵌套布局来创建所需的布局。

.NET MAUI layout classes.

  • StackLayout

    StackLayout 在水平或垂直方向上组织一维堆栈中的元素。 默认情况下,StackLayout 是垂直方向。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <StackLayout Margin="20"
    Spacing="6">
    <Label Text="Veritical" />
    <BoxView Color="Red"
    HeightRequest="40"
    WidthRequest="400"
    HorizontalOptions="Start"/>
    <BoxView Color="Yellow"
    HeightRequest="40"
    WidthRequest="400"
    HorizontalOptions="Center"/>
    <BoxView Color="Blue"
    HeightRequest="40"
    WidthRequest="400"
    HorizontalOptions="End"/>
    </StackLayout>
    <StackLayout Margin="20" HeightRequest="300"
    Spacing="100" Orientation="Horizontal">
    <Label Text="Horizontal" />
    <BoxView Color="Red"
    HeightRequest="200"
    WidthRequest="40"
    VerticalOptions="Start"/>
    <BoxView Color="Yellow"
    HeightRequest="200"
    WidthRequest="40"
    VerticalOptions="Center"/>
    <BoxView Color="Blue"
    HeightRequest="200"
    WidthRequest="40"
    VerticalOptions="End"/>
    </StackLayout>
  • Grid

    Grid 提供网格化的布局形式,它将其子级组织为可以有比例大小或绝对大小的行和列。

    • 创建Grid

      可以使用 RowDefinitionsColumnDefinitions 属性定义Grid 的布局行为,这两个属性分别是 RowDefinitionColumnDefinition 对象的集合。 这些集合定义 Grid 的行和列特征,并且应针对 Grid 中的每一行包含一个 RowDefinition 对象,以及针对 Grid 中的每一列包含一个 ColumnDefinition 对象。

      RowDefinition 类定义类型为 GridLengthHeight 属性,而 ColumnDefinition 类定义类型为 GridLengthWidth 属性。 GridLength 结构根据 GridUnitType 枚举指定行高或列宽,其中有三个成员:

      • Absolute – 行高或列宽的值采用与设备无关的单位(XAML 中的数字)。
      • Auto – 根据单元格内容自动调整行高或列宽(XAML 中的 Auto)。
      • Star – 按比例分配剩余行高或列宽(在 XAML 中,数字后跟 *)。
    • 将子视图包含于Grid单元格中

      可以将子视图放置在包含 Grid.ColumnGrid.Row 附加属性的特定 Grid单元格中。此外,要使子视图跨越多个行和列,请使用 Grid.RowSpanGrid.ColumnSpan 附加属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    <Grid>
    <Grid.RowDefinitions>
    <RowDefinition Height="2*" />
    <RowDefinition Height="*" />
    <RowDefinition Height="100" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
    <ColumnDefinition Width="100"/>
    <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <BoxView Color="Green" />
    <Label Text="Row 0, Column 0"
    HorizontalOptions="Center"
    VerticalOptions="Center" />
    <BoxView Grid.Column="1"
    Color="Blue" />
    <Label Grid.Column="1"
    Text="Row 0, Column 1"
    HorizontalOptions="Center"
    VerticalOptions="Center" />
    <BoxView Grid.Row="1"
    Color="Teal" />
    <Label Grid.Row="1"
    Text="Row 1, Column 0"
    HorizontalOptions="Center"
    VerticalOptions="Center" />
    <BoxView Grid.Row="1"
    Grid.Column="1"
    Color="Purple" />
    <Label Grid.Row="1"
    Grid.Column="1"
    Text="Row1, Column 1"
    HorizontalOptions="Center"
    VerticalOptions="Center" />
    <BoxView Grid.Row="2"
    Grid.ColumnSpan="2"
    Color="Red" />
    <Label Grid.Row="2"
    Grid.ColumnSpan="2"
    Text="Row 2, Columns 0 and 1"
    HorizontalOptions="Center"
    VerticalOptions="Center" />
    </Grid>

MAUI数据绑定

绑定的概念

数据绑定是将两个对象的属性链接起来的技术,其中一个属性的更改会自动反映在另一个属性中。 数据绑定涉及两个对象,其中一个几乎总是从View派生的元素,并构成页面可视界面的一部分。 另一个对象则是:

  • 派生自另一个View,且通常位于同一页面。
  • 或是代码文件中的对象。

目标和源

.NET Multi-platform App UI (.NET MAUI) 数据绑定在两个对象之间链接一对属性,其中至少一个对象通常是用户界面对象。 这两个对象称为“目标”和“源”:

  • “目标”是设置数据要绑定的对象(和属性)。
  • “源”是数据绑定引用的对象(和属性)。

XAML中的标记拓展

XAML 标记扩展支持从各种源设置元素属性,从而帮助增强 XAML 的功能和灵活性。

例如,通常会按如下设置Color 属性:

1
<BoxView Color="Blue" />

标记扩展是表达元素属性的另一种方法,这为我们提供了一种从已创建的变量、页面其它元素等其它途径设置Color属性的方法。 .NET MAUI XAML 标记扩展通常由括在大括号中的属性值标识:

1
<BoxView Color="{StaticResource themeColor}" />

.NET MAUI 中包含的主要标记拓展有:

  • x:Static 标记扩展:该类具有类型为 string 的名为 Member 的单一属性,你可以将其设置为公共常量、静态属性、静态字段或枚举成员的名称。

    1
    2
    3
    4
    5
    6
    namespace MarkupExtensions;

    static class AppConstants
    {
    public static double NormalFontSize = 18;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MarkupExtensions"
    x:Class="MarkupExtensions.StaticDemoPage">

    <Label Text="Label No. 3"
    FontSize="{x:Static Member=local:AppConstants.NormalFontSize}" />
    ...
    </ContentPage>

  • x:Reference 标记拓展: 该类具有 string 类型的名为 Name 的单一属性,可以将其设置为页面上已使用 x:Name 命名的元素的名称。

  • x:Binding 标记拓展:具体的实现绑定方式,将目标对象与源的属性实现绑定。

    1. 将目标对象的 BindingContext 属性设置为源对象
    2. 在目标对象上调用 SetBinding 方法(通常与 Binding 类结合使用),将该对象的属性绑定到源对象的属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <StackLayout>
    <Label Text="ROTATION"
    BindingContext="{x:Reference slider}"
    Rotation="{Binding Path=Value}"
    FontAttributes="Bold"
    FontSize="18"
    HorizontalOptions="Center"
    VerticalOptions="Center" />
    <Slider x:Name="slider"
    Maximum="360"
    VerticalOptions="Center" />
    <Label BindingContext="{x:Reference slider}"
    Text="{Binding Value, StringFormat='The angle is {0:F0} degrees'}"
    FontAttributes="Bold"
    FontSize="18"
    HorizontalOptions="Center"
    VerticalOptions="Center" />
    </StackLayout>
  • x:Type 标记拓展:标记扩展返回该类或结构的类型,通常与 x:Array 搭配使用。

  • x:Array标记拓展:在标记中定义数组。

    ListView:在ItemSource中定义要显示的项目集合,在ItemTemplate中用于指定显示每个项目模板(类型为DataTemplate

    ListView 中每个项的外观由 DataTemplate 定义,且 DataTemplate 必须引用 Cell 类来显示项。 每个单元格表示 ListView 中的数据项。 .NET MAUI 包含以下内置单元格:

    • TextCell,用于在单独的行上显示主要文本和辅助文本。
    • ImageCell,用于在单独的行上显示带有主要文本和辅助文本的图像。
    • SwitchCell(不常用),用于显示文本以及可打开或关闭的开关。
    • EntryCell(不常用),用于显示可编辑的标签和文本。
    • ViewCell,是一个自定义单元格,其外观由 View(也就是之前所讲的视图和布局的组合) 定义。 如果要完全定义 ListView 中每个项的外观,则应使用此单元格类型。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <ListView Margin="10">
    <ListView.ItemsSource>
    <x:Array Type="{x:Type Color}">
    <Color>Aqua</Color>
    <Color>Black</Color>
    <Color>Blue</Color>
    <Color>Fuchsia</Color>
    <Color>Gray</Color>
    <Color>Green</Color>
    <Color>Lime</Color>
    <Color>Maroon</Color>
    <Color>Navy</Color>
    <Color>Olive</Color>
    <Color>Pink</Color>
    <Color>Purple</Color>
    <Color>Red</Color>
    <Color>Silver</Color>
    <Color>Teal</Color>
    <Color>White</Color>
    <Color>Yellow</Color>
    </x:Array>
    </ListView.ItemsSource>
    <ListView.ItemTemplate>
    <DataTemplate>
    <ViewCell>
    <BoxView Color="{Binding}"
    HeightRequest="10"
    Margin="3" />
    </ViewCell>
    </DataTemplate>
    </ListView.ItemTemplate>
    </ListView>

绑定模式

  • Default-目标自身的默认绑定模式
  • OneWay - 数据从源到目标单向传输
  • OneWayToSource - 数据从目标到源单向传输
  • TwoWay - 数据在源和目标之间双向传输
  • OneTime - 数据从源到目标单向传输,但只有 BindingContext 发生更改时才会传输

MVVM设计模式

MVVM模式的组成部分

MVVM是Model-View-ViewModel的缩写,目的是更好地分离前后端,降低代码的耦合程度。

MVVM 模式中有三个核心组件:模型(Model)、视图(View)和视图模型(View Model)。以下示意图显示了这三个组件之间的关系。

A diagram demonstrating the parts of an MVVM-modeled application

  • View:负责定义用户在屏幕上看到的结构、布局和外观。 (纯前端外观)
  • View Model:实现视图可以数据绑定到的属性和命令,并通过更改通知事件通知视图任何状态更改。 (存了所有View中需要显示的东西)
  • Model:封装应用数据的非可视类,通常包括数据模型以及业务和验证逻辑。(View Model以外的其它类和数据)

MVVM案例分析:猴子查看软件

相关教程

微软官方教程:什么是 .NET MAUI? - .NET MAUI | Microsoft Learn

一个超赞的MAUI workshop,可能需要一些C#和多线程基础:dotnet-presentations/dotnet-maui-workshop(github.com)

我的心灵家园

这里是Grange的博客网站。
始建于2022年10月22日,在2023年5月19日被翻修,正式投入使用。

2023年5月20日凌晨

于清华园