
棋盘是游戏的脸面——Canvas让每一颗棋子都栩栩如生设计棋盘如下代码如下/** * ChessBoardView.ets - 可复用的五子棋棋盘组件 * 使用Canvas绘制棋盘、网格、棋子 */import{BOARD_SIZE,EMPTY,BLACK,WHITE}from./GameConstants;constBOARD_BG_COLOR:string#D4A868;constLINE_COLOR:string#5C4033;constSTAR_COLOR:string#3B2F2F;constLAST_MOVE_COLOR:string#E63946;Componentexportstruct ChessBoardView{/** 棋盘数据扁平数组row*15col */PropWatch(onDataChange)boardData:number[][];/** 最后一手行 */ProplastMoveRow:number-1;/** 最后一手列 */ProplastMoveCol:number-1;/** 点击回调 */onCellClick:(row:number,col:number)void(){};privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(newRenderingContextSettings(true));privatecanvasSize:number320;privateisReady:booleanfalse;build(){Canvas(this.context).width(100%).aspectRatio(1).onReady((){this.isReadytrue;this.drawBoard();}).onClick((event:ClickEvent){this.handleClick(event);}).onAreaChange((_oldValue:Area,newValue:Area){constwnewValue.width;if(typeofwnumber){this.canvasSizew;if(this.isReady){this.drawBoard();}}})}/** 数据变化时重绘 */onDataChange():void{if(this.isReady){this.drawBoard();}}/** 处理点击事件 */privatehandleClick(event:ClickEvent):void{constcellSizethis.canvasSize/BOARD_SIZE;constxevent.x;constyevent.y;constcolMath.round((x-cellSize/2)/cellSize);constrowMath.round((y-cellSize/2)/cellSize);if(row0rowBOARD_SIZEcol0colBOARD_SIZE){this.onCellClick(row,col);}}/** 绘制棋盘 */privatedrawBoard():void{constctxthis.context;constsizethis.canvasSize;if(size0)return;constcellSizesize/BOARD_SIZE;consthalfCellcellSize/2;// 清空画布ctx.clearRect(0,0,size,size);// 绘制背景ctx.fillStyleBOARD_BG_COLOR;ctx.fillRect(0,0,size,size);// 绘制网格线ctx.strokeStyleLINE_COLOR;ctx.lineWidth1;for(leti0;iBOARD_SIZE;i){constposhalfCelli*cellSize;// 水平线ctx.beginPath();ctx.moveTo(halfCell,pos);ctx.lineTo(size-halfCell,pos);ctx.stroke();// 垂直线ctx.beginPath();ctx.moveTo(pos,halfCell);ctx.lineTo(pos,size-halfCell);ctx.stroke();}// 绘制星位conststars:number[][][[3,3],[3,11],[7,7],[11,3],[11,11]];ctx.fillStyleSTAR_COLOR;for(conststarofstars){constsxhalfCellstar[1]*cellSize;constsyhalfCellstar[0]*cellSize;ctx.beginPath();ctx.arc(sx,sy,3,0,Math.PI*2);ctx.fill();}// 绘制棋子for(leti0;iBOARD_SIZE;i){for(letj0;jBOARD_SIZE;j){constidxi*BOARD_SIZEj;if(idxthis.boardData.length){constpiecethis.boardData[idx];if(piece!EMPTY){constpxhalfCellj*cellSize;constpyhalfCelli*cellSize;constradiuscellSize*0.42;this.drawPiece(ctx,px,py,radius,piece);}}}}// 绘制最后一手标记if(this.lastMoveRow0this.lastMoveCol0){constpxhalfCellthis.lastMoveCol*cellSize;constpyhalfCellthis.lastMoveRow*cellSize;ctx.strokeStyleLAST_MOVE_COLOR;ctx.lineWidth2;ctx.beginPath();ctx.arc(px,py,cellSize*0.16,0,Math.PI*2);ctx.stroke();}}/** 绘制单个棋子带渐变效果 */privatedrawPiece(ctx:CanvasRenderingContext2D,x:number,y:number,radius:number,player:number):void{if(playerBLACK){constgradientctx.createRadialGradient(x-radius*0.3,y-radius*0.3,radius*0.1,x,y,radius);gradient.addColorStop(0,#666666);gradient.addColorStop(0.5,#2A2A2A);gradient.addColorStop(1,#111111);ctx.fillStylegradient;}else{constgradientctx.createRadialGradient(x-radius*0.3,y-radius*0.3,radius*0.1,x,y,radius);gradient.addColorStop(0,#FFFFFF);gradient.addColorStop(0.7,#F0F0F0);gradient.addColorStop(1,#C8C8C8);ctx.fillStylegradient;}ctx.beginPath();ctx.arc(x,y,radius,0,Math.PI*2);ctx.fill();// 棋子边框ctx.strokeStyleplayerBLACK?#000000:#999999;ctx.lineWidth0.5;ctx.stroke();}}组件声明Componentexportstruct ChessBoardView{PropWatch(onDataChange)boardData:number[][];ProplastMoveRow:number-1;ProplastMoveCol:number-1;onCellClick:(row:number,col:number)void(){};privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(newRenderingContextSettings(true));privatecanvasSize:number320;privateisReady:booleanfalse;build(){Canvas(this.context).width(100%).aspectRatio(1).onReady((){this.isReadytrue;this.drawBoard();}).onClick((event:ClickEvent){this.handleClick(event);}).onAreaChange((_oldValue:Area,newValue:Area){constwnewValue.width;if(typeofwnumber){this.canvasSizew;if(this.isReady){this.drawBoard();}}})}}组件接口设计ChessBoardView是一个纯表现组件——只负责显示和事件传递输入PropPropWatch(onDataChange)boardData:number[][];// 棋盘数据ProplastMoveRow:number-1;// 最后一手行ProplastMoveCol:number-1;// 最后一手列输出回调onCellClick:(row:number,col:number)void(){};内部状态privatecontext:CanvasRenderingContext2D;// Canvas绘制上下文privatecanvasSize:number320;// 实际绘制尺寸privateisReady:booleanfalse;// Canvas是否就绪Canvas初始化privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(newRenderingContextSettings(true));RenderingContextSettings(true)中的true表示开启抗锯齿让棋子和线条更加平滑。Canvas组件的生命周期组件创建 ↓ Canvas挂载到组件树 ↓ onReady触发 → isReady true → drawBoard() ↓ onAreaChange触发 → canvasSize更新 → drawBoard() ↓ Prop boardData变化 → Watch onDataChange → drawBoard()onReady.onReady((){this.isReadytrue;this.drawBoard();})Canvas组件创建后需要等待onReady回调才能开始绘制。isReady标志位防止在Canvas未就绪时调用绘制方法。onAreaChange.onAreaChange((_oldValue:Area,newValue:Area){constwnewValue.width;if(typeofwnumber){this.canvasSizew;if(this.isReady){this.drawBoard();}}})当组件尺寸变化时如屏幕旋转、窗口调整重新获取尺寸并重绘。typeof w number检查是因为Area的width可能是number或string类型。Watch数据驱动重绘PropWatch(onDataChange)boardData:number[][];onDataChange():void{if(this.isReady){this.drawBoard();}}当父组件传入的boardData发生变化时Watch自动触发onDataChange进而调用drawBoard重绘。注意ArkTS中Prop数组变化需要创建新引用才能触发Watch。父组件中的做法privaterefreshBoardData():void{this.boardDatathis.engine.toFlatArray();// 创建新数组}toFlatArray()每次返回新数组确保引用变化触发Watch。drawBoard绘制流程privatedrawBoard():void{constctxthis.context;constsizethis.canvasSize;if(size0)return;constcellSizesize/BOARD_SIZE;consthalfCellcellSize/2;// 1. 清空画布ctx.clearRect(0,0,size,size);// 2. 绘制背景ctx.fillStyleBOARD_BG_COLOR;ctx.fillRect(0,0,size,size);// 3. 绘制网格线ctx.strokeStyleLINE_COLOR;ctx.lineWidth1;for(leti0;iBOARD_SIZE;i){constposhalfCelli*cellSize;ctx.beginPath();ctx.moveTo(halfCell,pos);ctx.lineTo(size-halfCell,pos);ctx.stroke();ctx.beginPath();ctx.moveTo(pos,halfCell);ctx.lineTo(pos,size-halfCell);ctx.stroke();}// 4. 绘制星位// 5. 绘制棋子// 6. 绘制最后一手标记}颜色常量constBOARD_BG_COLOR:string#D4A868;// 棋盘木色constLINE_COLOR:string#5C4033;// 网格线深棕constSTAR_COLOR:string#3B2F2F;// 星位黑色constLAST_MOVE_COLOR:string#E63946;// 最后一手红色这些颜色定义在组件文件顶部没有放入资源文件——因为它们是组件内部实现细节不需要国际化或主题适配。组件复用ChessBoardView被两个页面复用// TwoPlayerPageChessBoardView({boardData:this.boardData,lastMoveRow:this.lastMoveRow,lastMoveCol:this.lastMoveCol,onCellClick:(row:number,col:number){this.onCellClick(row,col);}})// AIBattlePage — 完全相同的用法ChessBoardView({boardData:this.boardData,lastMoveRow:this.lastMoveRow,lastMoveCol:this.lastMoveCol,onCellClick:(row:number,col:number){this.onCellClick(row,col);}})组件不关心数据来源双人对战还是AI对战只负责渲染和事件传递。总结ChessBoardView的设计展示了ArkUI组件的最佳实践Prop Watch数据驱动重绘Canvas生命周期onReady后才绘制onAreaChange响应式适配屏幕尺寸纯表现组件只负责显示和回调不含业务逻辑可复用被两个页面无缝复用