canvas 是给游戏、动画和自绘界面用的控件。自绘事件和动画接口与 plus 控件 相同(onDrawBackground、onDrawForeground、onDrawComplete、startAnimation、onAnimation,鼠标键盘事件也一样),plus 的自绘代码把 cls="plus" 换成 cls="canvas" 一般就能运行。
| plus | canvas | |
|---|---|---|
| 画布 | 每次重绘新建内存位图,伽马校正合成 | 常驻的 PARGB 内存位图(GDI+ 最快的格式),默认合成 |
| 动画节拍 | 窗口定时器,帧间隔不稳 | 跟随显示器刷新(60Hz 屏幕 16.7 毫秒,高刷屏更快) |
| 功能 | 文本、图标、皮肤、边框、圆角、进度条…… | 只有画布、鼠标键盘事件与动画 |
| 透明 | 剪切父窗口背景 | 不透明;orphanWindow(true) 后为透明分层窗口 |
见缝插针小游戏实测(200% DPI,880×1360 像素):plus 每帧绘图 24 毫秒、帧间隔约 31 毫秒;canvas 每帧 5 毫秒、帧间隔 16.7 毫秒。带整屏半透明遮罩的结算画面 plus 约 10 帧/秒,canvas 仍是 60 帧/秒。
游戏、动画和大量自绘一律用 canvas;按钮、图标文字、贴图等普通界面控件用 plus。canvas 上不要叠放其他控件,按钮直接画在画布上更好看(命中检测自己做);确实需要叠放时,对上层控件调用 orphanWindow(true)。
aardio 窗体设计器控件控工具箱控件排序:指针(用于在设计器上拖动控件),static,plus,canvas... , 第 3 个控件就是 canvas 。在控件工具箱中点选 canvas,然后在窗体设计器上拖动选好位置放置控件即可。canvas 在设计器中默认会设为白色背景。
import win.ui;
/*DSG{{*/
var winform = win.form(text="canvas";right=599;bottom=399)
winform.add(
canvas={cls="canvas";left=0;top=0;right=600;bottom=400;bgcolor=0x2A1A12;anchors={left=1;top=1;right=1;bottom=1};z=1}
)
/*}}*/
var x = 0;
var brush = gdip.solidBrush(0xFF4CC9F0);
winform.canvas.onDrawForeground = function(graphics,rc){
var dpi = winform.dpiScale(1);
graphics.scale(dpi,dpi); //之后按 100% DPI 的逻辑坐标绘图
graphics.fillCircle(brush,x,200,30);
}
winform.canvas.onAnimation = function(prevState,beginning,change,timestamp,duration,deltaMs){
x = (timestamp * 0.2) % 600; //按时间推进,不要按帧计数
return true; //返回 null 才停止(返回 false 不停止)
}
winform.onDestroy = function(){ brush.delete(); }
winform.canvas.startAnimation(); //省略间隔:每帧回调
winform.show();
win.loopMessage();
范例都在 ~/examples/canvas/ 目录:balls.aardio(弹跳小球)、game/(见缝插针等 10 个小游戏)、weather-card.aardio(动画天气卡片)、clock.aardio(透明分层窗口做的桌面时钟);~/examples/plus/av/celebrate.aardio 是盖在控件上面的透明特效层。
onDrawBackground、onDrawForeground、onDrawComplete。底色为 bgcolor,未指定时为父窗口的背景色,orphanWindow(true) 后为全透明。onDrawBackground 画在底色上面,返回值不影响绘图(与 plus 相同)。bgcolor 与 plus 一样用 0xBBGGRR 格式。背景只是一种纯色时直接指定 bgcolor,不必写 onDrawBackground。rc 是设备像素。游戏状态、碰撞建议用逻辑坐标,绘图时 graphics.scale(dpi,dpi),鼠标坐标除以 dpi。graphics.compositingQuality 默认为 0(线性混合),plus 是 2(伽马混合)。伽马混合下低 alpha 的半透明与细线明显更亮更清晰,但代价正比于混合流程覆盖的像素数:细线只贵 2 倍,整屏半透明遮罩贵十几倍。因此不要全局设为 2,静态背景应当缓存成位图并在建立缓存时开 2(见 GDI+ 绘图与性能优化)。gdip.bitmap(w,h,0xE200B)),与画布同尺寸的缓存背景用 SourceCopy 贴上去:graphics.compositingMode = 1 后 drawImageRect,画完改回 0。1520×1000 像素实测 SourceCopy 0.73 毫秒,默认混合 2.2 毫秒。onDrawBackground 里用 setClipPath 把内容裁剪到圆角矩形内,onDrawForeground 最后 resetClip 再描边(见 weather-card.aardio)。startAnimation() 立即回调一次 onAnimation,之后每次显示器刷新回调一次,回调后自动重绘。后台线程等系统合成器刷新后通知界面线程,不用窗口定时器。timestamp 是动画已运行的毫秒数,deltaMs 是距上一帧的毫秒数,都是高精度小数。高刷屏上回调更频繁,按时间推进才能在所有屏幕上速度一致。deltaMs 包含暂停的时长,游戏应限制单帧最大步长,例如 math.min(deltaMs,50)。startAnimation(interval) 的间隔小于 25 毫秒时每帧回调;大于等于 25 毫秒时按刷新周期的整数倍降频,例如 50 在 60Hz 屏幕上每 3 帧回调一次,适合不需要流畅的动画。startAnimation。事件由 win.ui.tracker 提供,与 plus 启用 notify 后相同:onMouseDown、onMouseUp、onMouseClick、onMouseMove、onMouseDrag、onMouseWheel、onKeyDown、onKeyUp、onFocusGot、onFocusLost 等。notify 默认为 true,一般不用写;设为 false 时鼠标穿透到下层窗口(例如父窗体),适合纯装饰的背景动画,运行时也可以修改。点击画布会自动获得键盘焦点。
画布默认接收方向键与字符键;要接收回车、Tab、Esc 设置 winform.canvas.dlgCode = 4/*_DLGC_WANTALLKEYS*/。cursor 属性可以换鼠标指针,例如 "hand"。
调用 orphanWindow(true) 后画布转为透明分层窗口,背景默认全透明,可以盖在其他控件上面画粒子。鼠标要穿透到下面的控件时加上 _WS_EX_TRANSPARENT:
winform.add(fx={cls="canvas";left=0;top=0;right=600;bottom=400;anchors={left=1;top=1;right=1;bottom=1};z=99});
winform.fx.orphanWindow(true); //在 winform.show() 之前调用
winform.fx.modifyStyleEx(,0x20/*_WS_EX_TRANSPARENT*/);
分层模式下文字不要用 ClearType(默认的抗锯齿即可)。
分层模式也能做任意形状的窗口:clock.aardio 的画布铺满一个无边框窗体后调用 orphanWindow(true),表盘缓存成位图,每帧只重画指针,圆形之外全透明;窗体本身用 win.ui.layered 变成全透明,只负责定位和拖动。
snap() 立即绘制一帧并返回副本(PARGB 格式的 gdip.bitmap),不依赖窗口显示,隐藏的窗口也能截图,适合无界面测试。用完要 delete。
var bmp = winform.canvas.snap();
var argb = bmp.getPixel(10,10);
bmp.delete();
画布的内存布局与 sys.mediaFoundation 视频写入器要求的帧一致(PARGB、自上而下、步幅为宽×4)。在 onDrawComplete 里把 owner.bitmap 传给写入器即可录下每一帧(画布尺寸必须与视频一致):
winform.canvas.onDrawComplete = function(graphics,rc,hMemBitmap){
if(writer) writer.writeFrame(owner.bitmap);
}
owner.bitmap 在画布尺寸改变时会重建,不要长期保存或删除它。
游戏音效用 sys.soundPool:启动时载入,之后一句 play(名字),多个声音可以同时响,不卡画面。