# aardio 范例: plus 控件 - GDI+ 绘图基础，按住鼠标左键可旋转方块

```aardio
/*
此示例演示在 plus 控件内使用 GDI+ 实现 3D 绘图或小游戏绘图的方法。
要注意 GDI+ 并不适合三维、游戏等复杂绘图，尤其是面积大、绘制数量与帧数多时性能受限。
*/
import fonts.fontAwesome;
import win.ui;
/*DSG{{*/
var winform = win.form(text="plus 控件 - GDI+ 绘图基础，按住鼠标左键可旋转方块";right=759;bottom=469;border="dialog frame";max=false)
winform.add(
plus={cls="plus";left=-1;top=0;right=760;bottom=470;db=1;dl=1;dr=1;dt=1;notify=1;z=1}
)
/*}}*/

// 3D 立方体顶点定义
var vertices = [
    [-50, -50, -50], [50, -50, -50], [50, 50, -50], [-50, 50, -50], 
    [-50, -50, 50], [50, -50, 50], [50, 50, 50], [-50, 50, 50]      
];

// 立方体的面（顶点索引）
var faces = [
    [1,2,3,4], [5,6,7,8], [1,2,6,5], [4,3,7,8], [1,4,8,5], [2,3,7,6]
];

var angleX, angleY = 0.5, 0.5;
var lastX, lastY;

// 3D 旋转矩阵计算
var rotate3D = function(point, ax, ay) {
    var x, y, z = table.unpack(point);
    var cosX, sinX = math.cos(ax), math.sin(ax);
    var y1 = y * cosX - z * sinX;
    var z1 = y * sinX + z * cosX;
    var cosY, sinY = math.cos(ay), math.sin(ay);
    var x2 = x * cosY + z1 * sinY;
    var z2 = -x * sinY + z1 * cosY;
    return [x2, y1, z2];
}

// 3D 投影到 2D
var project = function(point, centerX, centerY, distance) {
    var x, y, z = table.unpack(point);
    var scale = distance / (distance + z);
    return [centerX + x * scale, centerY + y * scale, z];
}

// 鼠标事件
winform.plus.onMouseDown = function(wParam,lParam){
    var x,y = win.getMessagePos(lParam);//指定参数 lParam 获取客户区坐标，否则获取屏幕坐标
    lastX, lastY = x, y;
    
	/*
    var rc = winform.plus.getClientRect()
	var x,y,cx,cy = winform.plus.getPos()
	var width = winform.plus.width //width 等于 cx
	*/
}

winform.plus.onMouseDrag = function(wParam,lParam){ 
    var x,y = win.getMessagePos(lParam);
    if(lastX) {
        angleY = angleY + (x - lastX) * 0.01;
        angleX = angleX + (y - lastY) * 0.01;
        lastX, lastY = x, y;
        owner.redraw();
    }
}

// 自绘步骤 1：绘制背景
winform.plus.onDrawBackground = function(graphics,rc,backgroundColor,foregroundColor){
    var p1 = ::POINTF(0, 0);
    var p2 = ::POINTF(rc.right, rc.bottom);
    var brush = gdip.lineBrush(p1, p2, 0xff1a1a2e, 0xff0f0f1a, 0);
    graphics.fillRectangle(brush, rc);
    brush.delete();
    
    return true;//返回 true 阻止绘制默认背景
}

// 自绘步骤 2：绘制前景（内容）
winform.plus.onDrawContent = function(graphics,rc,txtColor,rcContent,foregroundColor,font){
	/*
	plus 控件所有自绘事件的 graphics 参数都是内存画布（默认支持双缓冲）。 
	
	rc 参数是表示控件客户区的 ::RECT 结构体,等价于 owner.getClientRect() 的返回值。
	rcContent 是表示前景区的 ::RECT 结构体,排除了前景边距。 
	*/
	
    graphics.smoothingMode = 4; // 抗锯齿

    var centerX, centerY = rc.width() / 2, rc.height() / 2;
    var distance = 300;

    // 1. 演示 drawCurve: 绘制底部的装饰波浪线
    var penCurve = gdip.pen(0x40FFFFFF, 2);
    graphics.drawCurve(penCurve, [0,rc.bottom-50, 200,rc.bottom-100, 500,rc.bottom-20, rc.right,rc.bottom-60]);
    penCurve.delete();

    // 2. 演示 fillPie: 绘制一个发光的装饰圆弧（类似仪表盘或太阳）
    var brushPie = gdip.solidBrush(0x30FFFF00);
    graphics.fillPie(brushPie, rc.right-100, 20, 80, 80, 0, 270);
    brushPie.delete();

    // 3D 顶点计算
    var projected = [];
    for(i=1; #vertices) {
        var rotated = rotate3D(vertices[i], angleX, angleY);
        projected[i] = project(rotated, centerX, centerY, distance);
    }

    // 深度排序
    var faceDepths = [];
    for(i=1; #faces) {
        var face = faces[i];
        var avgZ = 0;
        for(j=1; #face) avgZ = avgZ + projected[face[j]][3];
        faceDepths[i] = {index=i; depth=avgZ/#face};
    }
    table.sort(faceDepths, lambda(next) owner.depth > next.depth); // 远者先画

    //scaleX,scaleY 为窗口当前水平与垂直方向缩放倍数，dpiX,dpiY 仅 DPI 缩放倍数，如果窗口未缩放则 scaleX,scaleY  等于 dpiX,dpiY
	var scaleX,scaleY,dpiX,dpiY = winform.getScale(); 
	graphics.scaleRect(rcContent,scaleX,scaleY);

    //如果只需要 DPI 缩放比例建议这样写
    var dpiX,dpiY = winform.dpiScale(1,1);
	
    // 绘制 3D 面
    for(i=1; #faceDepths) {
        var faceIdx = faceDepths[i].index;
        var face = faces[faceIdx];
        var pts = [];
        for(j=1; #face) {
            var p = projected[face[j]];
            pts[j] = ::POINTF(p[1], p[2]);
        }

        // 3. 演示 fillPolygon 与 drawPolygon
        var colors = [0xAA00d4ff, 0xAA7209b7, 0xAAf72585, 0xAA4cc9f0, 0xAA4361ee, 0xAA3a0ca3];

        // 特殊演示：为第一个面使用 pathGradientBrush (路径渐变画刷)
        if(faceIdx == 1){
            var path = gdip.path();
            path.addPolygon(pts);//参数可指定单个 ::POINTF 或数值数组，多参数自动转数组
            var pathBrush = gdip.pathGradientBrush(path);
            pathBrush.centerColor = 0xFFFFFFFF;
            pathBrush.surroundColors = {0xFF00d4ff};
            graphics.fillPath(pathBrush, path);
            pathBrush.delete();
            path.delete();
        }
        else {
            var brush = gdip.solidBrush(colors[faceIdx]);
            graphics.fillPolygon(brush, pts); // 填充多边形
            brush.delete();
        }

        var pen = gdip.pen(0xFFFFFFFF, 1);
        graphics.drawPolygon(pen, pts); // 描边多边形
        pen.delete();
    }
    
    graphics.resetTransform();//清除缩放
}

// 自绘步骤 3：绘制前景结束
winform.plus.onDrawForegroundEnd = function(graphics,rc,rcContent){
    // 4. 演示 drawString: 在屏幕左上角显示状态信息
    var family = gdip.family("Tahoma");
    
    //字体大小为 9pt，参数 3 必须指定为 _UnitPoint 以适应高分屏
    var fontText = family.createFont(9, 0, 3/*_UnitPoint*/);
    var strformat = gdip.stringformat();
    var brushText = gdip.solidBrush(0xFFFFFFFF);

    var info = string.format("旋转角度: X=%.2f, Y=%.2f", angleX, angleY);
    
    //要么根据控件实际大小计算大小，要么根据 DPI 缩放倍数计算大小，务必避免在高分屏下绘制的形状太小
    var dpiX,dpiY = winform.dpiScale(1,1) 
    graphics.drawString(info, fontText, ::RECTF(20, 20, 200*dpiX, 50*dpiY), strformat, brushText);

    brushText.delete();
    strformat.delete();
    fontText.delete();
    family.delete();
}

// 动画控制
winform.plus.onAnimation = function(state,beginning,change,timestamp,duration){
    angleY = angleY + 0.02;
    angleX = angleX + 0.01;
    //winform.plus.redrawTransparent(); //startAnimation 自动调用此函数
    return state + 1; //**切记 onAnimation 返回 null 值则停止动画**
}

/* 
开始动画，参数 @1 指定为 40 为 25FPS(帧每秒)，
超过 24  帧的动画人眼会感觉流畅。
可根据需要适当加快，但是太密集重绘是无意义的，
例如这个例子速度调再快人眼也感觉不出明显分别，甚至会因为重绘太多而变慢。
*/
winform.plus.startAnimation(30/*毫秒*/, 0);
winform.plus.setFocus();

/**
	// 常用事件模板（保持演示目的）
	winform.plus.onMouseMove = function(wParam,lParam){
		//不要直接在这个事件里密集重绘画布。
		//但 onMouseMove 的触发比之 onAnimation 更为密集，而且极不规则。
		//可能两次鼠标移动事件间隔非常短导致过于密集又不必要的重绘负担。
		//也可能两次间隔很大导致反应迟钝的假象。
		//建议仅在这个事件中更新坐标或数据，让 onAnimation 负责重绘
	}
	winform.plus.onMouseDoubleClick = function(wParam,lParam){}
	winform.plus.onMouseUp = function(wParam,lParam){} 
	winform.plus.onMouseWheel = function(flags,delta,lParam){
    	// 这里可以处理缩放立方体
	}
	winform.plus.onMouseHWheel = function(flags,delta,lParam){}
	winform.plus.onMouseEnter = function(wParam,lParam){}
	winform.plus.onMouseLeave = function(wParam,lParam){}
	winform.plus.onMouseHover = function(wParam,lParam){}
	winform.plus.onRightMouseDown = function(wParam,lParam){}
	winform.plus.onRightMouseUp = function(wParam,lParam){}
	
	winform.plus.dlgCode = 4/*_DLGC_WANTALLKEYS*/
	winform.plus.onKeyDown = function(keyCode,lParam,repeat){
		//注意设置 dlgCode 属性指定可接收的按键
	}
	winform.plus.onKeyUp = function(keyCode,lParam){}
	winform.plus.onContextMenu = function(x,y){ 
		
	}
**/

winform.show();
win.loopMessage();
```