鴻蒙NEXT開發案例:二維碼的生成與識別

zhongcx發表於2024-11-24

【引言】

在本篇文章中,我們將探討如何在鴻蒙NEXT平臺上實現二維碼的生成與識別功能。透過使用ArkUI元件庫和相關的媒體庫,我們將建立一個簡單的應用程式,使用者可以生成二維碼並掃描識別。

【環境準備】

• 作業系統:Windows 10

• 開發工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806

• 目標裝置:華為Mate60 Pro

• 開發語言:ArkTS

• 框架:ArkUI

• API版本:API 12

• 許可權:ohos.permission.WRITE_IMAGEVIDEO(為實現將圖片儲存至相簿功能)

【專案介紹】

1. 專案結構

我們首先定義一個名為QrCodeGeneratorAndScanner的元件,使用@Component裝飾器進行標記。該元件包含多個狀態變數和方法,用於處理二維碼的生成、識別和剪貼簿操作。

2. 元件狀態

元件的狀態包括:

  • buttonOptions: 定義分段按鈕的選項,用於切換生成和識別二維碼的功能。
  • inputText: 使用者輸入的文字,用於生成二維碼。
  • scanResult: 掃描結果文字。
  • scanResultObject: 儲存掃描結果的物件。

3. 使用者介面構建

build方法中,我們使用ColumnRow佈局來構建使用者介面。主要包含以下部分:

  • 分段按鈕:使用者可以選擇生成二維碼或識別二維碼。
  • 輸入區域:使用者可以輸入文字並生成二維碼。
  • 二維碼顯示:根據輸入文字生成二維碼。
  • 掃描區域:使用者可以透過相機掃描二維碼或從相簿選擇圖片進行識別。

4. 二維碼生成

二維碼生成使用QRCode元件,輸入文字透過this.inputText傳遞。使用者輸入後,二維碼會實時更新。

5. 二維碼識別

二維碼識別功能透過scanBarcode模組實現。使用者可以點選“掃一掃”按鈕,啟動相機進行掃描,或選擇相簿中的圖片進行識別。識別結果將顯示在介面上,並提供複製功能。

6. 剪貼簿操作

使用者可以將掃描結果複製到剪貼簿,使用pasteboard模組實現。點選“複製”按鈕後,掃描結果將被複制,使用者會收到提示。

【完整程式碼】

填寫許可權使用宣告字串:src/main/resources/base/element/string.json

{
  "string": [
    {
      "name": "module_desc",
      "value": "module description"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "label"
    },
    {
      "name": "WRITE_IMAGEVIDEO_info",
      "value": "儲存功能需要該許可權"
    }
  ]
}

配置許可權:src/main/module.json5

