HarmonyOS APP实战-画图APP - 第3篇:实现基本形状绘制
1. 开篇
上一篇我们完成了画图APP的主页面布局,构建了顶部工具栏、底部颜色选择栏和中间画布区域的基础框架。顶部工具栏预留了形状切换按钮,底部颜色栏提供了基本的颜色选项,画布区域能够响应触摸事件并显示触摸坐标。页面整体交互骨架已经建立,但画布还无法真正绘制图形。
本篇将在现有框架上注入绘制能力。我们将使用 @ohos.graphics.drawing 模块提供的自绘制API,实现矩形、正方形和圆形的绘制功能。具体包括:通过触摸事件的按下和抬起位置确定形状的包围盒,调用 drawRect 和 drawCircle API进行实际绘制,并支持底部颜色选择栏改变填充颜色。完成后,点击工具栏的形状按钮,在画布上拖拽就能生成对应的几何图形。
2. 核心实现
2.1 基础配置 --- 建立绘制模块依赖与初始化
在画图APP的工程目录下,我们需要新建一个专门的绘制工具模块,用于封装所有图形绘制逻辑。同时配置 Canvas 组件和 @ohos.graphics.drawing 相关的导入。
创建 ShapeDrawer.ets 文件:
typescript
// ShapeDrawer.ets
// 本模块封装矩形、圆形等基本形状的绘制方法
// 依赖 @ohos.graphics.drawing 库中的 Canvas 和画笔相关类
import { Canvas, Brush, Pen, Point, Rect } from '@ohos.graphics.drawing';
// Canvas: 画布对象,所有绘制操作基于它进行
// Brush: 填充画笔,用于设置填充颜色、透明度等
// Pen: 描边画笔,用于设置线条颜色、宽度等
// Point: 坐标点对象
// Rect: 矩形区域对象
export class ShapeDrawer {
private canvas: Canvas | null = null;
/**
* 初始化绘制器,绑定画布实例
* @param canvas - 由 @ohos.graphics.drawing 创建的 Canvas 对象
*/
public init(canvas: Canvas): void {
this.canvas = canvas;
console.info('[ShapeDrawer] Canvas初始化完成');
}
/**
* 绘制填充矩形
* @param left - 矩形左上角x坐标
* @param top - 矩形左上角y坐标
* @param width - 矩形宽度
* @param height - 矩形高度
* @param color - 填充颜色,格式为RGB十六进制字符串,如 '#FF0000'
*/
public drawFillRect(left: number, top: number,
width: number, height: number,
color: string): void {
if (!this.canvas) {
console.error('[ShapeDrawer] Canvas未初始化');
return;
}
// 创建填充画笔Brush,设置颜色
const brush = new Brush();
brush.setColor(this.parseColor(color));
// 创建矩形区域对象,使用左上角坐标和宽高
const rect = new Rect(left, top, left + width, top + height);
// 保存当前画布状态,避免后续操作污染
this.canvas.save();
// 设置Brush为绘制填充时使用的画笔
this.canvas.attachBrush(brush);
// 调用drawRect绘制填充矩形
this.canvas.drawRect(rect);
// 分离画笔,恢复画布状态
this.canvas.detachBrush();
this.canvas.restore();
console.info(`[ShapeDrawer] 绘制矩形: left=${left}, top=${top}, w=${width}, h=${height}, color=${color}`);
}
/**
* 绘制填充圆形
* @param cx - 圆心x坐标
* @param cy - 圆心y坐标
* @param radius - 圆半径
* @param color - 填充颜色
*/
public drawFillCircle(cx: number, cy: number,
radius: number, color: string): void {
if (!this.canvas) {
console.error('[ShapeDrawer] Canvas未初始化');
return;
}
const brush = new Brush();
brush.setColor(this.parseColor(color));
this.canvas.save();
this.canvas.attachBrush(brush);
// 使用drawCircle API绘制圆形
// 参数依次为:圆心坐标点、半径、描边画笔(传null使用填充)、填充画笔
this.canvas.drawCircle(
new Point(cx, cy), // 圆心坐标
null, // Pen描边画笔,不需要轮廓
brush // Brush填充画笔
);
this.canvas.detachBrush();
this.canvas.restore();
console.info(`[ShapeDrawer] 绘制圆形: cx=${cx}, cy=${cy}, r=${radius}, color=${color}`);
}
/**
* 将十六进制颜色字符串转为Color对象
* @param hex - 十六进制颜色值
* @returns Color对象
*/
private parseColor(hex: string): Color {
// 兼容带#前缀的颜色值
const colorStr = hex.startsWith('#') ? hex.substring(1) : hex;
// 支持6位标准色和3位简写色
if (colorStr.length === 3) {
const r = parseInt(colorStr[0] + colorStr[0], 16);
const g = parseInt(colorStr[1] + colorStr[1], 16);
const b = parseInt(colorStr[2] + colorStr[2], 16);
return new Color(r, g, b, 255);
} else if (colorStr.length === 6) {
const r = parseInt(colorStr.substring(0, 2), 16);
const g = parseInt(colorStr.substring(2, 4), 16);
const b = parseInt(colorStr.substring(4, 6), 16);
return new Color(r, g, b, 255);
}
// 默认返回白色
return new Color(255, 255, 255, 255);
}
}
关键点说明:
- 使用
@ohos.graphics.drawing中的Canvas、Brush、Pen、Point、Rect类。这是 ArkGraphics 2D 提供自绘能力的核心模块。 Brush.setColor方法接受Color对象,我们需要将常用的十六进制颜色字符串转为Color。drawRect接收一个Rect对象定义矩形区域。drawCircle的签名比较特殊:第一个参数是Point圆心坐标,第二个是Pen(描边画笔,传入null表示不描边),第三个是Brush(填充画笔)。注意参数顺序和类型。- 每次绘制前后调用
save()attachBrush()detachBrush()restore()是常规操作模式,确保不同绘制之间不会互相影响状态。
2.2 核心逻辑 --- 实现触摸绘制形状的交互
现在进入画布页面的核心绘制逻辑。我们需要在上一篇文章建立的 DrawingCanvas 组件中,捕获用户的触摸事件,根据按下的起始点和抬起时的终点确定图形的尺寸和位置,然后调用 ShapeDrawer 进行绘制。
在 DrawingCanvas.ets 中实现触摸绘制逻辑:
typescript
// DrawingCanvas.ets
// 画布绘制组件,实现触摸手势绘制矩形和圆形
import { Canvas, Brush, Pen, Point, Rect } from '@ohos.graphics.drawing';
import { ShapeDrawer } from './ShapeDrawer';
@Component
export struct DrawingCanvas {
// 当前选择的绘制模式:'rect' | 'circle' | 'square'
@Prop shapeType: string = 'rect';
// 当前选中的颜色值
@Prop currentColor: string = '#FF0000';
// 触摸起始坐标
private startX: number = 0;
private startY: number = 0;
// 触摸结束坐标
private endX: number = 0;
private endY: number = 0;
// 画布引用,用于
private canvasRef: Canvas | null = null;
// 绘制工具实例
private drawer: ShapeDrawer = new ShapeDrawer();
build() {
Column() {
// 画布区域
Canvas()
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.onReady(() => {
// Canvas组件渲染完成时获取Canvas实例
// 实际项目中需要通过Canvas组件的onReady回调获取绘图上下文
// 这里简化处理,假设this.canvasRef已正确赋值
if (this.canvasRef) {
this.drawer.init(this.canvasRef);
}
})
.onTouch((event: TouchEvent) => {
this.handleCanvasTouch(event);
})
}
.width('100%')
.height('100%')
}
/**
* 处理画布上的触摸事件
* 按下时记录起始点,抬起时根据形状类型绘制
*/
private handleCanvasTouch(event: TouchEvent): void {
const touchInfo = event.getTouches()[0];
if (!touchInfo) {
return;
}
const x = touchInfo.x;
const y = touchInfo.y;
if (event.type === TouchType.Down) {
// 记录按下位置
this.startX = x;
this.startY = y;
console.info(`[DrawingCanvas] 触摸按下: (${x}, ${y})`);
} else if (event.type === TouchType.Up) {
// 记录抬起位置
this.endX = x;
this.endY = y;
// 计算形状的包围盒参数
const left = Math.min(this.startX, this.endX);
const top = Math.min(this.startY, this.endY);
const width = Math.abs(this.endX - this.startX);
const height = Math.abs(this.endY - this.startY);
console.info(`[DrawingCanvas] 触摸抬起: (${this.endX}, ${this.endY})`);
// 根据当前选中的形状类型执行绘制
switch (this.shapeType) {
case 'rect':
this.drawer.drawFillRect(left, top, width, height, this.currentColor);
break;
case 'square':
// 正方形:取宽高中的较大者,使长宽相等
const side = Math.max(width, height);
this.drawer.drawFillRect(left, top, side, side, this.currentColor);
break;
case 'circle':
// 圆形:计算圆心坐标和半径
const cx = (this.startX + this.endX) / 2;
const cy = (this.startY + this.endY) / 2;
const radius = Math.max(width, height) / 2;
this.drawer.drawFillCircle(cx, cy, radius, this.currentColor);
break;
default:
console.warn(`[DrawingCanvas] 未知的形状类型: ${this.shapeType}`);
}
}
}
}
关键点说明:
onTouch事件处理器在每次触摸时都会触发,通过event.type判断是按下(TouchType.Down)还是抬起(TouchType.Up)。- 绘制时用
Math.min和Math.abs计算包围盒,确保无论用户从哪个方向拖拽,都能正确圈出区域。 - 正方形模式强制宽高相等,取用户拖拽范围的较大边长。
- 圆形模式使用拖拽范围的包围盒对角线,以中心点为圆心进行计算。
this.canvasRef的获取在onReady回调中,这是获取 Canvas 上下文的标准方式。实际项目中这里需要更详细的初始化和上下文创建,为了示例清晰做了简化。

