Skip to content

畫布元件

畫布元件是 uni-app 提供的用於繪製圖形的元件,可用於實現各種自訂繪圖、圖表、簽名等功能。

canvas 畫布

canvas 元件提供了一個畫布,開發者可以使用 JavaScript 在上面繪製各種圖形。

屬性說明

屬性名類型預設值說明
canvas-idStringcanvas 元件的唯一識別符,必填
disable-scrollBooleanfalse當在 canvas 中移動時,是否禁止頁面滾動
typeString2d指定 canvas 類型,支援 2d 和 webgl
widthNumbercanvas 寬度,單位為 px
heightNumbercanvas 高度,單位為 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>

注意事項

  1. Canvas 的座標系以左上角為原點 (0, 0),x 軸向右,y 軸向下。

  2. 在使用 Canvas 時,需要注意不同平台的相容性問題,某些 API 可能在特定平台上不可用。

  3. Canvas 繪圖是一次性的,如果需要更新畫布內容,需要重新繪製整個畫布。

  4. 使用 draw() 方法時,第一個參數為 true 表示保留上一次繪製的結果,為 false 或不傳表示清空畫布再繪製。

  5. 在小程式中,Canvas 的大小單位是 px,不是 rpx。如果需要適配不同螢幕,可以透過 uni.getSystemInfoSync() 取得裝置資訊來動態設定 Canvas 的大小。

  6. 對於複雜的圖表需求,建議使用專業的圖表庫,如 ECharts、F2 等,它們提供了更豐富的功能和更好的效能。

  7. 在進行 Canvas 繪圖時,應當注意效能問題,避免在一個頁面中同時繪製多個複雜的 Canvas。

  8. 使用 canvasToTempFilePath 方法可以將 Canvas 匯出為圖片,但需要注意該方法是非同步的,應當在 draw() 方法的回呼函數中呼叫。

一次開發,多端部署 - 讓跨平台開發更簡單