#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析6:如何将SVG插入到画布中 原创
本文会详细介绍如何将矢量图(SVG)插入画布,矢量图需要被编码为base64,然后直接保存到js代码中。如显示动物列表的js代码如下图所示:

每一个svg是一个数组元素。最终的SVG显示界面如下图所示。

这个界面是使用InsertImages组件实现的。。
InsertImages类实现的详细解释与注释
代码解释
这段代码实现了一个用于选择图像的组件 InsertImages。该组件由一组按钮和一个Webview组成,按钮用于选择不同的图像类别,Webview用于显示图像列表。用户可以点击按钮从不同的图像类别中选择图像,并将选择的结果通过回调函数返回。
详细代码注释
// 从相对路径引入常量,这些常量定义了一些面板和按钮的背景颜色
import { PANEL_BACKGROUND_COLOR, PANEL_BUTTON_BACKGROUND_COLOR } from '../common/const'
// 引入ArkWeb中的webview模块
import { webview } from '@kit.ArkWeb';
// 引入常用的工具函数,如去除引号
import { trimQuotes } from '../common/common';
// 使用@Entry标记这个组件为入口组件
@Entry
// 声明一个组件InsertImages
@Component
export struct InsertImages {
  
  // 可选的回调函数,当选择图像时触发,传递图像列表的名称和索引
  imageSelected?: (imageListName: string, imageListIndex: number) => void;
  
  // 可选的回调函数,当图像列表关闭时触发
  imageListClosed?: () => void;
  
  // 声明一个WebviewController,用于控制Webview组件
  controller1: webview.WebviewController = new webview.WebviewController();
  
