HarmonyOS NEXT 屏幕取色器设计与实现详解

HarmonyOS NEXT 屏幕取色器设计与实现详解

一、引言

在 UI 设计、前端开发和数字创意领域,从屏幕上精确提取颜色是一项基础而频繁的需求。设计师需要从参考图中获取品牌色,开发者需要还原设计稿中的色值。随着 HarmonyOS NEXT 在 PC 领域的扩展,在鸿蒙原生平台上拥有一款高性能屏幕取色工具变得日益重要。

本文详细解析了一款基于 HarmonyOS NEXT(API 23)并使用 ArkTS 开发的 PC 端屏幕取色器。该工具支持鼠标悬停实时取色、HEX / RGB / HSL 三种格式一键复制、颜色历史记录管理及像素级放大镜预览。文章涵盖架构设计、核心算法、UI 实现和 ArkTS 兼容性适配,为鸿蒙开发者提供翔实的 ArkTS 实战参考。


二、项目背景与技术栈

2.1 为什么需要原生屏幕取色器?

随着 HarmonyOS NEXT 生态在 PC 领域的拓展,越来越多的设计工具和创意应用需要在鸿蒙原生环境中运行。这些应用对色彩拾取有着天然需求。在鸿蒙原生平台上构建一款取色器,不仅填补了工具空白,也为后续创意工具生态提供了基础设施。

2.2 技术栈选型

技术维度 选型 说明
操作系统 HarmonyOS NEXT 纯血鸿蒙,自研微内核架构
API 版本 API 23(ArkTS 3.0+) 最新声明式 UI 开发体系
编程语言 ArkTS HarmonyOS 原生声明式语言
图像处理 @kit.ImageKit 提供 PixelMap、图像编解码能力
UI 框架 ArkUI(声明式) 组件化、数据驱动、类似 SwiftUI
构建工具 hvigor HAP 打包与构建

2.3 运行环境

  • 操作系统:HarmonyOS NEXT(PC 模式)
  • 最小窗口:1200 × 800 像素
  • 输入设备:鼠标(用于悬停取色交互)

三、系统架构与数据流

3.1 整体架构

工具采用主从组件 + 数据流驱动 UI 的架构模式,由三部分构成:

  1. 数据层(Model)ColorInfo 颜色模型、HslColor HSL 模型、HistoryItem 历史记录模型,以及 @State 装饰的状态变量。
  2. 业务逻辑层(Controller):截图引擎、颜色拾取算法、颜色空间转换函数、历史管理逻辑、剪贴板操作。
  3. 视图层(View) :主组件 ColorPickerTool、子组件 ColorValueRow(颜色值行)、ColorHistoryRow(历史条目)。

3.2 核心数据流

复制代码
鼠标移动 → pickColor() → 像素缓冲区索引 → 颜色分量
    → 更新 currentColor(@State) → UI 刷新 → 更新放大镜 Canvas

鼠标点击 → pickColor() → addToHistory()
    → 去重检查 → 头部插入 → 截断至24条 → UI 刷新

3.3 关键数据结构

typescript 复制代码
interface ColorInfo {
  hex: string;       // "#FF6600"
  rgb: string;       // "rgb(255, 102, 0)"
  hsl: string;       // "hsl(24, 100%, 50%)"
  r: number; g: number; b: number;
  timestamp: number;
}

interface HslColor {
  h: number; s: number; l: number;
}

interface HistoryItem {
  color: ColorInfo;
  id: number;
}

四、核心功能详解

4.1 屏幕截图与 PixelMap 渲染

应用启动时(aboutToAppear),自动触发截图流程。当前实现使用 createPixelMap 创建 1920×1080 的 PixelMap 并用测试图案填充。在真实设备上可替换为 screen.getScreenCapture() 获取真实屏幕。

typescript 复制代码
const initOps: image.InitializationOptions = {
  alphaType: image.AlphaType.PREMUL,
  editable: true,
  pixelFormat: image.PixelMapFormat.RGBA_8888,
  size: { height: screenH, width: screenW }
};
const pixelMap = await image.createPixelMap(buf, initOps);

每个像素占 4 字节(RGBA),1920×1080 共约 8MB。editable: true 允许后续通过 writeBufferToPixels 写入像素数据。

主截图区域使用 Canvas,通过 CanvasRenderingContext2D.drawImage() 将 PixelMap 绘制到 Canvas 上。

4.2 鼠标悬停实时取色

