HarmonyOS NEXT实战:手把手打造一款吉他和弦查询器

从指板绘制到变调夹计算,带你用 Canvas 和 ArkTS 实现一个吉他手必备工具


前言

作为一名吉他爱好者,你是否遇到过这些场景?

  • 练新歌时看不懂和弦图,百度半天找不到清晰的指法
  • 变调夹上手之后,不知道夹在第几品该按什么和弦
  • 想快速查一个和弦的指法,手机里却只有几张模糊的截图

作为一个开发者,与其被这些问题困扰,不如自己动手写一个。本文带你用 HarmonyOS NEXT 从零开发一款「吉他和弦查询器」,涵盖和弦库构建、Canvas 指板绘制、分类搜索、变调夹计算等核心技术点。

项目亮点:

  • 内置 60+ 常用和弦数据,覆盖 9 大类和弦
  • Canvas 绘制的吉他指板图,弦线粗细、圆点、横按一应俱全
  • 分类筛选 + 模糊搜索,快速定位和弦
  • 变调夹计算器,实时计算移调结果
  • 和弦详情页,展示指板图、指法说明、组成音、同类型推荐

一、项目架构

1.1 功能模块

首页(Index)
├── 分类标签栏(大三/小三/属七... 横向滚动)
├── 搜索栏(弦名模糊搜索)
├── 和弦网格列表
│   └── 点击进入详情页
├── 和弦详情页(ChordDetail)
│   ├── 指板图(Canvas 绘制)
│   ├── 指法说明卡片
│   ├── 组成音展示
│   └── 同类型推荐
└── 变调夹计算器(CapoTool)
    ├── 目标音选择
    ├── 品格选择器
    └── 计算结果 + 指法图

1.2 目录结构

entry/src/main/ets/
├── components/
│   └── FretboardView.ets    # 吉他指板 Canvas 绘制组件
├── model/
│   ├── ChordData.ets        # 数据类型定义 + 常量
│   └── ChordLibrary.ets     # 和弦库数据 + 查询函数
└── pages/
    ├── Index.ets             # 首页(和弦列表)
    ├── ChordDetail.ets       # 和弦详情页
    └── CapoTool.ets          # 变调夹计算器

二、数据模型设计

2.1 和弦数据结构

一个吉他和弦需要用 6 根弦的品格位置和手指编号来描述:

指板排列:[E6, A5, D4, G3, B2, e1]
         从6弦(粗) → 1弦(细)
export enum ChordType {
  MAJOR = 'major',
  MINOR = 'minor',
  SEVENTH = '7th',
  MAJ7 = 'maj7',
  MIN7 = 'm7',
  SUS = 'sus',
  DIM = 'dim',
  AUG = 'aug',
  POWER = 'power'
}

export enum ChordCategory {
  OPEN = '开放和弦',
  BARRE = '大横按和弦',
  JAZZ = '爵士和弦'
}

export interface BarreInfo {
  fret: number;          // 横按品格
  startString: number;   // 起始弦号
  endString: number;     // 结束弦号
}

export interface Chord {
  name: string;                // 和弦名称,如 "C"、"Am"、"G7"
  type: ChordType;             // 和弦类型
  category: ChordCategory;     // 分类
  frets: number[];             // 6根弦的品格: -1=不弹, 0=空弦, 1+=品格
  fingers: number[];           // 6根弦的手指: 0=不按, 1=食指, 2=中指, ...
  barre?: BarreInfo;           // 横按信息(可选)
  notes?: string[];            // 每根弦的音名
  description?: string;        // 指法说明文字
  groupTag?: string;           // CAGED 分组标签
}

2.2 和弦类型标签

export const CHORD_TYPE_LABELS: Record<string, string> = {
  [ChordType.MAJOR]: '大三和弦',
  [ChordType.MINOR]: '小三和弦',
  [ChordType.SEVENTH]: '属七和弦',
  [ChordType.MAJ7]: '大七和弦',
  [ChordType.MIN7]: '小七和弦',
  [ChordType.SUS]: '挂留和弦',
  [ChordType.DIM]: '减和弦',
  [ChordType.AUG]: '增和弦',
  [ChordType.POWER]: '强力和弦'
};

三、和弦库构建