  // 声明一个状态变量formatName,用于存储格式名称
  @State formatName: string = '';
  // build方法定义了组件的UI结构
  build() {
    // 创建一个Column组件,用于垂直布局
    Column() {
      
      // 创建一个Row组件,用于水平布局
      Row() {
        
        // 再次创建一个Column组件,内嵌的Column,用于放置多个按钮
        Column() {
          
          // 创建一个Row组件,用于放置选择按钮和关闭图标
          Row() {
            
            // 创建一个按钮组件,用于触发选择图像的操作
            Button({ type: ButtonType.Normal }) {
              // 按钮内部的文本
              Text('选择')
                .fontSize(16) // 设置字体大小
                .fontColor('#FFFFFF') // 设置字体颜色
                .width('100%') // 设置宽度为100%
                .height('100%') // 设置高度为100%
                .textAlign(TextAlign.Center) // 设置文本居中对齐
            }
            .borderRadius(5) // 设置按钮的圆角半径
            .width(60) // 设置按钮的宽度
            .height(30) // 设置按钮的高度
            .backgroundColor('#CC0000') // 设置按钮的背景颜色
            .onClick(() => { // 设置按钮点击事件
              
              // 调用Webview中的JavaScript函数获取选中的图像数据
              this.controller1.runJavaScript(`getSelectedImageData()`)
                .then(data => {
                  
                  if (data === '') { // 如果没有选中任何图像
                    // 显示一个提示对话框,告知用户未选择图像
                    AlertDialog.show({
                      message: '还没有选择图像',
                      alignment: DialogAlignment.Center,
                      primaryButton: {
                        value: '关闭',
                        action: () => {
                          // 对话框关闭时不执行任何操作
                        }
                      }
                    })
                  } else {
                    
                    // 将返回的JSON字符串解析为数组
                    let parsedArray: [string, number];
                    try {
                      parsedArray = JSON.parse(data);
                      // 提取图像列表名称和索引
                      let imageListName: string = parsedArray[0];
                      let imageListIndex: number = parsedArray[1];
                      
                      // 如果imageSelected回调函数存在,则调用该回调函数
                      if (this.imageSelected) {
                        this.imageSelected(imageListName, imageListIndex);
                      }
                    } catch (e) {
                      console.error("Error parsing JSON string:", e); // 捕捉解析错误并输出日志
                    }
                  }
                });
            })
            // 创建一个关闭图像列表的图标,点击时触发关闭事件
            Image($r('app.media.close')).width(20).height(20)
              .margin({ left: 5, top: 0 }) // 设置图标的左边距
              .onClick(() => {
                if (this.imageListClosed) { // 如果imageListClosed回调函数存在,则调用该回调函数
                  this.imageListClosed();
                }
              })
          }
          .width('100%') // 设置行宽度为100%
          .height(30) // 设置行高度为30
          .margin({ bottom: 15, top: 10 }) // 设置行的上下边距
          .justifyContent(FlexAlign.Center) // 设置内容居中对齐
          // 在Row下创建一个Text组件,用于显示分隔线
          Text() {
          } 
          .width('100%') // 设置分隔线的宽度为100%
          .height(2) // 设置分隔线的高度
          .backgroundColor('#CCCCCC') // 设置分隔线的背景颜色
          .margin({ bottom: 15 }) // 设置分隔线的下边距
          
          // 以下是创建多个按钮的代码,每个按钮对应一个图像类别,点击时更新Webview中的图像列表
          
          // 创建一个“动物”按钮
          Button({ type: ButtonType.Normal }) {
            Text('动物')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("animals",5)`);
          })
          // 创建一个“人物”按钮
          Button({ type: ButtonType.Normal }) {
            Text('人物')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("persons",5)`);
          })
          // 创建一个“美食”按钮
          Button({ type: ButtonType.Normal }) {
            Text('美食')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("finefoods",5)`);
          })
          // 创建一个“水果”按钮
          Button({ type: ButtonType.Normal }) {
            Text('水果')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("fruits",5)`);
          })
          // 创建一个“建筑”按钮
          Button({ type: ButtonType.Normal }) {
            Text('建筑')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("buildings",5)`);
          })
          // 创建一个“交通工具”按钮
          Button({ type: ButtonType.Normal }) {
            Text('交通工具')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("vehicles",5)`);
          })
          // 创建一个“星辰大海”按钮
          Button({ type: ButtonType.Normal }) {
            Text('星辰大海')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("stars",5)`);
          })
          // 创建一个“植物”按钮
          Button({ type: ButtonType
.Normal }) {
            Text('植物')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("plants",5)`);
          })
          // 创建一个“基础图形”按钮
          Button({ type: ButtonType.Normal }) {
            Text('基础图形')
              .fontSize(16)
              .fontColor('#FFFFFF')
              .width('100%')
              .height('100%')
              .textAlign(TextAlign.Center)
          }
          .borderRadius(5)
          .width('100%')
          .height(30)
          .margin({ bottom: 10 })
          .onClick(() => {
            this.controller1.runJavaScript(`updateImageList("basics",5)`);
          })
        }
        .backgroundColor(PANEL_BACKGROUND_COLOR) // 设置内嵌Column的背景颜色
        .border({ width: 0, color: '#000000' }) // 设置边框宽度和颜色
        .height('100%') // 设置高度为100%
        .padding({ left: 10, right: 10 }) // 设置左右内边距
        .width(100) // 设置宽度为100
        
        // 创建另一个Column组件用于放置Webview
        Column() {
          // 创建一个Webview组件,用于展示图像列表
          Web({ src: $rawfile('photo_studio/system_image_list.html'), controller: this.controller1 })
            .size({ width: '100%', height: '100%' }) // 设置Webview的大小为100%
        }
        .width('100%') // 设置宽度为100%
        .height('100%') // 设置高度为100%
      }
      .width('100%') // 设置外部Row的宽度为100%
      .height('100%') // 设置外部Row的高度为100%
    }
    .width('100%') // 设置最外层Column的宽度为100%
    .height('100%') // 设置最外层Column的高度为100%
  }
}
代码的原理和实现过程
这个组件的主要功能是提供一个界面,让用户可以从多个预定义的图像类别中选择图像,并且通过与Webview的交互来展示不同类别的图像列表。
- 
组件的结构:
- 使用ArkTS的UI组件(如
Column、Row、Button等)来构建界面,包含一系列按钮和一个Webview。 - 每个按钮对应不同的图像类别,点击按钮时,通过
WebviewController来执行JavaScript代码,以更新Webview中的图像列表。 
 - 使用ArkTS的UI组件(如
 - 
事件处理:
- 使用
onClick事件处理按钮点击,当用户点击某个类别按钮时,WebviewController会运行对应的JavaScript函数,更新Webview中的内容。 - 通过
imageSelected和imageListClosed这两个可选的回调函数,组件可以在外部处理图像选择和列表关闭的逻辑。 
 - 使用
 - 
Webview交互:
WebviewController的runJavaScript方法用于在Webview中执行JavaScript代码,组件通过这种方式与Webview进行通信。
 - 
UI设计:
- 界面布局通过多个
Column和Row组件实现,按钮的样式和布局使用了ArkTS的样式属性(如borderRadius、backgroundColor、margin等)。 - Webview组件显示图像列表的HTML页面,页面路径通过
src属性指定。 
 - 界面布局通过多个
 
这段代码展示了如何在ArkTS中构建一个复杂的UI组件,并结合Webview进行灵活的内容展示与交互。
在选择svg时,会调用imageSelected事件函数,该函数的代码如下:
imageSelected(imageListName: string, imageListIndex: number)
{  
     this.controller1.runJavaScript(`addSVGImage("${imageListName}",${imageListIndex});`).then(data => {
     
    });
   
    this.toggleInsertImagesPanel();
  }
}
在上面的代码中,调用了用js实现的addSVGImage函数,这个函数是根据svg图像的索引将svg图像插入到画布中,代码如下:
function addSVGImage(imageList, index) {
    try {
        // 使用fabric.loadSVGFromString来加载SVG代码
        fabric.loadSVGFromString(window[imageList][index], function (objects, options) {
            var svg = fabric.util.groupSVGElements(objects, options);
            // 计算尺寸缩放比例以适应预定尺寸
            const scale = Math.min(
                150 / svg.width,
                150 / svg.height
            );
            svg.scale(scale).set({
                left: 20,
                top: 20
            });
            // 将SVG图像添加到canvas
            canvas.add(svg);
            canvas.renderAll();
        });
    } catch(e) {
    }
}
上面的代码使用了loadSVGFromString函数装载SVG图像,下面是对这个函数的详细解释。
fabric.loadSVGFromString 函数的原型和用法详解
fabric.loadSVGFromString 是 Fabric.js 库中用于从 SVG 字符串加载 SVG 图像的函数。Fabric.js 是一个强大的 JavaScript 库,用于在 HTML5 Canvas 上轻松绘制复杂的矢量图形和图片。
原型
fabric.loadSVGFromString(
  svgString: string,
  callback: (objects: fabric.Object[], options: object) => void,
  reviver?: (element: SVGElement, object: fabric.Object) => void
): void
参数解释
- 
svgString: string
- 描述: SVG 图像的字符串形式。这个字符串包含了完整的 SVG 定义,例如 
<svg>标签及其内容。 - 类型: 
string - 示例:
const svgString = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /> </svg>`; 
 - 描述: SVG 图像的字符串形式。这个字符串包含了完整的 SVG 定义,例如 
 - 
callback: (objects: fabric.Object[], options: object) => void
- 描述: 一个回调函数,在 SVG 解析完成后调用。这个回调函数接收两个参数:
objects: Fabric.js 对象数组,每个对象代表解析后的 SVG 元素(例如,fabric.Circle,fabric.Rect等)。options: 包含一些额外的选项,如 SVG 的width和height。
 - 类型: 
function - 示例:
function callback(objects, options) { const svgGroup = new fabric.Group(objects); canvas.add(svgGroup); canvas.renderAll(); } 
 - 描述: 一个回调函数,在 SVG 解析完成后调用。这个回调函数接收两个参数:
 - 
reviver: (element: SVGElement, object: fabric.Object) => void (可选)
- 描述: 一个可选的“复活器”函数,它在每个 SVG 元素被解析成 
fabric.Object对象之前调用。通过这个函数,你可以自定义每个对象的属性或行为。 - 类型: 
function - 示例:
function reviver(element, object) { if (object.type === 'circle') { object.set({ fill: 'blue' }); } } 
 - 描述: 一个可选的“复活器”函数,它在每个 SVG 元素被解析成 
 
返回值
- 返回值类型: 
void - 描述: 该函数没有返回值,而是通过回调函数来处理解析后的对象和选项。
 
用法示例
以下是一个完整的例子,演示如何使用 fabric.loadSVGFromString 加载 SVG 字符串,并将其添加到 Fabric.js 的画布中。
// 创建一个 Fabric.js 画布
const canvas = new fabric.Canvas('canvas');
// SVG 字符串
const svgString = `
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
    <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
  </svg>`;
// 加载 SVG 字符串
fabric.loadSVGFromString(svgString, function(objects, options) {
  // 创建一个组(Group),将解析出的对象添加到组中
  const svgGroup = new fabric.Group(objects);
  
  // 添加组到画布上
  canvas.add(svgGroup);
  
  // 渲染画布
  canvas.renderAll();
});
详细说明
- SVG 解析: 
fabric.loadSVGFromString函数内部使用浏览器的原生 SVG 解析能力,解析传入的 SVG 字符串,并将其转化为 Fabric.js 可以理解的对象模型。 - 异步操作: 这个函数是异步的,它不会立即返回解析结果,而是在完成解析后调用提供的回调函数。这意味着在解析过程中,主线程不会被阻塞,确保应用程序的响应性。
 - 对象创建: 解析后的 SVG 元素被转化为 Fabric.js 的对象,如 
fabric.Circle,fabric.Rect,fabric.Path等。这些对象可以像其他 Fabric.js 对象一样进行操作和管理。 - 组管理: 解析出的对象通常会被分组(使用 
fabric.Group),这样可以作为一个整体来管理。通过对组进行操作,可以轻松移动、缩放、旋转整个 SVG 图像。 
使用场景
- 动态加载 SVG: 你可以从服务器或用户输入动态加载 SVG 内容,并将其显示在 Fabric.js 的画布上。
 - 编辑 SVG: 解析后的 Fabric.js 对象可以直接编辑,允许你在应用程序中提供 SVG 图像的编辑功能。
 - 导入外部图形: 将外部的矢量图形导入到你的 Fabric.js 画布中,以便进行进一步的操作或展示。
 
通过 fabric.loadSVGFromString,你可以轻松地将 SVG 数据集成到 Fabric.js 的绘图环境中,为创建复杂、可交互的矢量图形应用提供了便利。




















