0
点赞
收藏
分享

微信扫一扫

Hello Qt(五十三)———QtQuick Canvas

WikongGuan 2022-03-11 阅读 119

一、QtQuick Canvas简介

QT5引入了Canvas元素。Canvas元素提供了一种与分辨率无关的位图绘制机制。通过Canvas,可以使用 JavaScript 代码进行绘制。Canvas元素的基本思想是,使用一个2D上下文对象渲染路径。2D上下文对象包含所必须的绘制函数,从而使Canvas元素看起来就像一个画板。2D上下文对象支持画笔、填充、渐变、文本以及其它一系列路径创建函数。

import QtQuick 2.0


Canvas
{
    id: root
    // 画板大小
    width: 200; height: 200
    // 重写绘制函数
    onPaint:
    {
        // 获得 2D 上下文对象
        var ctx = getContext("2d")

        // 设置画笔
        ctx.lineWidth = 4
        ctx.strokeStyle = "blue"

        // 设置填充
        ctx.fillStyle = "steelblue"

        // 开始绘制路径
        ctx.beginPath()

        // 移动到左上点作为起始点
        ctx.moveTo(50,50)

        // 上边线
        ctx.lineTo(150,50)

        // 右边线
        ctx.lineTo(150,150)

        // 底边线
        ctx.lineTo(50,150)

        // 左边线,并结束路径
        ctx.closePath()

        // 使用填充填充路径
        ctx.fill()

        // 使用画笔绘制边线
        ctx.stroke()
    }
}

本例中,画笔的宽度设置为4像素;使用strokeStyle属性,将画笔的颜色设置为蓝色。fillStyle属性则是设置填充色为steelblue。只有调用stroke()或fill()函数时,真实的绘制才会执行。调用stroke()或fill()函数会将当前路径绘制出来。路径是不能够被复用的,只有当前绘制状态才能够被复用。所谓“当前绘制状态”,指的是当前的画笔颜色、宽度、填充色等属性。

Canvas元素就是一种绘制的容器。2D上下文对象作为实际绘制的执行者。绘制过程必须在onPaint事件处理函数中完成。

Canvas本身提供一个典型的二维坐标系,原点在左上角,X轴正方向向右,Y轴正方向向下。使用Canvas进行绘制的典型过程是:

A、设置画笔和填充样式

B、创建路径

C、应用画笔和填充

二、Canvas操作

1、绘制API

除了自己进行路径的创建外,Canvas还提供了一系列方便使用的函数,用于一次添加一个矩形等

import QtQuick 2.0


Canvas
{
    id: root
    width: 120; height: 120
    onPaint:
    {
        var ctx = getContext("2d")
        ctx.fillStyle = 'green'
        ctx.strokeStyle = "blue"
        ctx.lineWidth = 4
        // 填充矩形
        ctx.fillRect(20, 20, 80, 80)
        // 裁减掉内部矩形
        ctx.clearRect(30,30, 60, 60)
        // 从左上角起,到外层矩形中心绘制一个边框
        ctx.strokeRect(20,20, 40, 40)
    }
}

2、渐变

Canvas元素可以使用颜色进行填充,也可以使用渐变。

import QtQuick 2.0


Canvas
{
    id: root
    width: 120; height: 120
    onPaint:
    {
        var ctx = getContext("2d")
        var gradient = ctx.createLinearGradient(100,0,100,200)
        gradient.addColorStop(0, "blue")
        gradient.addColorStop(0.5, "lightsteelblue")
        ctx.fillStyle = gradient
        ctx.fillRect(10,10,100,100)
    }
}

本例中,渐变的起始点位于(100, 0),终止点位于(100, 200)。两个点创建了一条位于画布中央位置的竖直线。渐变类似于插值,可以在 [0.0, 1.0] 区间内插入一个定义好的确定的颜色;其中,0.0 意味着渐变的起始点,1.0 意味着渐变的终止点。在0.0的位置(渐变起始点(100,0)的位置)设置颜色为“blue”;在1.0的位置(渐变终止点 (100, 200)位置)设置颜色为“lightsteelblue”。渐变的范围可以大于实际绘制的矩形,但绘制出来的矩形实际上裁减了渐变的一部分。渐变的定义其实是依据画布的坐标,不是定义的绘制路径的坐标。

3、阴影

路径可以使用阴影增强视觉表现力,可以把阴影定义为一个围绕在路径周围的区域,区域会有一定的偏移、有一定的颜色和特殊的模糊效果。可以使用shadowColor属性定义阴影的颜色;使用shadowOffsetX属性定义阴影在X轴方向的偏移量;使用shadowOffsetY属性定义阴影在Y轴方向的偏移量;使用shadowBlur属性定义阴影模糊的程度。可以实现一种围绕在路径周边的发光特效。

