畫布元件
畫布元件是 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()方法的回呼函數中呼叫。