事件监听

Canvas 注册 onMouse 事件监听鼠标移动:

typescript 复制代码
.onMouse((event: MouseEvent) => this.onCanvasMouseEvent(event))

private onCanvasMouseEvent(event: MouseEvent): void {
  if (event.action === MouseAction.Move) {
    this.mouseX = event.x;
    this.mouseY = event.y;
    this.cursorInCanvas = true;
    this.pickColor(event.x, event.y);
    this.renderLoupe();
  }
}
颜色拾取算法

为兼容 API 23,摒弃了不可用的 readBufferToPixels,改用像素缓冲区直接内存索引:

typescript 复制代码
private pickColor(canvasX: number, canvasY: number): void {
  const scaleX = this.screenshotWidth / this.canvasWidth;
  const scaleY = this.screenshotHeight / this.canvasHeight;
  const imgX = Math.round(canvasX * scaleX);
  const imgY = Math.round(canvasY * scaleY);

  if (imgX < 0 || imgX >= this.screenshotWidth ||
      imgY < 0 || imgY >= this.screenshotHeight) return;

  const buf = new Uint8Array(this.pixelBuffer);
  const idx = (imgY * this.screenshotWidth + imgX) * 4;
  const r = buf[idx], g = buf[idx + 1], b = buf[idx + 2];

  this.currentColor = {
    hex: rgbToHex(r, g, b),
    rgb: rgbToRgbStr(r, g, b),
    hsl: rgbToHslStr(r, g, b),
    r, g, b, timestamp: Date.now()
  };
}

坐标转换是关键:Canvas 的显示尺寸与原始截图尺寸可能不同,需根据宽高比例因子换算坐标。

4.3 点击取色与历史记录

点击 Canvas 时触发:

typescript 复制代码
private onCanvasClick(event: ClickEvent): void {
  this.pickColor(event.x, event.y);
  setTimeout(() => this.addToHistory(this.currentColor), 150);
}

历史管理实现了去重(最新历史与当前颜色 HEX 相同则跳过)、头部插入(新颜色在最上方)、容量控制(最多 24 条):

typescript 复制代码
private addToHistory(color: ColorInfo): void {
  if (this.colorHistory.length > 0 &&
      this.colorHistory[0].color.hex === color.hex) return;

  const newItem: HistoryItem = { color: { ... }, id: Date.now() };
  this.colorHistory = [newItem].concat(this.colorHistory);

  if (this.colorHistory.length > MAX_HISTORY) {
    this.colorHistory = this.colorHistory.slice(0, MAX_HISTORY);
  }
}

4.4 像素级放大镜(Loupe)

放大镜是一个圆形 Canvas,将鼠标指针周围区域放大 10 倍显示,并配有十字准星辅助定位。

渲染流程
  • 计算源区域:根据鼠标位置和缩放比确定原始图像中对应的区域。
  • 圆形裁剪:使用 ctx.beginPath() + ctx.arc() + ctx.clip() 实现。
  • 绘制放大图像:ctx.drawImage() 将源区域放大渲染。
  • 十字准星:使用白色半透明线条绘制(rgba(255,255,255,0.9)),确保在任何背景色上可见。
typescript 复制代码
this.loupeCtx.save();
this.loupeCtx.beginPath();
this.loupeCtx.arc(lr, lr, lr - 2, 0, Math.PI * 2);
this.loupeCtx.clip();
this.loupeCtx.drawImage(this.currentScreenshot,
  srcCX - srcHalfW, srcCY - srcHalfH,
  srcHalfW * 2, srcHalfH * 2, 0, 0, size, size);
this.loupeCtx.restore();

技术要点save() / restore() 必须配对,否则会影响后续绘制状态。

4.5 颜色空间转换

HEX 编码
typescript 复制代码
function rgbToHex(r: number, g: number, b: number): string {
  return '#' + r.toString(16).padStart(2, '0') +
    g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
}

padStart(2, '0') 确保单通道值(如 0x0F)格式化为两位。

RGB → HSL 转换

HSL(色相、饱和度、明度)更接近人类对颜色的感知方式:

typescript 复制代码
function rgbToHsl(r: number, g: number, b: number): HslColor {
  const rN = r / 255, gN = g / 255, bN = b / 255;
  const max = Math.max(rN, gN, bN);
  const min = Math.min(rN, gN, bN);
  const delta = max - min;
  let h = 0, s = 0, l = (max + min) / 2;

  if (delta !== 0) {
    s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);
    if (max === rN) h = ((gN - bN) / delta + (gN < bN ? 6 : 0)) * 60;
    else if (max === gN) h = ((bN - rN) / delta + 2) * 60;
    else h = ((rN - gN) / delta + 4) * 60;
  }
  return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };
}