3.1 工厂函数

使用工厂函数统一创建和弦对象,避免类型推断问题:

function chord(
  name: string, type: ChordType, category: ChordCategory,
  frets: number[], fingers: number[],
  description: string,
  notes?: string[],
  barre?: BarreInfo
): Chord {
  const r: Chord = { name, type, category, frets, fingers, description };
  if (notes !== undefined) { r.notes = notes; }
  if (barre !== undefined) { r.barre = barre; }
  return r;
}

3.2 开放和弦示例

// C 和弦:6弦不弹,5弦3品,4弦2品,3弦空弦,2弦1品,1弦空弦
chord('C', ChordType.MAJOR, ChordCategory.OPEN,
  [-1, 3, 2, 0, 1, 0],          // frets
  [0, 3, 2, 0, 1, 0],           // fingers
  '无名指5弦3品 · 中指4弦2品 · 食指2弦1品',
  ['-', 'C', 'E', 'G', 'C', 'E']),

// Am 和弦
chord('Am', ChordType.MINOR, ChordCategory.OPEN,
  [-1, 0, 2, 2, 1, 0],
  [0, 0, 2, 3, 1, 0],
  '中指4弦2品 · 无名指3弦2品 · 食指2弦1品',
  ['-', 'A', 'E', 'A', 'C', 'E']),

// G7 和弦(带横按)
chord('F', ChordType.MAJOR, ChordCategory.BARRE,
  [1, 1, 2, 3, 3, 1],
  [1, 1, 2, 3, 4, 1],
  '食指横按1品 · 中指3弦2品 · 无名指4弦3品 · 小指5弦3品',
  ['F', 'C', 'F', 'A', 'C', 'F'],
  { fret: 1, startString: 6, endString: 1 }),

3.3 完整和弦库

和弦库按类型分为 9 个分组:

分组 数量 示例
大三和弦 (Major) 7 C, D, E, F, G, A, B
小三和弦 (Minor) 7 Am, Dm, Em, Bm, Fm, Gm, Cm
属七和弦 (7th) 7 C7, D7, E7, G7, A7, B7, F7
大七和弦 (maj7) 5 Cmaj7, Dmaj7, Emaj7, Amaj7, Gmaj7
小七和弦 (m7) 5 Am7, Dm7, Em7, F#m7, Gm7
挂留和弦 (Sus) 8 Csus2, Csus4, Dsus2, Dsus4, Asus2…
减和弦 (Dim) 4 Bdim, Ddim, G#dim, Edim
增和弦 (Aug) 3 Caug, Eaug, Gaup
强力和弦 (Power) 6 C5, D5, E5, G5, A5, F5

3.4 搜索与查询

/** 获取所有和弦(扁平列表) */
export function getAllChords(): Chord[] {
  const result: Chord[] = [];
  for (const group of ALL_GROUPS) {
    for (const chord of group.chords) {
      result.push(chord);
    }
  }
  return result;
}

/** 按名称搜索和弦 */
export function searchChords(query: string): Chord[] {
  const q = query.toLowerCase().trim();
  if (!q) return [];
  const all = getAllChords();
  return all.filter(chord =>
    chord.name.toLowerCase().includes(q) ||
    (chord.description?.toLowerCase().includes(q))
  );
}

四、首页:和弦分类列表

4.1 分类标签栏

使用横向滚动的 Scroll 实现可滑动的分类按钮:

// 分类标签(横向滚动)
Scroll() {
  Row() {
    // "全部"按钮
    Text('All')
      .fontColor(this.currentCategory === -1 ? '#FFF' : '#555')
      .backgroundColor(this.currentCategory === -1 ? '#D14334' : '#EEE')
      .borderRadius(16)
      .onClick(() => { this.currentCategory = -1; this.updateDisplay(); })

    ForEach(this.chordGroups, (group: ChordGroup, index: number) => {
      Text(group.title.split(' ')[0])  // 只取英文名
        .fontColor(this.currentCategory === index ? '#FFF' : '#555')
        .backgroundColor(this.currentCategory === index ? '#2B5F8A' : '#EEE')
        .borderRadius(16)
        .padding({ left: 14, right: 14, top: 6, bottom: 6 })
        .margin({ right: 8 })
        .onClick(() => {
          this.currentCategory = index;
          this.searchQuery = '';
          this.updateDisplay();
        })
    })
  }
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)