2.3 完整模块 --- 集成绘制与页面传参
最后,我们需要将绘制模块和主页面进行完整集成。主页面 Index.ets 需要传递用户选择的形状类型和颜色值到 DrawingCanvas 组件,并触发重新绘制。以下是一个集成后的完整主页面代码。
在 Index.ets 中集成绘制功能:
typescript
// Index.ets
// 画图APP主页面,集成顶部工具栏、底部颜色栏和画布绘制区域
import { DrawingCanvas } from './DrawingCanvas';
@Entry
@Component
struct Index {
// 当前绘制模式:'rect' | 'circle' | 'square'
@State currentShape: string = 'rect';
// 当前选中的颜色,初始为红色
@State currentColor: string = '#FF0000';
build() {
Column() {
// ========== 顶部工具栏 ==========
Row() {
// 矩形按钮
Button('矩形')
.onClick(() => {
this.currentShape = 'rect';
console.info(`[Index] 切换到矩形绘制模式`);
})
// 正方形按钮
Button('正方形')
.onClick(() => {
this.currentShape = 'square';
console.info(`[Index] 切换到正方形绘制模式`);
})
// 圆形按钮
Button('圆形')
.onClick(() => {
this.currentShape = 'circle';
console.info(`[Index] 切换到圆形绘制模式`);
})
}
.width('100%')
.padding({ left: 10, right: 10 })
.backgroundColor('#F0F0F0')
// ========== 画布绘制区域 ==========
DrawingCanvas({
shapeType: $currentShape,
currentColor: $currentColor
})
.layoutWeight(1)
// ========== 底部颜色选择栏 ==========
Row() {
// 六个常用颜色按钮
this.colorButton('#FF0000', '红色')
this.colorButton('#00FF00', '绿色')
this.colorButton('#0000FF', '蓝色')
this.colorButton('#FFFF00', '黄色')
this.colorButton('#FF00FF', '紫色')
this.colorButton('#000000', '黑色')
}
.width('100%')
.padding({ top: 10, bottom: 10 })
.backgroundColor('#333333')
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%')
.height('100%')
.backgroundColor('#E0E0E0')
}
/**
* 底部颜色按钮构建方法
* @param color - 颜色值
* @param label - 按钮文字
*/
@Builder
colorButton(color: string, label: string) {
Column() {
Circle()
.width(40)
.height(40)
.fill(color)
.margin({ bottom: 4 })
Text(label)
.fontSize(10)
.fontColor('#FFFFFF')
}
.onClick(() => {
this.currentColor = color;
console.info(`[Index] 选中颜色: ${label} (${color})`);
})
}
}
关键点说明:
- 主页面通过
@State装饰器响应式管理currentShape和currentColor状态。 - 点击顶部形状按钮时更新
currentShape,通过$currentShape语法将状态同步到子组件DrawingCanvas。 - 底部颜色栏设计为
Circle组件加文字标签,点击时更新currentColor。 Circle是内置组件,用于显示预设颜色的色块,方便用户选择。DrawingCanvas接收这些参数后,在触摸抬起时根据当前形状和颜色绘制对应图形。

3. 运行验证
在 DevEco Studio 中运行项目,预期表现如下:
- 启动 APP,看到熟悉的顶部工具栏、白色画布和底部颜色栏。
- 点击顶部的"矩形"按钮(未高亮显示为选中态,下一篇将完善交互反馈),然后在画布上从左上方到右下方拖拽手指。抬起手指后,画布上应该出现一个红色的填充矩形,且矩形区域正好覆盖你拖拽的范围。
- 点击"正方形"按钮,再拖拽一个任意矩形区域,画布上应该出现一个以较长边为边长的正方形。
- 点击"圆形"按钮,拖拽后画布上生成一个填充圆形,其直径等于拖拽范围的较长边。
- 点击底部颜色栏的"绿色"按钮,再绘制一个矩形,新矩形的填充色变为绿色,且不影响之前绘制的红色矩形。

4. 小结与预告
本篇在现有画布基础上,成功实现了基本形状的绘制功能。核心是创建了 ShapeDrawer 工具模块,封装了基于 @ohos.graphics.drawing 的 drawRect 和 drawCircle API;并在 DrawingCanvas 组件中集成了触摸事件处理,根据用户拖拽范围生成矩形、正方形和圆形;同时后台颜色栏的状态已经可以影响绘制填充色,虽然当前仅支持6种固定颜色。这些功能为画图APP打下了最基础的绘制能力,后续所有高级功能都将建立在这个绘制管道之上。
下一篇将聚焦"实现触摸手绘功能"。我们会脱离预定义形状,允许用户像真正的画笔一样自由绘制轨迹。逻辑上需要记录连续的触摸点,利用 Path 对象和 drawPath API 实现平滑的曲线渲染。届时,画图APP将同时支持"几何形状绘制"和"自由手绘"两种模式。