输出如 hsl(24, 100%, 50%),H 为 0--360° 色环角度,S 和 L 为 0--100%。

4.6 一键复制

颜色值行组件 ColorValueRow 接受 onCopy 回调:

typescript 复制代码
struct ColorValueRow {
  private onCopy: () => void = () => {};

  build() {
    Row() {
      Text(this.label).fontSize(11).width(36);
      Text(this.value).fontSize(12).layoutWeight(1);
      Button('复制').width(44).height(22).onClick(() => this.onCopy());
    }
  }
}

复制时通过 copyToClipboard 函数实现:

typescript 复制代码
function copyToClipboard(text: string): void {
  promptAction.showToast({ message: '已复制: ' + text, duration: 1500 });
}

未来可升级为 @ohos.pasteboard 的完整剪贴板 API。

4.7 鼠标离开状态处理

由于 onMouseLeave 在 Canvas 和 Stack 组件上均不支持,改用 onHover

typescript 复制代码
.onHover((isHover: boolean) => {
  if (!isHover) this.onCanvasMouseLeave();
})

private onCanvasMouseLeave(): void {
  this.cursorInCanvas = false;
  this.loupeCtx.clearRect(0, 0, LOUPE_RADIUS * 2, LOUPE_RADIUS * 2);
}

五、UI 布局详解

5.1 整体结构

采用经典两栏布局:

复制代码
┌──────────────────────────────────────────────┬──────┐
│  左侧截图区域 (layoutWeight=1)              │ 右侧 │
│  ┌─ 工具栏 ─────────────────────────────┐   │ 面板 │
│  │ [重新截图] [清除历史]  状态文字       │   │280px │
│  └──────────────────────────────────────┘   │      │
│  ┌── Stack ─────────────────────────────┐  │ ├───┤ │
│  │       Canvas(截图展示)              │  │ │当前│ │
│  │                                      │  │ │取色│ │
│  └──────────────────────────────────────┘  │ ├───┤ │
│                                            │ │放大│ │
│                                            │ │镜  │ │
│                                            │ ├───┤ │
│                                            │ │取色│ │
│                                            │ │历史│ │
└────────────────────────────────────────────┴──┴───┘

左侧截图区域最大化,右侧面板固定 280px,信息流自上而下。

当前取色卡片