{
  "module": {
    "requestPermissions": [
      {
        "name": 'ohos.permission.WRITE_IMAGEVIDEO',
        "reason": "$string:WRITE_IMAGEVIDEO_info",
        "usedScene": {

        }
      }
    ],
//... ...

示例程式碼:src/main/ets/pages/Index.ets

import {
  componentSnapshot, // 元件快照
  promptAction, // 提示操作
  SegmentButton, // 分段按鈕
  SegmentButtonItemTuple, // 分段按鈕項元組
  SegmentButtonOptions // 分段按鈕選項
} from '@kit.ArkUI'; // 引入 ArkUI 元件庫

import { photoAccessHelper } from '@kit.MediaLibraryKit'; // 引入 MediaLibraryKit 中的照片訪問助手
import { common } from '@kit.AbilityKit'; // 引入 AbilityKit 中的通用功能
import { fileIo as fs } from '@kit.CoreFileKit'; // 引入 CoreFileKit 中的檔案 I/O 模組
import { image } from '@kit.ImageKit'; // 引入 ImageKit 中的影像處理模組
import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; // 引入 BasicServicesKit 中的業務錯誤和剪貼簿操作
import { hilog } from '@kit.PerformanceAnalysisKit'; // 引入 PerformanceAnalysisKit 中的效能分析模組
import { detectBarcode, scanBarcode } from '@kit.ScanKit'; // 引入 ScanKit 中的條形碼識別模組

@Entry
  // 入口標記
@Component
  // 元件標記
struct QrCodeGeneratorAndScanner { // 定義二維碼生成與識別元件
  @State private buttonOptions: SegmentButtonOptions = SegmentButtonOptions.capsule({
    // 定義分段按鈕選項
    buttons: [{ text: '生成二維碼' }, { text: '識別二維碼' }] as SegmentButtonItemTuple, // 按鈕文字
    multiply: false, // 不允許多選
    fontColor: Color.White, // 字型顏色為白色
    selectedFontColor: Color.White, // 選中字型顏色為白色
    selectedBackgroundColor: Color.Orange, // 選中背景顏色為橙色
    backgroundColor: "#d5d5d5", // 背景顏色
    backgroundBlurStyle: BlurStyle.BACKGROUND_THICK // 背景模糊樣式
  })
  @State private sampleText: string = 'hello world'; // 示例文字
  @State private inputText: string = ""; // 輸入文字
  @State private scanResult: string = ""; // 掃描結果
  @State @Watch('selectIndexChanged') selectIndex: number = 0 // 選擇索引
  @State @Watch('selectedIndexesChanged') selectedIndexes: number[] = [0]; // 選中索引陣列
  private qrCodeId: string = "qrCodeId" // 二維碼 ID
  @State private scanResultObject: scanBarcode.ScanResult = ({} as scanBarcode.ScanResult) // 掃描結果物件
  @State private textColor: string = "#2e2e2e"; // 文字顏色
  @State private shadowColor: string = "#d5d5d5"; // 陰影顏色
  @State private basePadding: number = 30; // 基礎內邊距

  selectedIndexesChanged() { // 選中索引改變事件
    console.info(`this.selectedIndexes[0]:${this.selectedIndexes[0]}`)
    this.selectIndex = this.selectedIndexes[0]
  }

  selectIndexChanged() { // 選擇索引改變事件
    console.info(`selectIndex:${this.selectIndex}`)
    this.selectedIndexes[0] = this.selectIndex
  }

  private copyToClipboard(text: string): void { // 複製文字到剪貼簿
    const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); // 建立剪貼簿資料
    const systemPasteboard = pasteboard.getSystemPasteboard(); // 獲取系統剪貼簿
    systemPasteboard.setData(pasteboardData); // 設定資料
    promptAction.showToast({ message: '已複製' }); // 彈出提示訊息
  }

  build() { // 構建介面
    Column() { // 列布局
      SegmentButton({ // 分段按鈕
        options: this.buttonOptions, // 選項
        selectedIndexes: this.selectedIndexes // 選中索引
      }).width('400lpx').margin({ top: 20 }) // 設定寬度和外邊距
      Tabs({ index: this.selectIndex }) { // 選項卡
        TabContent() { // 選項卡內容
          Scroll() { // 滾動檢視
            Column() { // 列布局
              Column() { // 列布局
                Row() { // 行佈局
                  Text('示例') // 文字
                    .fontColor("#5871ce") // 字型顏色
                    .fontSize(16) // 字型大小
                    .padding(`${this.basePadding / 2}lpx`) // 內邊距
                    .backgroundColor("#f2f1fd") // 背景顏色
                    .borderRadius(5) // 邊框圓角
                    .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 點選效果
                    .onClick(() => { // 點選事件
                      this.inputText = this.sampleText; // 設定輸入文字為示例文字
                    });
                  Blank(); // 空白佔位
                  Text('清空') // 清空按鈕
                    .fontColor("#e48742") // 字型顏色
                    .fontSize(16) // 字型大小
                    .padding(`${this.basePadding / 2}lpx`) // 內邊距
                    .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 點選效果
                    .backgroundColor("#ffefe6") // 背景顏色
                    .borderRadius(5) // 邊框圓角
                    .onClick(() => { // 點選事件
                      this.inputText = ""; // 清空輸入文字
                    });
                }
                .height(45) // 設定高度
                .justifyContent(FlexAlign.SpaceBetween) // 主軸對齊方式
                .width('100%'); // 設定寬度

                Divider(); // 分隔線
                TextArea({ text: $$this.inputText, placeholder: `請輸入內容` }) // 文字輸入框
                  .width(`${650 - this.basePadding * 2}lpx`) // 設定寬度
                  .height(100) // 設定高度
                  .fontSize(16) // 字型大小
                  .caretColor(this.textColor) // 游標顏色
                  .fontColor(this.textColor) // 字型顏色
                  .margin({ top: `${this.basePadding}lpx` }) // 外邊距
                  .padding(0) // 內邊距
                  .backgroundColor(Color.Transparent) // 背景顏色
                  .borderRadius(0) // 邊框圓角
                  .textAlign(TextAlign.JUSTIFY); // 文字對齊方式
              }
              .alignItems(HorizontalAlign.Start) // 交叉軸對齊方式
              .width('650lpx') // 設定寬度
              .padding(`${this.basePadding}lpx`) // 內邊距
              .borderRadius(10) // 邊框圓角
              .backgroundColor(Color.White) // 背景顏色
              .shadow({ // 陰影
                radius: 10, // 陰影半徑
                color: this.shadowColor, // 陰影顏色
                offsetX: 0, // X 軸偏移
                offsetY: 0 // Y 軸偏移
              });

              Row() { // 行佈局
                QRCode(this.inputText) // 二維碼元件
                  .width('300lpx') // 設定寬度
                  .aspectRatio(1) // 設定寬高比
                  .id(this.qrCodeId) // 設定 ID
                SaveButton() // 儲存按鈕
                  .onClick(async (_event: ClickEvent, result: SaveButtonOnClickResult) => { // 點選事件
                    if (result === SaveButtonOnClickResult.SUCCESS) { // 如果儲存成功
                      const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // 獲取上下文
                      let helper = photoAccessHelper.getPhotoAccessHelper(context); // 獲取照片訪問助手
                      try { // 嘗試
                        let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 建立圖片資源
                        let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); // 開啟檔案
                        componentSnapshot.get(this.qrCodeId).then((pixelMap) => { // 獲取二維碼快照
                          let packOpts: image.PackingOption = { format: 'image/png', quality: 100 } // 打包選項
                          const imagePacker: image.ImagePacker = image.createImagePacker(); // 建立影像打包器
                          return imagePacker.packToFile(pixelMap, file.fd, packOpts).finally(() => { // 打包並儲存檔案
                            imagePacker.release(); // 釋放打包器
                            fs.close(file.fd); // 關閉檔案
                            promptAction.showToast({ // 彈出提示訊息
                              message: '圖片已儲存至相簿', // 提示內容
                              duration: 2000 // 持續時間
                            });
                          });
                        })
                      } catch (error) { // 捕獲錯誤
                        const err: BusinessError = error as BusinessError; // 轉換為業務錯誤
                        console.error(`Failed to save photo. Code is ${err.code}, message is ${err.message}`); // 列印錯誤資訊
                      }

                    } else { // 如果儲存失敗
                      promptAction.showToast({ // 彈出提示訊息
                        message: '設定許可權失敗!', // 提示內容
                        duration: 2000 // 持續時間
                      });
                    }
                  })
              }
              .visibility(this.inputText ? Visibility.Visible : Visibility.Hidden) // 根據輸入文字設定可見性
              .justifyContent(FlexAlign.SpaceBetween) // 主軸對齊方式
              .width('650lpx') // 設定寬度
              .padding(`${this.basePadding}lpx`) // 內邊距
              .margin({ top: `${this.basePadding}lpx` }) // 外邊距
              .borderRadius(10) // 邊框圓角
              .backgroundColor(Color.White) // 背景顏色
              .shadow({ // 陰影
                radius: 10, // 陰影半徑
                color: this.shadowColor, // 陰影顏色
                offsetX: 0, // X 軸偏移
                offsetY: 0 // Y 軸偏移
              });
            }.padding({ top: 20, bottom: 20 }) // 設定內邊距
            .width('100%') // 設定寬度
          }.scrollBar(BarState.Off) // 禁用捲軸
          .align(Alignment.Top) // 頂部對齊
          .height('100%') // 設定高度
        }
        TabContent() { // 第二個選項卡內容
          Scroll() { // 滾動檢視
            Column() { // 列布局
              Row() { // 行佈局
                Text('掃一掃') // 掃一掃文字
                  .fontSize(20) // 字型大小
                  .textAlign(TextAlign.Center) // 文字居中對齊
                  .fontColor("#5871ce") // 字型顏色
                  .backgroundColor("#f2f1fd") // 背景顏色
                  .clickEffect({ scale: 0.8, level: ClickEffectLevel.LIGHT }) // 點選效果
                  .borderRadius(10) // 邊框圓角
                  .height('250lpx') // 設定高度
                  .layoutWeight(1) // 佈局權重
                  .onClick(() => { // 點選事件
                    if (canIUse('SystemCapability.Multimedia.Scan.ScanBarcode')) { // 檢查是否支援掃描
                      try { // 嘗試
                        scanBarcode.startScanForResult(getContext(this), { // 開始掃描
                          enableMultiMode: true, // 啟用多模式
                          enableAlbum: true // 啟用相簿選擇
                        },
                          (error: BusinessError, result: scanBarcode.ScanResult) => { // 掃描結果回撥
                            if (error) { // 如果發生錯誤
                              hilog.error(0x0001, '[Scan CPSample]', // 記錄錯誤日誌
                                `Failed to get ScanResult by callback with options. Code: ${error.code}, message: ${error.message}`);
                              return; // 退出
                            }
                            hilog.info(0x0001, '[Scan CPSample]', // 記錄成功日誌
                              `Succeeded in getting ScanResult by callback with options, result is ${JSON.stringify(result)}`);
                            this.scanResultObject = result; // 設定掃描結果物件
                            this.scanResult = result.originalValue ? result.originalValue : '無法識別'; // 設定掃描結果文字
                          });
                      } catch (error) { // 捕獲錯誤
                        hilog.error(0x0001, '[Scan CPSample]', // 記錄錯誤日誌
                          `Failed to start the scanning service. Code:${error.code}, message: ${error.message}`);
                      }
                    } else { // 如果不支援掃描
                      promptAction.showToast({ message: '當前裝置不支援二維碼掃描' }); // 彈出提示訊息
                    }
                  });

                Line().width(`${this.basePadding}lpx`).aspectRatio(1); // 分隔線

                Text('相簿選') // 相簿選擇文字
                  .fontSize(20) // 字型大小
                  .textAlign(TextAlign.Center) // 文字居中對齊
                  .fontColor("#e48742") // 字型顏色
                  .backgroundColor("#ffefe6") // 背景顏色
                  .borderRadius(10) // 邊框圓角
                  .clickEffect({ scale: 0.8, level: ClickEffectLevel.LIGHT }) // 點選效果
                  .height('250lpx') // 設定高度
                  .layoutWeight(1) // 佈局權重
                  .onClick(() => { // 點選事件
                    if (canIUse('SystemCapability.Multimedia.Scan.ScanBarcode')) { // 檢查是否支援掃描
                      let photoOption = new photoAccessHelper.PhotoSelectOptions(); // 建立照片選擇選項
                      photoOption.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; // 設定 MIME 型別
                      photoOption.maxSelectNumber = 1; // 設定最大選擇數量
                      let photoPicker = new photoAccessHelper.PhotoViewPicker(); // 建立照片選擇器
                      photoPicker.select(photoOption).then((result) => { // 選擇照片
                        let inputImage: detectBarcode.InputImage = { uri: result.photoUris[0] }; // 獲取選中的圖片 URI
                        try { // 嘗試
                          detectBarcode.decode(inputImage, // 解碼條形碼
                            (error: BusinessError, result: Array<scanBarcode.ScanResult>) => { // 解碼結果回撥
                              if (error && error.code) { // 如果發生錯誤
                                hilog.error(0x0001, '[Scan Sample]', // 記錄錯誤日誌
                                  `Failed to get ScanResult by callback. Code: ${error.code}, message: ${error.message}`);
                                return; // 退出
                              }
                              hilog.info(0x0001, '[Scan Sample]', // 記錄成功日誌
                                `Succeeded in getting ScanResult by callback, result is ${JSON.stringify(result, null, '\u00A0\u00A0')}`);
                              if (result.length > 0) { // 如果有結果
                                this.scanResultObject = result[0]; // 設定掃描結果物件
                                this.scanResult = result[0].originalValue ? result[0].originalValue : '無法識別'; // 設定掃描結果文字
                              } else { // 如果沒有結果
                                this.scanResult = '不存在二維碼'; // 設定結果文字
                              }
                            });
                        } catch (error) { // 捕獲錯誤
                          hilog.error(0x0001, '[Scan Sample]', // 記錄錯誤日誌
                            `Failed to detect Barcode. Code: ${error.code}, message: ${error.message}`);
                        }
                      });
                    } else { // 如果不支援掃描
                      promptAction.showToast({ message: '當前裝置不支援二維碼掃描' }); // 彈出提示訊息
                    }
                  });
              }
              .justifyContent(FlexAlign.SpaceEvenly) // 主軸對齊方式
              .width('650lpx') // 設定寬度
              .padding(`${this.basePadding}lpx`) // 內邊距
              .borderRadius(10) // 邊框圓角
              .backgroundColor(Color.White) // 背景顏色
              .shadow({ // 陰影
                radius: 10, // 陰影半徑
                color: this.shadowColor, // 陰影顏色
                offsetX: 0, // X 軸偏移
                offsetY: 0 // Y 軸偏移
              });

              Column() { // 列布局
                Row() { // 行佈局
                  Text(`解析結果:\n${this.scanResult}`) // 顯示解析結果文字
                    .fontColor(this.textColor) // 設定字型顏色
                    .fontSize(18) // 設定字型大小
                    .layoutWeight(1) // 設定佈局權重
                  Text('複製') // 複製按鈕文字
                    .fontColor(Color.White) // 設定字型顏色為白色
                    .fontSize(16) // 設定字型大小
                    .padding(`${this.basePadding / 2}lpx`) // 設定內邊距
                    .backgroundColor("#0052d9") // 設定背景顏色
                    .borderRadius(5) // 設定邊框圓角
                    .clickEffect({ level: ClickEffectLevel.LIGHT, scale: 0.8 }) // 設定點選效果
                    .onClick(() => { // 點選事件
                      this.copyToClipboard(this.scanResult); // 複製掃描結果到剪貼簿
                    });
                }.constraintSize({ minHeight: 45 }) // 設定最小高度約束
                .justifyContent(FlexAlign.SpaceBetween) // 設定主軸對齊方式
                .width('100%'); // 設定寬度為100%

              }
              .visibility(this.scanResult ? Visibility.Visible : Visibility.Hidden) // 根據掃描結果設定可見性
              .alignItems(HorizontalAlign.Start) // 設定交叉軸對齊方式
              .width('650lpx') // 設定寬度
              .padding(`${this.basePadding}lpx`) // 設定內邊距
              .margin({ top: `${this.basePadding}lpx` }) // 設定外邊距
              .borderRadius(10) // 設定邊框圓角
              .backgroundColor(Color.White) // 設定背景顏色為白色
              .shadow({ // 設定陰影
                radius: 10, // 設定陰影半徑
                color: this.shadowColor, // 設定陰影顏色
                offsetX: 0, // 設定X軸偏移
                offsetY: 0 // 設定Y軸偏移
              });

              Column() { // 列布局
                Row() { // 行佈局
                  Text(`完整結果:`).fontColor(this.textColor).fontSize(18).layoutWeight(1) // 顯示完整結果文字
                }.constraintSize({ minHeight: 45 }) // 設定最小高度約束
                .justifyContent(FlexAlign.SpaceBetween) // 設定主軸對齊方式
                .width('100%'); // 設定寬度為100%

                Divider().margin({ top: 2, bottom: 15 }); // 新增分隔線並設定外邊距

                Row() { // 行佈局
                  Text(`${JSON.stringify(this.scanResultObject, null, '\u00A0\u00A0')}`) // 顯示完整掃描結果
                    .fontColor(this.textColor) // 設定字型顏色
                    .fontSize(18) // 設定字型大小
                    .layoutWeight(1) // 設定佈局權重
                    .copyOption(CopyOptions.LocalDevice); // 設定複製選項
                }.constraintSize({ minHeight: 45 }) // 設定最小高度約束
                .justifyContent(FlexAlign.SpaceBetween) // 設定主軸對齊方式
                .width('100%'); // 設定寬度為100%

              }
              .visibility(this.scanResult ? Visibility.Visible : Visibility.Hidden) // 根據掃描結果設定可見性
              .alignItems(HorizontalAlign.Start) // 設定交叉軸對齊方式
              .width('650lpx') // 設定寬度
              .padding(`${this.basePadding}lpx`) // 設定內邊距
              .margin({ top: `${this.basePadding}lpx` }) // 設定外邊距
              .borderRadius(10) // 設定邊框圓角
              .backgroundColor(Color.White) // 設定背景顏色為白色
              .shadow({ // 設定陰影
                radius: 10, // 設定陰影半徑
                color: this.shadowColor, // 設定陰影顏色
                offsetX: 0, // 設定X軸偏移
                offsetY: 0 // 設定Y軸偏移
              });

            }
            .padding({ top: 20, bottom: 20 }) // 設定內邊距
            .width('100%') // 設定寬度為100%
          }.scrollBar(BarState.Off) // 禁用捲軸
          .align(Alignment.Top) // 頂部對齊
          .height('100%') // 設定高度為100%
        }
      }
      .barHeight(0) // 設定選項卡條高度為0
      .tabIndex(this.selectIndex) // 設定當前選中的索引
      .width('100%') // 設定寬度為100%
      .layoutWeight(1) // 設定佈局權重
      .onChange((index: number) => { // 選項卡變化事件
        this.selectIndex = index; // 更新選擇的索引
      });
    }
    .height('100%') // 設定高度為100%
    .width('100%') // 設定寬度為100%
    .backgroundColor("#f4f8fb"); // 設定背景顏色
  }
}

  

相關文章