画布组件
画布组件是 uni-app 提供的用于绘制图形的组件,可用于实现各种自定义绘图、图表、签名等功能。
canvas 画布
canvas 组件提供了一个画布,开发者可以使用 JavaScript 在上面绘制各种图形。
属性说明
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| canvas-id | String | canvas 组件的唯一标识符,必填 | |
| disable-scroll | Boolean | false | 当在 canvas 中移动时,是否禁止页面滚动 |
| type | String | 2d | 指定 canvas 类型,支持 2d 和 webgl |
| width | Number | canvas 宽度,单位为 px | |
| height | Number | canvas 高度,单位为 px |
事件说明
| 事件名 | 说明 | 返回值 |
|---|---|---|
| @touchstart | 手指触摸动作开始时触发 | event |
| @touchmove | 手指触摸后移动时触发 | event |
| @touchend | 手指触摸动作结束时触发 | event |
| @touchcancel | 手指触摸动作被打断时触发 | event |
| @longtap | 手指长按 500ms 之后触发 | event |
| @error | 发生错误时触发 | event |
示例代码
基础绘图
vue
<template>
<view class="container">
<canvas
canvas-id="myCanvas"
:width="canvasWidth"
:height="canvasHeight"
:disable-scroll="true"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
style="width: 100%; height: 300px; background-color: #f1f1f1;"
></canvas>
<view class="canvas-tools">
<view class="tool-item" v-for="(color, index) in colors" :key="index">
<view
class="color-block"
:style="{ backgroundColor: color }"
:class="{ active: currentColor === color }"
@click="selectColor(color)"
></view>
</view>
<view class="tool-item">
<slider
:value="lineWidth"
:min="1"
:max="20"
:step="1"
show-value
@change="changeLineWidth"
></slider>
</view>
<view class="tool-actions">
<button type="default" @click="clearCanvas">清空画布</button>
<button type="primary" @click="saveCanvas">保存图片</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
canvasWidth: 300,
canvasHeight: 200,
canvasContext: null,
lastX: 0,
lastY: 0,
isTouching: false,
lineWidth: 5,
currentColor: '#000000',
colors: ['#000000', '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff']
}
},
onReady() {
// 获取设备信息以设置 canvas 大小
const sysInfo = uni.getSystemInfoSync();
this.canvasWidth = sysInfo.windowWidth;
this.canvasHeight = 300;
// 获取 canvas 上下文
this.canvasContext = uni.createCanvasContext('myCanvas', this);
// 设置默认样式
this.canvasContext.setStrokeStyle(this.currentColor);
this.canvasContext.setLineWidth(this.lineWidth);
this.canvasContext.setLineCap('round');
this.canvasContext.setLineJoin('round');
},
methods: {
// 触摸开始事件
touchStart(e) {
const touch = e.touches[0];
this.lastX = touch.x;
this.lastY = touch.y;
this.isTouching = true;
},
// 触摸移动事件
touchMove(e) {
if (!this.isTouching) return;
const touch = e.touches[0];
const x = touch.x;
const y = touch.y;
// 绘制线条
this.canvasContext.beginPath();
this.canvasContext.moveTo(this.lastX, this.lastY);
this.canvasContext.lineTo(x, y);
this.canvasContext.stroke();
this.canvasContext.draw(true);
// 更新最后的坐标
this.lastX = x;
this.lastY = y;
},
// 触摸结束事件
touchEnd() {
this.isTouching = false;
},
// 选择颜色
selectColor(color) {
this.currentColor = color;
this.canvasContext.setStrokeStyle(color);
},
// 改变线宽
changeLineWidth(e) {
this.lineWidth = e.detail.value;
this.canvasContext.setLineWidth(this.lineWidth);
},
// 清空画布
clearCanvas() {
this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.canvasContext.draw();
},
// 保存画布为图片
saveCanvas() {
uni.canvasToTempFilePath({
canvasId: 'myCanvas',
success: (res) => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
icon: 'success'
});
},
fail: (err) => {
console.error('保存失败:', err);
uni.showToast({
title: '保存失败',
icon: 'none'
});
}
});
},
fail: (err) => {
console.error('导出失败:', err);
uni.showToast({
title: '导出失败',
icon: 'none'
});
}
}, this);
}
}
}
</script>
<style>
.container {
padding: 15px;
}
.canvas-tools {
margin-top: 20px;
}
.tool-item {
margin-bottom: 15px;
}
.color-block {
width: 30px;
height: 30px;
border-radius: 50%;
margin-right: 10px;
display: inline-block;
border: 2px solid #ddd;
}
.color-block.active {
border: 2px solid #007AFF;
}
.tool-actions {
display: flex;
justify-content: space-between;
}
.tool-actions button {
width: 48%;
}
</style>绘制图表
vue
<template>
<view class="container">
<canvas
canvas-id="chartCanvas"
:width="canvasWidth"
:height="canvasHeight"
style="width: 100%; height: 300px; background-color: #ffffff;"
></canvas>
<view class="chart-controls">
<button type="primary" @click="drawBarChart">柱状图</button>
<button type="primary" @click="drawLineChart">折线图</button>
<button type="primary" @click="drawPieChart">饼图</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
canvasWidth: 300,
canvasHeight: 300,
canvasContext: null,
chartData: [
{ name: '一月', value: 55 },
{ name: '二月', value: 66 },
{ name: '三月', value: 78 },
{ name: '四月', value: 95 },
{ name: '五月', value: 110 },
{ name: '六月', value: 130 }
],
colors: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272']
}
},
onReady() {
// 获取设备信息以设置 canvas 大小
const sysInfo = uni.getSystemInfoSync();
this.canvasWidth = sysInfo.windowWidth - 30; // 减去 padding
this.canvasHeight = 300;
// 获取 canvas 上下文
this.canvasContext = uni.createCanvasContext('chartCanvas', this);
// 默认绘制柱状图
this.$nextTick(() => {
this.drawBarChart();
});
},
methods: {
// 绘制柱状图
drawBarChart() {
const ctx = this.canvasContext;
const data = this.chartData;
const width = this.canvasWidth;
const height = this.canvasHeight;
const padding = 40;
const barWidth = (width - padding * 2) / data.length - 10;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 找出最大值
const maxValue = Math.max(...data.map(item => item.value));
// 绘制坐标轴
ctx.beginPath();
ctx.setLineWidth(2);
ctx.setStrokeStyle('#333333');
ctx.moveTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding); // x轴
ctx.moveTo(padding, height - padding);
ctx.lineTo(padding, padding); // y轴
ctx.stroke();
// 绘制柱状图
data.forEach((item, index) => {
const x = padding + index * ((width - padding * 2) / data.length) + 5;
const barHeight = ((height - padding * 2) * item.value) / maxValue;
const y = height - padding - barHeight;
// 绘制柱子
ctx.beginPath();
ctx.setFillStyle(this.colors[index % this.colors.length]);
ctx.fillRect(x, y, barWidth, barHeight);
// 绘制数值
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(item.value.toString(), x + barWidth / 2 - 10, y - 5);
// 绘制标签
ctx.fillText(item.name, x + barWidth / 2 - 10, height - padding + 20);
});
// 绘制 y 轴刻度
for (let i = 0; i <= 5; i++) {
const y = height - padding - (i * (height - padding * 2)) / 5;
const value = Math.round((i * maxValue) / 5);
ctx.beginPath();
ctx.setLineWidth(1);
ctx.setStrokeStyle('#cccccc');
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(value.toString(), padding - 25, y + 5);
}
// 绘制标题
ctx.setFontSize(16);
ctx.setFillStyle('#333333');
ctx.fillText('月度销售数据', width / 2 - 50, 20);
ctx.draw();
},
// 绘制折线图
drawLineChart() {
const ctx = this.canvasContext;
const data = this.chartData;
const width = this.canvasWidth;
const height = this.canvasHeight;
const padding = 40;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 找出最大值
const maxValue = Math.max(...data.map(item => item.value));
// 绘制坐标轴
ctx.beginPath();
ctx.setLineWidth(2);
ctx.setStrokeStyle('#333333');
ctx.moveTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding); // x轴
ctx.moveTo(padding, height - padding);
ctx.lineTo(padding, padding); // y轴
ctx.stroke();
// 绘制折线
ctx.beginPath();
ctx.setLineWidth(2);
ctx.setStrokeStyle('#5470c6');
data.forEach((item, index) => {
const x = padding + index * ((width - padding * 2) / (data.length - 1));
const y = height - padding - ((height - padding * 2) * item.value) / maxValue;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
// 绘制数据点
ctx.setFillStyle('#5470c6');
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fill();
// 绘制数值
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(item.value.toString(), x - 10, y - 10);
// 绘制标签
ctx.fillText(item.name, x - 10, height - padding + 20);
});
ctx.stroke();
// 绘制 y 轴刻度
for (let i = 0; i <= 5; i++) {
const y = height - padding - (i * (height - padding * 2)) / 5;
const value = Math.round((i * maxValue) / 5);
ctx.beginPath();
ctx.setLineWidth(1);
ctx.setStrokeStyle('#cccccc');
ctx.moveTo(padding, y);
ctx.lineTo(width - padding, y);
ctx.stroke();
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(value.toString(), padding - 25, y + 5);
}
// 绘制标题
ctx.setFontSize(16);
ctx.setFillStyle('#333333');
ctx.fillText('月度销售趋势', width / 2 - 50, 20);
ctx.draw();
},
// 绘制饼图
drawPieChart() {
const ctx = this.canvasContext;
const data = this.chartData;
const width = this.canvasWidth;
const height = this.canvasHeight;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - 60;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 计算总和
const total = data.reduce((sum, item) => sum + item.value, 0);
// 绘制饼图
let startAngle = 0;
data.forEach((item, index) => {
const portion = item.value / total;
const endAngle = startAngle + portion * 2 * Math.PI;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.setFillStyle(this.colors[index % this.colors.length]);
ctx.fill();
// 绘制标签线和文字
const midAngle = startAngle + (endAngle - startAngle) / 2;
const labelRadius = radius * 1.2;
const labelX = centerX + Math.cos(midAngle) * labelRadius;
const labelY = centerY + Math.sin(midAngle) * labelRadius;
ctx.beginPath();
ctx.moveTo(centerX + Math.cos(midAngle) * radius, centerY + Math.sin(midAngle) * radius);
ctx.lineTo(labelX, labelY);
ctx.setStrokeStyle('#333333');
ctx.setLineWidth(1);
ctx.stroke();
// 绘制百分比
const percentage = Math.round(portion * 100) + '%';
ctx.setFontSize(12);
ctx.setFillStyle('#333333');
ctx.fillText(`${item.name}: ${percentage}`, labelX - 20, labelY);
startAngle = endAngle;
});
// 绘制标题
ctx.setFontSize(16);
ctx.setFillStyle('#333333');
ctx.fillText('销售占比分析', width / 2 - 50, 20);
ctx.draw();
}
}
}
</script>
<style>
.container {
padding: 15px;
}
.chart-controls {
display: flex;
justify-content: space-around;
margin-top: 20px;
}
.chart-controls button {
width: 30%;
}
</style>注意事项
Canvas 的坐标系以左上角为原点 (0, 0),x 轴向右,y 轴向下。
在使用 Canvas 时,需要注意不同平台的兼容性问题,某些 API 可能在特定平台上不可用。
Canvas 绘图是一次性的,如果需要更新画布内容,需要重新绘制整个画布。
使用
draw()方法时,第一个参数为true表示保留上一次绘制的结果,为false或不传表示清空画布再绘制。在小程序中,Canvas 的大小单位是 px,不是 rpx。如果需要适配不同屏幕,可以通过
uni.getSystemInfoSync()获取设备信息来动态设置 Canvas 的大小。对于复杂的图表需求,建议使用专业的图表库,如 ECharts、F2 等,它们提供了更丰富的功能和更好的性能。
在进行 Canvas 绘图时,应当注意性能问题,避免在一个页面中同时绘制多个复杂的 Canvas。
使用
canvasToTempFilePath方法可以将 Canvas 导出为图片,但需要注意该方法是异步的,应当在draw()方法的回调函数中调用。