Canvas
{
    id: root
    width: 280; height: 120
    onPaint:
    {
        var ctx = getContext("2d")
        // 背景矩形
        ctx.strokeStyle = "#333"
        ctx.fillRect(0, 0, root.width, root.height);

        // 设置阴影属性
        ctx.shadowColor = "blue";
        ctx.shadowOffsetX = 2;
        ctx.shadowOffsetY = 2;
        ctx.shadowBlur = 10;

        // 设置字体并绘制
        ctx.font = 'bold 80px sans-serif';
        ctx.fillStyle = "#33a9ff";
        ctx.fillText("Hello", 30, 80);
    }
}

 本例创建一个带有发光效果的文本。为了更明显的显示发光效果,其背景界面将会是深色的。利用#333填充了一个背景矩形。矩形的起始点位于原点,长度和宽度分别绑定到画布的长度和宽度。定义阴影的属性,设置文本字体为80像素加粗的sans-serif,绘制了“Hello”。

4、图像

Canvas元素支持从多种源绘制图像。为了绘制图像,需要首先加载图像;使用Component.onCompleted事件处理函数可以达到目的。

import QtQuick 2.0


Canvas
{
    id: root
    width: 300; height: 300
    onPaint:
    {
        var ctx = getContext("2d")
        // 绘制图像
        ctx.drawImage('images/saturn.png', 10, 10)

        // 保存当前状态
        ctx.save()

        // 平移坐标系
        ctx.translate(100,0)
        ctx.strokeStyle = 'red'

        // 创建裁剪范围
        ctx.beginPath()
        ctx.moveTo(10,10)
        ctx.lineTo(55,10)
        ctx.lineTo(35,55)
        ctx.closePath()
        ctx.clip()  // 根据路径裁剪

        // 绘制图像并应用裁剪
        ctx.drawImage('images/saturn.png', 10, 10)

        // 绘制路径
        ctx.stroke()

        // 恢复状态
        ctx.restore()
    }

    Component.onCompleted:
    {
        loadImage("images/saturn.png")
    }

}

 5、变换

Canvas中的“变形”,主要指的是坐标系的变换,而不是路径的变换,变换的原点是画布原点。例如,如果以一个路径的中心点为定点进行缩放,需要现将画布原点移动到路径中心点。

import QtQuick 2.0


Canvas
{
    id: root
    width: 240; height: 120
    onPaint:
    {
        var ctx = getContext("2d")
        ctx.strokeStyle = "blue"
        ctx.lineWidth = 4
        ctx.translate(120, 60)
        ctx.strokeRect(-20, -20, 40, 40)

        // draw path now rotated
        ctx.strokeStyle = "green"
        ctx.rotate(Math.PI / 4)
        ctx.strokeRect(-20, -20, 40, 40)
        ctx.restore()
    }
}

6、组合

组合是将绘制的图形与已存在的像素做一些融合操作。Canvas支持几种组合方式,使用globalCompositeOperation属性可以设置组合的模式。

import QtQuick 2.0



Canvas
{
    id: root
    width: 600; height: 450
    property var operation : [
        'source-over', 'source-in', 'source-over',
        'source-atop', 'destination-over', 'destination-in',
        'destination-out', 'destination-atop', 'lighter',
        'copy', 'xor', 'qt-clear', 'qt-destination',
        'qt-multiply', 'qt-screen', 'qt-overlay', 'qt-darken',
        'qt-lighten', 'qt-color-dodge', 'qt-color-burn',
        'qt-hard-light', 'qt-soft-light', 'qt-difference',
        'qt-exclusion'
        ]

    onPaint:
    {
        var ctx = getContext('2d')
        for(var i=0; i<operation.length; i++)
        {
            var dx = Math.floor(i%6)*100
            var dy = Math.floor(i/6)*100
            ctx.save()
            ctx.fillStyle = '#33a9ff'
            ctx.fillRect(10+dx,10+dy,60,60)

            // TODO: does not work yet
            ctx.globalCompositeOperation = root.operation[i]
            ctx.fillStyle = '#ff33a9'
            ctx.globalAlpha = 0.75
            ctx.beginPath()
            ctx.arc(60+dx, 60+dy, 30, 0, 2*Math.PI)

            ctx.closePath()
            ctx.fill()
            ctx.restore()
        }
    }
}

 7、像素缓存