包含三个 ColorValueRow 子组件,使用不同强调色区分:HEX(蓝 #0078D4)、RGB(绿 #10B981)、HSL(紫 #8B5CF6),分别对应 Web 开发、设计工具和色彩理论研究场景。

取色历史卡片

使用 Scroll + ForEach 实现滚动列表。每条记录含 20×20 颜色预览块、HEX 值和取色时间。空状态显示提示文字。

typescript 复制代码
Scroll() {
  Column({ space: 6 }) {
    ForEach(this.colorHistory, (item: HistoryItem) => {
      ColorHistoryRow({ color: item.color, onCopy: ... });
    }, (item: HistoryItem) => item.id.toString());
  }
}
.height(240);

5.3 工具栏

控件 功能 样式
「📷 重新截图」 重新捕获屏幕 蓝色背景,白色文字
「🗑️ 清除历史」 清空所有历史记录 白色背景,灰色边框
状态文字 显示当前操作状态 灰色 11px 文字

六、ArkTS 兼容性适配挑战

从标准 TypeScript 迁移到 ArkTS 过程中面临的编译器限制及解决方案:

6.1 解构赋值限制

typescript 复制代码
// ❌ const { h, s, l } = rgbToHsl(r, g, b);
// ✅
const hsl = rgbToHsl(r, g, b);
const h = hsl.h; const s = hsl.s; const l = hsl.l;

6.2 保留标识符冲突

ColorPicker 与系统保留词冲突,重命名为 ColorPickerTool

6.3 匿名对象类型

返回匿名对象字面量的方法不能作为类型引用,改为定义命名接口 HslColor

6.4 对象/数组展开运算符

两者皆不支持,分别改为逐字段赋值和 concat

typescript 复制代码
// 对象展开 → 逐字段复制
// 数组展开 → [newItem].concat(this.colorHistory)

6.5 回调属性签名

回调属性必须显式声明为函数签名:private onCopy: () => void = () => {};

6.6 不可用 API

PixelMap.readBufferToPixels() 在 API 23 中已移除。改为在写入时保存 ArrayBuffer,取色时直接 Uint8Array 索引。

6.7 事件类型

  • MouseAction.Leave 不存在 → 改用 onHover 检测鼠标离开。
  • Stack 不支持 justifyContent/alignItems → 改用 alignContent(Alignment.Center)

七、测试图案生成

内置测试图案使用三个不同频率和相位偏移的正弦波生成 RGB 通道,产生平滑渐变且色彩丰富的图像:

typescript 复制代码
const r = 128 + 127 * Math.sin(x * 0.003 + y * 0.002);
const g = 128 + 127 * Math.sin(x * 0.002 + y * 0.003 + 2);
const b = 128 + 127 * Math.sin(x * 0.001 + y * 0.004 + 4);

128 + 127 × sin(...) 确保各通道值在 1--255 范围。三个通道使用不同频率和相位,避免出现简单重复模式。


八、项目启动与运行

8.1 启动配置

EntryAbility.ets 自动加载主页面:

typescript 复制代码
windowStage.loadContent('pages/ColorPicker', (err) => {
  if (err.code) hilog.error(DOMAIN, 'Failed to load content.', JS, ON.stringify(err));
});

8.2 页面注册

main_pages.json 中配置页面路由:

json 复制代码
{ "src": ["pages/ColorPicker"] }

8.3 构建命令

bash 复制代码
hvigorw --mode module -p module=entry -p product=default assembleHap

产物路径:entry/build/default/outputs/default/entry-default.hap


九、扩展方向

9.1 真实屏幕截图

drawTestPattern 替换为 screen.getScreenCapture()window.snapshot(),加入 ohos.permission.CAPTURE_SCREEN 权限即可。

9.2 吸管光标与拖拽取色

在画布上绘制跟随鼠标的吸管光标,并支持拖拽到屏幕任意位置取色。

9.3 调色板导出与对比度分析

支持将历史颜色导出为 CSS 变量或 JSON,以及基于 WCAG 2.1 标准计算颜色对比度,提示无障碍达标情况。


十、总结

本文详细介绍了一款基于 HarmonyOS NEXT(API 23)的 ArkTS PC 端屏幕取色器的完整设计与实现。工具核心亮点:

  1. 实时取色:Canvas onMouse 事件 + 像素缓冲区直接索引,毫秒级响应。
  2. 三种颜色格式:HEX、RGB、HSL 同时显示,满足不同场景需求。
  3. 像素放大镜:10 倍放大 + 十字准星,支持像素级精确取色。
  4. 取色历史:最多 24 条记录,去重、滚动查看、快速复制。
  5. ArkTS 适配:全面解决严格模式下编译器限制,作为 ArkTS 开发实践参考。

随着 HarmonyOS NEXT 生态在 PC 领域的持续建设,原生工具链将日益丰富。这款取色器作为一个实用工具的实现,希望能为鸿蒙原生应用开发者提供有价值的参考。


最后更新:2025年7月

相关推荐
伶俜661 小时前
# ✨ 零基础学 ArkUI 动画(专题一):从 animateTo 到 Lottie,一篇吃透全部
学习·华为·harmonyos
●VON1 小时前
AtomGit Flutter鸿蒙客户端:Provider状态管理
flutter·华为·跨平台·harmonyos·鸿蒙
伶俜661 小时前
# [特殊字符] 零基础学 ArkUI 数据持久化(专题三):5 种存储方案深度对比
学习·华为·wpf·harmonyos
FrameNotWork1 小时前
HarmonyOS6.1 图像分类应用完整实战:从模型到界面
人工智能·分类·数据挖掘·harmonyos
带刺的坐椅2 小时前
SolonCode(编码智能体)支持鸿蒙 PC
java·web·ai编程·harmonyos·soloncode·鸿蒙 pc
李二。2 小时前
HarmonyOS NEXT 定时关机工具:从设计到实现的完整技术解析
华为·harmonyos
川石课堂软件测试2 小时前
UI自动化测试|CSS元素定位实践
css·测试工具·ui·fiddler·单元测试·appium·harmonyos
yuegu7772 小时前
HarmonyOS应用<节气通>开发第15篇:学习记录页面
学习·信息可视化·harmonyos