4.2 搜索栏

搜索框点击后展开完整搜索模式:

Row() {
  if (this.showSearch) {
    TextInput({ placeholder: '搜索和弦名称 (C, Am, G7...)', text: this.searchQuery })
      .layoutWeight(1)
      .height(40)
      .borderRadius(20)
      .onChange((val: string) => {
        this.searchQuery = val;
        this.updateDisplay();
      })
    Text('取消')
      .onClick(() => {
        this.showSearch = false;
        this.searchQuery = '';
        this.updateDisplay();
      })
  } else {
    TextInput({ placeholder: '搜索和弦...', text: this.searchQuery })
      .layoutWeight(1)
      .borderRadius(20)
      .onClick(() => { this.showSearch = true; })
  }
}

4.3 和弦卡片

每个和弦卡片显示:和弦名称、类型标签、品格缩略预览:

@Builder
ChordCard(item: Chord) {
  Row() {
    // 和弦名称(大号)
    Column() {
      Text(item.name).fontSize(22).fontWeight(FontWeight.Bold)
      Text(CHORD_TYPE_LABELS[item.type] ?? '').fontSize(11).fontColor('#888')
    }
    .width(72)
    .alignItems(HorizontalAlign.Center)

    // 简要指法预览(用 O/X/数字缩略显示品格位置)
    Column() {
      Row() {
        ForEach(item.frets, (fret: number, idx: number) => {
          Text(this.fretSymbol(fret))
            .fontSize(11)
            .fontColor(this.fretColor(fret))
            .width(20)
        })
      }
      Text(item.description ?? '').fontSize(10).maxLines(1)
    }
    .layoutWeight(1)

    Text('›').fontSize(22).fontColor('#CCC')
  }
  .backgroundColor('#FFF')
  .borderRadius(12)
  .onClick(() => {
    router.pushUrl({
      url: 'pages/ChordDetail',
      params: { chordName: item.name, chordType: item.type }
    });
  })
}

4.4 底部导航