使用canvas,可以将canvas内容的像素数据读取出来,并且能够针对像素数据做一些操作。

使用createImageData(sw, sh)或getImageData(sx, sy, sw, sh)函数可以读取图像数据。两个函数都会返回一个ImageData对象,ImageData对象具有width、height和data等变量。data包含一个以RGBA格式存储的像素一维数组,其每一个分量值的范围都是[0,255]。如果要设置画布上面的像素,可以使用putImageData(imagedata, dx, dy)函数。

另外一个获取画布内容的方法是,将数据保存到一个图片。可以通过Canvas的函数save(path)或toDataURL(mimeType)实现,后者会返回一个图像的 URL,可以供Image元素加载图像。

import QtQuick 2.0


Rectangle
{
    width: 240; height: 120
    Canvas
    {
        id: canvas
        x: 10; y: 10
        width: 100; height: 100
        property real hue: 0.0
        onPaint:
        {
            var ctx = getContext("2d")
            var x = 10 + Math.random(80)*80
            var y = 10 + Math.random(80)*80
            hue += Math.random()*0.1
            if(hue > 1.0) { hue -= 1 }
            ctx.globalAlpha = 0.7
            ctx.fillStyle = Qt.hsla(hue, 0.5, 0.5, 1.0)
            ctx.beginPath()
            ctx.moveTo(x+5,y)
            ctx.arc(x,y, x/10, 0, 360)
            ctx.closePath()
            ctx.fill()
        }

        MouseArea
        {
            anchors.fill: parent
            onClicked:
            {
                var url = canvas.toDataURL('image/png')
                print('image url=', url)
                image.source = url
            }
        }
    }

    Image
    {
        id: image
        x: 130; y: 10
        width: 100; height: 100
    }

    Timer
    {
        interval: 1000
        running: true
        triggeredOnStart: true
        repeat: true
        onTriggered: canvas.requestPaint()
    }
}

本例创建了两个画布,左侧的画布每一秒产生一个圆点;鼠标点击会将画布内容保存,并且生成一个图像的 URL,右侧则会显示这个图像。

8、HTML Canvas移植

QML的Canvas对象由HTML 5的Canvas标签借鉴而来,将HTML 5的Canvas 应用移植到QML Canvas很容易。

HTML 5 canvas:

function draw() 
{
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.fillRect(0,0,300,300);
  for (var i=0;i<3;i++) 
  {
    for (var j=0;j<3;j++) 
    {
      ctx.save();
      ctx.strokeStyle = "#9CFF00";
      ctx.translate(50+j*100,50+i*100);
      drawSpirograph(ctx,20*(j+2)/(j+1),-8*(i+3)/(i+1),10);
      ctx.restore();
    }
  }
}

function drawSpirograph(ctx,R,r,O)
{
  var x1 = R-O;
  var y1 = 0;
  var i  = 1;
  ctx.beginPath();
  ctx.moveTo(x1,y1);
  do {
    if (i>20000) break;
    var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
    var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
    ctx.lineTo(x2,y2);
    x1 = x2;
    y1 = y2;
    i++;
  } while (x2 != R-O && y2 != 0 );

  ctx.stroke();
}

draw();

HTML按照顺序执行,draw()会成为脚本的入口函数。但在QML中,绘制必须在onPaint中完成,需要将draw()函数的调用移至onPaint。通常会在onPaint中获取绘制上下文,给draw()函数添加一个参数,用于接受Context2D对象。

QML Canvas:

import QtQuick 2.2


Canvas {
    id: root
    width: 300; height: 300
    onPaint:
    {
        var ctx = getContext("2d");
        draw(ctx);
    }

    function draw (ctx)
    {
        ctx.fillRect(0, 0, 300, 300);
        for (var i = 0; i < 3; i++)
        {
            for (var j = 0; j < 3; j++)
            {
                ctx.save();
                ctx.strokeStyle = "#9CFF00";
                ctx.translate(50 + j * 100, 50 + i * 100);
                drawSpirograph(ctx, 20 * (j + 2) / (j + 1), -8 * (i + 3) / (i + 1), 10);
                ctx.restore();
            }
        }
    }

    function drawSpirograph (ctx, R, r, O)
    {
        var x1 = R - O;
        var y1 = 0;
        var i  = 1;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        do {
            if (i > 20000) break;
            var x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72))

            var y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72))
            ctx.lineTo(x2, y2);
            x1 = x2;
            y1 = y2;
            i++;
        } while (x2 != R-O && y2 != 0 );

        ctx.stroke();
    }
}
举报

相关推荐

0 条评论