Row() {
  // 和弦查询 Tab
  Column() {
    Text('🎸').fontSize(20)
    Text('和弦查询').fontSize(11).fontColor('#D14334')
  }
  .layoutWeight(1)

  // 变调夹计算 Tab
  Column() {
    Text('🎛️').fontSize(20)
    Text('变调夹计算').fontSize(11).fontColor('#888')
  }
  .layoutWeight(1)
  .onClick(() => {
    router.pushUrl({ url: 'pages/CapoTool' });
  })
}
.width('100%')
.height(56)
.backgroundColor('#FFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.shadow({ radius: 8, color: '#10000000', offsetY: -2 })

五、核心挑战:Canvas 指板图绘制

这是本应用的技术亮点——使用 Canvas 2D API 绘制吉他指板图。

5.1 布局计算

@Component
export struct FretboardView {
  @Prop chord: Chord;
  @Prop startFret: number = 1;

  // 布局常量
  private readonly PADDING_LEFT: number = 36;
  private readonly PADDING_RIGHT: number = 16;
  private readonly PADDING_TOP: number = 42;
  private readonly PADDING_BOTTOM: number = 20;
  private readonly NUT_WIDTH: number = 6;

  // 颜色常量
  private readonly COLOR_STRINGS: string = '#999999';
  private readonly COLOR_FRET: string = '#888888';
  private readonly COLOR_DOT_FILL: string = '#2B5F8A';
  private readonly COLOR_DOT_ROOT: string = '#D14334';

  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D();

关键布局计算:

stringSpacing = 画布可用宽度 / 5
fretSpacing   = 画布可用高度 / 5

弦线从6弦(粗)到1弦(细):线宽 [3.2, 2.8, 2.4, 2.0, 1.6, 1.2]
品丝从0(上枕)到5品共6条横线

5.2 绘制步骤

完整的绘制流程分为 7 步:

drawFretboard(): void {
  const ctx = this.context;
  const w = ctx.width;
  const h = ctx.height;

  // 1. 清空 + 背景
  ctx.clearRect(0, 0, w, h);
  this.roundRect(ctx, 0, 0, w, h, 12);
  ctx.fillStyle = '#FFF9F0';
  ctx.fill();

  // 2. 绘制品丝(横线)
  for (let f = 0; f <= 5; f++) {
    const y = pt + f * fretSpacing;
    ctx.moveTo(pl, y);
    ctx.lineTo(pl + drawW, y);
  }

  // 3. 绘制上枕(第一根横线加粗)
  ctx.lineWidth = this.NUT_WIDTH;  // 6px 粗线
  ctx.moveTo(pl, pt);
  ctx.lineTo(pl + drawW, pt);

  // 4. 绘制弦线(竖线,粗细不同)
  const stringWidths = [3.2, 2.8, 2.4, 2.0, 1.6, 1.2];
  for (let s = 0; s < 6; s++) {
    ctx.lineWidth = stringWidths[s];
    ctx.moveTo(x, pt);
    ctx.lineTo(x, pt + drawH);
  }

  // 5. 绘制手指圆点
  for (let s = 0; s < 6; s++) {
    if (fret <= 0) continue;  // 跳过空弦和不弹
    ctx.arc(x, y, dotRadius, 0, Math.PI * 2);
    ctx.fillStyle = isRoot ? '#D14334' : '#2B5F8A';
    ctx.fill();
    
    // 指法编号 1~4
    ctx.fillStyle = '#FFF';
    ctx.fillText(fingers[s].toString(), x, y);
  }

  // 6. 绘制横按指示(圆弧矩形 + 数字1)
  if (chord.barre) {
    this.roundRect(ctx, xStart - r, yBarre - r, width, r * 2, r);
    ctx.fillStyle = '#2B5F8A';
    ctx.fill();
    ctx.fillStyle = '#FFF';
    ctx.fillText('1', (xStart + xEnd) / 2, yBarre);
  }

  // 7. 绘制空弦O / 不弹X + 音名
  for (let s = 0; s < 6; s++) {
    if (frets[s] === 0) {
      ctx.arc(x, y, 7, 0, Math.PI * 2);
      ctx.strokeStyle = '#2B5F8A';  // 空心圆 O
    } else if (frets[s] === -1) {
      ctx.moveTo(x-5, y-5); ctx.lineTo(x+5, y+5);  // 打叉 X
      ctx.moveTo(x+5, y-5); ctx.lineTo(x-5, y+5);
    }
  }
}

5.3 圆角矩形工具函数

ArkUI 的 Canvas API 没有内置 roundRect,需要自己实现:

roundRect(ctx: CanvasRenderingContext2D, x: number, y: number,
          w: number, h: number, r: number): void {
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

5.4 注意事项

踩坑记录:

  1. Canvas 必须在 onReady() 回调中绘制,此时 ctx.width/height 才有效
  2. 尺寸变化时要重绘,绑定 onAreaChange 事件
  3. 使用 setTimeout(fn, 50) 延迟重绘,确保布局已刷新
Canvas(this.context)
  .width('100%')
  .aspectRatio(1.0)
  .constraintSize({ maxHeight: 340 })
  .backgroundColor('#FFF9F0')
  .borderRadius(12)
  .onReady(() => {
    this.drawFretboard();
  })
  .onAreaChange(() => {
    setTimeout(() => {
      this.drawFretboard();
    }, 50);
  });

六、和弦详情页

6.1 参数传递

使用 router.pushUrl 传递和弦名称和类型:

router.pushUrl({
  url: 'pages/ChordDetail',
  params: {
    chordName: item.name,
    chordType: item.type
  }
});

6.2 和弦信息展示

详情页包含 5 个区域:

Scroll() {
  Column() {
    // 1. 和弦名称 + 类型标签
    Row() {
      Text(this.chord.name).fontSize(36)
      Text(CHORD_TYPE_LABELS[this.chord.type] ?? '')
        .backgroundColor(分类对应颜色)
        .borderRadius(10)
    }

    // 2. 指板图
    FretboardView({ chord: this.chord, startFret: ... })

    // 3. 指法说明卡片
    Column() {
      Text('🎯 指法说明')
      Text(this.chord.description ?? '')
      if (this.chord.barre) {
        Text(`横按: 食指按第${barre.fret}`)
      }
      // 指法表
      Row() {
        ForEach(this.chord.frets, (fret, idx) => {
          Text(['6弦','5弦','4弦','3弦','2弦','1弦'][idx])
          Text(fret符号)
        })
      }
    }

    // 4. 组成音展示
    Column() {
      Text('🎵 组成音')
      ForEach(this.chord.notes, (note) => {
        Text(note).backgroundColor('#EBF2FA')
      })
    }

    // 5. 同类型推荐
    Column() {
      Text('📋 其他' + 类型名)
      Scroll() {
        Row() {
          ForEach(this.relatedChords, (related: Chord) => {
            Column() {
              Text(related.name).fontSize(18)
              Text(简写标签).fontSize(10)
            }
            .onClick(() => { /* 跳转到选中和弦 */ })
          })
        }
      }
    }
  }
}

6.3 同类型推荐

// 查找同类型其他和弦
this.relatedChords = all.filter(c =>
  c.type === this.chord?.type && c.name !== this.chord?.name
).slice(0, 6);

七、变调夹计算器

7.1 变调夹原理

变调夹是吉他手常用的工具——夹在第 N 品时,原本按 C 和弦的指法,实际发出的声音升高了 N 个半音。

计算公式:

实际弹奏和弦 = 目标音和弦的根音 - N 个半音
示例:想弹出 C 大调的声音,夹 3 品 → 实际弹 A 和弦
因为 C - 3半音 = A
export function applyCapo(chordName: string, capoFret: number): string {
  const noteNames: string[] = [
    'C', 'C#', 'D', 'D#', 'E', 'F',
    'F#', 'G', 'G#', 'A', 'A#', 'B'
  ];

  // 分离根音和后缀(如 "Cmaj7" → "C" + "maj7")
  const rootMatch = chordName.match(/^([A-G][#b]?)(.*)$/);
  if (rootMatch === null) return chordName;

  let root = rootMatch[1];
  const suffix = rootMatch[2];

  // 降号转升号
  if (root.includes('b')) {
    const flatMap: Record<string, string> = {
      'Bb': 'A#', 'Db': 'C#', 'Eb': 'D#', 'Gb': 'F#', 'Ab': 'G#'
    };
    root = flatMap[root] ?? root;
  }

  const rootIndex = noteNames.indexOf(root);
  if (rootIndex === -1) return chordName;

  // 根音向后移动 N 个半音
  const actualIndex = (rootIndex - capoFret + 12) % 12;
  return noteNames[actualIndex] + suffix;
}

7.2 交互设计

@Component
struct CapoTool {
  @State selectedRoot: string = 'C';
  @State selectedType: ChordType = ChordType.MAJOR;
  @State capoFret: number = 3;
  @State resultChordName: string = '';
  @State resultChord: Chord | null = null;

  calculateResult(): void {
    const fullName = this.selectedRoot + CHORD_SHORT_SUFFIX(this.selectedType);
    this.resultChordName = applyCapo(fullName, this.capoFret);
    // 在库中查找计算结果
    this.resultChord = all.find(c => c.name === this.resultChordName);
  }
}

7.3 UI 布局

变调夹计算器分三个区域:

Scroll() {
  Column() {
    // Section 1: 目标音和弦(根音选择 + 类型选择)
    Column() {
      Text('🎯 目标音和弦(你想听到的声音)')
      
      // 根音选择:C, C#, D... 共12个圆形按钮
      Scroll() {
        Row() {
          ForEach(this.rootNotes, (note: string) => {
            Text(note)
              .width(40).height(40).borderRadius(20)
              .backgroundColor(note === this.selectedRoot ? '#D14334' : '#EEE')
              .onClick(() => { this.onRootChange(note); })
          })
        }
      }

      // 类型选择:Major / Minor / 7th...
      Row() {
        ForEach(this.chordTypeList, (item: ChordTypeItem) => {
          Text(item.label)
            .backgroundColor(item.type === this.selectedType ? '#2B5F8A' : '#EEE')
            .borderRadius(18)
            .onClick(() => { this.onTypeChange(item.type); })
        })
      }
    }

    // Section 2: 变调夹位置(1-12品网格)
    Column() {
      Text('📌 变调夹位置')
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach([1..12], (fret: number) => {
          Text(fret.toString())
            .backgroundColor(fret === this.capoFret ? '#D14334' : '#F5F5F5')
            .borderRadius(6)
            .onClick(() => { this.onCapoChange(fret); })
        })
      }
    }

    // Section 3: 计算结果 + 指法图
    Column() {
      Text('✅ 计算结果')
      // 公式展示
      Text(`${C} → 夹 3 品 → 弹 A`)
      
      // 如果有对应的和弦指法图
      if (this.resultChord) {
        FretboardView({ chord: this.resultChord, startFret: ... })
      }
    }
  }
}

八、页面路由与导航

8.1 页面注册

main_pages.json 中注册所有页面:

{
  "src": [
    "pages/Index",
    "pages/ChordDetail",
    "pages/CapoTool"
  ]
}

8.2 路由跳转

import { router } from '@kit.ArkUI';

// 跳转到详情页(带参数)
router.pushUrl({
  url: 'pages/ChordDetail',
  params: {
    chordName: item.name,
    chordType: item.type
  }
});

// 在详情页接收参数
aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  this.chordName = params['chordName'] as string;
  this.chordType = params['chordType'] as ChordType;
}

// 返回上一页
router.back();

九、项目配置

9.1 app.json5

{
  "app": {
    "bundleName": "com.example.chordfinder",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:layered_image",
    "label": "$string:app_name"
  }
}

9.2 module.json5

{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": ["phone"],
    "pages": "$profile:main_pages",
    "abilities": [{
      "name": "EntryAbility",
      "exported": true,
      "skills": [{
        "actions": ["ohos.want.action.home"],
        "entities": ["entity.system.home"]
      }]
    }]
  }
}

十、关键技术总结

10.1 Canvas 指板绘制

要素 实现方式
6根弦线 竖直线,宽度递减 [3.2, 2.8, 2.4, 2.0, 1.6, 1.2]
品丝 横线,第一根加粗作为上枕
手指圆点 arc() 绘制实心圆,根音红色/其他蓝色
指法编号 fillText() 在圆点中心显示 1~4
横按指示 弧线矩形 + “1” 字
空弦/不弹标记 空心圆 O / 打叉 X

10.2 和弦数据建模

frets: number[]    // 6个数字,每个代表品格位置
fingers: number[]  // 6个数字,每个代表手指编号
barre?: BarreInfo  // { fret, startString, endString }

-1 = 不弹该弦
 0 = 空弦
1+ = 按在第 N 品

10.3 变调夹算法

目标和弦根音 - 变调夹品格 = 实际弹奏根音

降号转升号处理确保索引查找正确。

10.4 搜索实现

模糊搜索支持:

  • 按和弦名称(C、Am、G7…)
  • 按指法描述关键词

十一、运行效果展示

在这里插入图片描述
在这里插入图片描述


十二、扩展方向

  1. 音频播放:点击和弦播放对应声音
  2. 指法动画:逐弦演示按弦顺序
  3. 更多和弦:扩展爵士和弦、九和弦、十一和弦
  4. 曲谱模式:显示常用歌曲的和弦进行
  5. 调音器:内置半音阶调音功能
  6. 自定义和弦:用户在指板上点击创建自定义和弦
  7. CAGED 系统:可视化展示 CAGED 系统五种指型

结语

这个项目涵盖了 HarmonyOS 开发的多个核心技术点:

  • Canvas 2D API:指板图绘制(圆形、圆弧、文字、粗细线条)
  • 数据建模:和弦数据结构的抽象与工厂函数
  • 状态管理:@State、@Prop、@Watch 的响应式更新
  • 路由导航:router.pushUrl 参数传递与 router.back
  • 手势交互:点击跳转、搜索输入、分类筛选
  • 算法实现:变调夹移调计算

对于吉他手来说,这是一个实用的日常工具;对于开发者来说,这是一个完美的 Canvas 绘图入门项目。代码已在文中完整呈现,遇到问题欢迎交流!


技术栈:HarmonyOS NEXT + ArkTS + Canvas 2D
开发环境:DevEco Studio 5.0+ / SDK API 23
和弦数量:60+(9大类)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