lua是一种小巧、高效而且较为现代化的一种脚本语言,经常有人问到关于lua的学习的事情,一般来说我们推荐下面一些途径:
1)《Lua程序设计》,lua开发的必备手册,想必大家早就知道了
2)Lua的官方在线文档(http://www.lua.org/docs.html),包括一些lua api的使用,一些lua特性的介绍,虽然是英文的,但是有些lua开发基础的还是很容易看懂,遇到不清楚的lua api,建议首先查阅官方的文档
3)Lua的源代码 lua是一门完全开源的脚本语言,使用纯C编写,核心代码量不过几万行,各个模块分割较为明确,代码规范也较好,是深入学习lua的必经途径,也可以了解现代化的脚本语言是怎么设计的等
因为lua里面lua虚拟机和协程都用lua_state标识,所以为了避免出错,lua并没有对协程提供lua_pushstate(lua_state, luastate)的接口,那么怎么在c++的lua虚拟机里面操作协程? 比较典型的可以通过lua的注册表来保存,比如通过luaL_ref和lua_rawgeti,而不用去关心真正的lua数据类型。
比如想保存当前栈上第二个参数,可以
lua_pushvalue(luaState, 2);
int ref = luaL_ref(luaState, LUA_REGISTRYINDEX);
在需要使用这个协程的时候,放入同一个虚拟机的某个luastate的lua栈,可以使用
lua_rawgeti(luaState, LUA_REGISTRYINDEX, ref);
这个方法对于所有的lua类型都有效,只不过协程更特殊一些
Lua协程并不是真正的线程,和windows系统下面的线程不同,不是并发执行的,同一个时刻,同一个虚拟机里面只可以有一个协程在运行,并且需要显示的调度和切换,和windows下面的纤程概念类型,协程有自己的lua栈和指令指针、局部变量等,可以保存自己的执行流,但是又和其它协程共享同一个虚拟机里面的很多东西,比如全局变量表,注册表等。
协程编程属于较为高级的lua应用,可以进行保存当前的执行流,显式调度执行流,这些特性对于一般的程序员来说,都是用的较少的东西,正如windows下面的纤程,我们基本不用。协程是一个lua里面一个重型的应用,开销相对其它数据类型较大,所以我们并不推荐使用协程来开发。
之前有人询问到使用协程来实现异步框架,其实lua的闭包特性的支持和函数作为第一类值的特性,可以支持类似函数式语言的编程方式,已经可以较好的支持异步的特性,并且实现的异步和c++里面的异步概念相似,以回调的形式,显式把一个执行序列分隔开;假如使用协程,相当于每个涉及到异步操作的执行流都需要一个协程,来保存异步调用时候的lua执行流和指令指针,看起来较为直观,但是异步操作的执行流很难界定界线,在同一个虚拟机里面,哪些部分属于一个执行流,多个异步操作怎么分割到lua协程里面,并不会让工作量减少,而且写出的代码并不一定有回调形式的异步框架简单明了。
总起来说,我们并不推荐使用协程,这一块属于较高级的特性,lua的协程本身是否完善,过度使用会有什么问题,现在还都不清楚(大家应该都知道,涉及到lua虚拟机的bug和崩溃修复是最复杂),如果你对lua有足够的了解,并且对于这种协同编程有经验,那么可以适当的使用协程。
现在luaruntime内部,Lua的所有数值类型(TNUMBER)都是基于双精度浮点数double类型的,现在用的double实现一般都是IEEE 754标准,双精度浮点用4个字来表示,也即是8bytes,包括1bit的符号位,11bit的指数,和52bit的有效位,所以一般来说,如果你的int64整型数,有效位数为53位或者更少,那么完全可以转换成double,然后push到lua虚拟机里面,并且可以转换回来,不会造成精度的丢失;但是如果在lua代码涉及到一些数值运算,再转换过来,就可能会溢出截断,导致精度的丢失。如果你的int64只是用来表示文件大小、时间秒数等,一般来说53位足够了,但是如果是什么任务id、哈希值之类的,那么可能会使用int64里面任意一位,这种情况下不能直接当成double放到lua虚拟机里面了,因为该转换是不可逆的,会导致出各种莫名其妙的问题,一般来说在lua里面使用64位长整型有以下几种解决方案:
1) 使用字符串来表示int64,不方便的地方是无法在lua里面进行数值运算
2) 使用lightuserdata,同样无法在lua里面进行数值运算,甚至不能直接比较
3) 使用扩展的userdata,推荐使用XAF库里面定义的int64类,可以很方便的和lua number、string相互转换,支持绝大部分数学运算等
4) 在c/c++层避免使用64位的长整型,这样最彻底了,但是很多时候不太现实。
在lua里面,nil是一种数据类型,但是none表示某个位置没有数据,下面两种情况下,lua_isnone会是true
1) 尝试取一个超过lua栈顶位置的值的类型,那么就会返回一个none
2) 取超出当前函数所在闭包的upvals个数的值的类型,那么也会返回一个none
一般来说,在C封装里面,假如一个函数期望3个值,而想知道lua调用者某个参数有没有传递,可以使用lua_isnoneornil来判断,注意此时用lua_isnil可能返回false;假如调用者只传递了1个参数,那么对于第二、第三个参数来说,lua_isnoneornil就为true,但是lua_isnil为false
但是对于lua_pcall、XLLRT_LuaCall发起的lua调用,一般需要指定期望的lua返回值的个数,那么判断用户到底返回了几个有效值,使用lua_isnil和lua_isnoneornil是完全一样的,因为lua会用nil来填充返回值个数不够的栈槽,比如lua_pcall(luaState,2,3…),表示期望三个返回值,而用户只返回了1个,那么对于第二、第三个返回值,lua虚拟机会用nil来填充,所以lua_isnil也是返回true。
一般来说userdata都需要指定一个元表,可以通过元表里面有没有这个字段,来判断存在不存在这个方法。获取一个userdata的元表方法如下
local mt = getmetatable(obj)
local mt = obj.__index
但是这里有一点是需要注意的是,对于使用luaruntime注册的自定义类型,并且使用XLLRT_PushObject放到lua虚拟机去的userdata,那么上面两种方法是等同的,因为我们内部的实现,元表的__index字段是指向了元表自身,而用户在该userdata上面自定义的方法,都是放到默认的元表里面的,这样可以节省一个table的开销。
但是假如是绕开了luaruntime,自定义了元表,那么取决于你的实现,如果你让元方法__index指向的是另外一个table甚至是方法,那么上面两种方法就不等同了,判断自定义方法是否存在,应该使用 local mt = obj.__index,此时你拿到的并不是真正的元表,但是可以判断你的自定义方法是否存在。
枚举指定类型的上的所有方法,可以如下:
for k, v in pairs(object.__index) do
if type(v) == "function" then
print(k, v)
end
end
由于引擎本身会不断的增加新方法,一般来说使用者为了兼容性,需要根据某个类型上是否存在指定方法,来决定怎么操作,那么可以使用上述的方法来解决。
引擎内部的核心对象,是基于句柄机制的,包括下面几种:
1)元对象,比如LayoutObject/FlashObject/自定义control等
2)动画对象
3)Hostwnd对象
4)模板对象
5)对象树对象
上面几种对象的生命周期,都是基于句柄机制的,而非基于引用计数,这些对象在push到lua虚拟机后,lua对象持有的只是该对象的句柄,那么会导致的一个问题就是:在某个时刻,该对象已经被销毁,也就是句柄无效了,但是对应的某个或者某些lua对象还存在,那么在使用这些lua对象操作该真正对象的时候,可能会出现非预期的结果,比如操作失败,或者脚本错误等,那么怎么判断该对象时候被销毁了呢? 下面每种对象给出相应的判断方法:
1)元对象,GetClass方法
2)动画对象, GetClass方法
3)Hostwnd对象,GetClassName方法
4)模板对象,新版本(276)会增加GetClass方法,现在可以通过GetID来判断,只要返回值不为nil即有效,但有可能为空串
5)对象树对象,可以通过GetID来判断,只要返回值不为nil即有效
只要上面几个方法,返回值不为nil,那么说明该对象有效;否则说明该对象已经被销毁了,不可以再调用对象上的任何方法了
只有FrameHostWnd和ModalHostWnd两种hostwnd提供该函数,接收的参数是该窗口需要居中的窗口,参数取值可以是:
1)HWND,也就是窗口的GDI句柄
2)Xlue内部的hostwnd的句柄,就是hostwnd的lua对象
如果没有指定窗口,或者上面两种方式指定的窗口无效,那么会采用默认居中窗口,策略如下
1) 如果是popup窗口,那么相对于owner居中
2) 如果是child窗口,那么相对于parent居中
3) 对于popup窗口,如果owner不存在,那么取当前窗口所在屏幕的工作区进行居中,如果并无所在屏幕,那么取主屏幕进行居中。
大家可以根据xlue内部的居中策略,来传递相应的参数,尤其是popup的窗口,如果创建时候并没有指定owner,或者想对非owner窗口进行居中,那么需要传入合适的窗口句柄。
由于之前引擎并没有提供OnMouseEnter消息,所以大家都是使用OnMouseMove来模拟该该事件,一般来说还需要设置一个标志位。从今年四月份的版本起,xlue已经支持了OnMouseEnter事件,该事件和OnMouseLeave是严格对称的,事实上引擎内部实现,正是根据该对象上收到的第一次mousemove消息生成了对应的mouseenter事件,事件参数类型和mousemove完全一致,和稍后收到的第一次mousemove的参数也完全相同。
现在大家在开发一些新控件或者功能时候,可以使用该事件,这样可以使得逻辑更清楚明了,更容易控制,减少不必要的开销。
鉴于大家在开发中,经常碰到下面的需求:一个复杂控件,里面有多个对象,需要判断鼠标是否移动该控件的任意一个object,或者已经离开所有的object,这种需求看似简单,但是对于之间xlue,实现该功能的工作量却很大,涉及到的细节也很多。所以引擎增加了OnControlMouseEnter和OnControlMouseLeave,来方便基于上面需求的开发工作。
OnControlMouseEnter和OnControlMouseLeave只在自定义控件的object上触发,触发条件说明如下:
假如之前鼠标在object A上面,现在移动到了object B上面,其中A和B都可以接收鼠标和键盘消息,那么
1)对于某个control,如果A属于该control内部对象,而B不属于该control内部对象,那么该control会触发OnControlMouseLeave
2)对于某个control,如果A不属于该control内部对象,而B属于该control内部对象,那么该control会触发OnControlMouseEnter
3)对于某个control,如果A、B都属于该control内部对象,那么不会触发两个事件中的任意一个
4)OnControlMouseEnter和OnControlMouseLeave事件的触发均是递归向上的,从A或者B到所在对象树的根对象,该路径上所有满足1和2条件的control,都会触发对应的事件。
一个对象是否可以获取焦点,有下面几个关键要素:
1)对象是否可见,只有可见对象可以获得焦点
2)对象是否位enable,被disable的对象不可获得焦点
3)对象是否可以处理鼠标和键盘消息,如果该对象上并没有挂接任意的鼠标和键盘消息,那么不可以获得焦点
4)对象的zorder、位置信息和limitrect等影响该对象输入的,也都对获取焦点有影响
在一个对象上发生了左键、右键或者中键的down事件,会自动设置该对象焦点状态,同时触发以下事件:
1)获取焦点对象上触发OnFocusChange(true)事件
2)失去焦点对象上触发OnFocusChange(false)事件
3)外围控件上会根据具体情况,触发OnControlFocusChange事件,逻辑和OnControlMouseEnter、OnControlMouseLeave一致
判断一个元对象是否获取焦点,可以通过调用GetFocus()来判断判断一个对象树上当前获取焦点的元对象,可以通过调用GetFocusObject()来获取
如果一个对象,把左键、中键或者右键的down事件,向上路由到了直接或者间接parent,那么按照11里面描述的规则,对于某个parent来说,就好比收到了相应的鼠标事件。这里需要注意的是对于这个parent来说,并不知道该事件是由用户触发的,还是某个孩子调用了routetofather而来的,所以导致的结果就是该parent也会设置焦点。这样处理导致的一个现象就是:在一个对象里面点击了左键、中键或者右键,该对象首先获取了焦点,但是同时由于RouteToFather调用导致的副作用,焦点又转移到了相应的parent上面,从而和用户所预期的可能不一致
如果你的确是想让某个parent在处理相应的鼠标事件的同时,也获得焦点,那么可以安心的调用RouteToFather;但是这并不是你所预期的,那么请不要直接调用RouteToFather,而是调用更上层的、约定好的接口,来知会parent做相应的处理
一个对象变为invisible,可能是对象本身调用了SetVisible(false),也可能是某个直接或者间接parent调用了SetChildrenVisible(false);类似的,一个对象变为disable的时候,可能是对象本身调用了SetEnable(false),也可能是某个直接或者间接parent调用了SetChildrenEnable(false)。
在一个对象从visible切换到invisible,或者从enable切换到disable时候,可能会按照先后顺序,触发以下事件:
1)如果该对象在执行拖放操作中,那么会触发OnDragLeave事件
2)如果该对象正在捕获鼠标,那么会终止捕获,触发OnCaptureChange(false)事件如果该对象在MouseIn状态(也就是收到了OnMouseEnter但是没有leave),那么会触发OnMouseLeave和OnControlMouseLeave事件
3)如果该对象获取了焦点,那么会失去焦点,触发OnFocusChange(false)和OnControlFocusChange(false)事件
4)最后触发该对象自身的OnVisibleChange(false)事件或者OnEnableChange(false)事件
大家可能在开发控件过程会碰到这些问题,需要注意一下
Xlue内部的窗口有下面五种:
1)FrameHostWnd
2)ModalHostWnd
3)TipsHostWnd
4)MenuHostWnd
5)DragDropHostWnd
这五种窗口各司其职,分别在不同的场合有不同的作用,默认这些窗口都是top level的windows窗口,其中除了FrameHostWnd,其余的窗口也只可以作为top level的窗口,而FrameHostWnd可以作为子窗口存在,但是一般很少使用(关于child window和top level window这些windows下面窗口的基础知识,这里不再累述,大家可以去参考msdn里面关于窗口类别的描述)
创建一个作为子窗口的FrameHostWnd,需要先对FrameHostWnd调用SetParent,设置有效的父窗口进去,然后调用Create函数创建即可,如果SetParent指定的父窗口有效,那么Create函数会忽略Create传入的owner参数,创建一个子窗口。之所以我们不推荐使用子窗口,因为子窗口有很多限制,尤其对层窗口(layeredwindow)的限制,子窗口不可以作为每个像素自带alpha通道的窗口,其所在父窗口也不可以作为每个像素自带alpha通道的窗口,只可以是普通的窗口,视觉效果上大打折扣。
默认情况下,由于历史上的一些原因,有下面的规则:
1)FrameHostWnd的Create传入的参数为该窗口的owner(注意不是parent),可以通过GetOwner来获取popup窗口的owner;如果事先有调用SetParent,那么会创建一个子窗口,可以通过GetParent来获取父窗口
2)ModalHostWnd的DoModal传入的第一个参数是该窗口的owner,如果没有指定owner,并不会取SetParent设置进来的窗口,而是取当前的活动窗口来作为owner,可以通过GetOwner来获取该窗口的owner
3)MenuHostWnd的TrackPopupMenu传入的第一个参数,是menu所在窗口的owner,如果没有指定,该窗口会置顶,并按照系统默认设置owner,可以通过GetOwner来获取该窗口的owner
4)TipsHostWnd,在自动模式下,使用SetParent设置进来的窗口作为owner,可以通过GetOwner或者GetParent来获取该窗口的owner;在手动模式下,使用Create的第一个参数作为窗口的owner,如果没有指定,则按照系统默认,可以通过GetOwner来获取该窗口的owner
5)DragDropHostWnd不接受指定owner,均采用系统默认
对于FrameHostWnd,可以设置visible属性位false,先创建窗口,然后调用Show(4)来显示
ModalHostWnd内部是基于系统模态对话框的,所以必抢焦点;想设计不抢焦点的窗口,请使用FrameHostWnd
MenuHostWnd和TipsHostWnd内部自动控制显示,不抢焦点
目前涉及到颜色设置的对象有
TextObject
FillObject
EditObject
RichEditObject
FlashObject
LineObject
RectangleObject
一般来说,元对象的每个颜色属性都有对应的四个方法,比如TextObject的SetTextColor:
SetTextColor,设置颜色值,可以接收的类型包括
.字符串,和资源xml里面配置的一致,可以是RGB(0,0,255),RGBA(0,0,255,255)或者000000FF
.整型,内部会直接当做xl_color来处理
.XL_Color的lua封装类,一般是由别处得来
SetTextColorResID 设置颜色的资源id
GetTextColor 获取颜色值,一般是一个lua_integer类型
GetTextColorResID 获取颜色的资源id,如果一个该颜色值由SetTextColorResID或者xml里面配置id而来,那么返回对应的id,如果已经调用过SetTextColor,那么返回一个空串
在使用XGP库之前,一定要对其进行初始化操作,否则可能出现崩溃、操作失败等各种不可预期的问题
关于XGP得得初始化和反初始化,说明如下:
1)初始化
XGP库依赖于XLFSIO、XLGraphic和XLLuaruntime(带lua封装的版本)三个库,所以XGP库在初始化之前,首先需要初始化XLGraphic库,而XLGraphic库内部又会初始化XLFSIO库。
关于XLLuaruntime库的初始化,主要是创建全局默认的环境和默认的runtime,如果使用XGP的程序也同时使用XLUE,那么需要先初始化XLUE(XLUE_InitLoader),内部会初始化luaruntime的;假如不使用XLUE,只是使用XLGraphic,并且是带lua封装的XGP版本,那么需要自己初始化luaruntime,创建默认的env或者runtime;如果不希望XGP注册到默认的env,可以在初始化时候修改参数为不注册luahost,而稍后自己调用接口来注册到指定的env
初始化代码如下:
XLGraphicPlusParam plusParam;
XLGP_PrepareGraphicPlusParam(&plusParam);
//这里可以修改一些初始化选项
XLGP_InitGraphicPlus(&plusParam);
2)反初始化
只要保证晚于XLGraphic库的初始化即可,反初始化代码很简单:XLGP_UnInitGraphicPlus();
目前整个xlue整个系统的设计都是基于像素的,并没有设计基于英寸等的坐标系,因为像素都是整数,所以对象的位置也就只能是整数,所以在设置对象位置时候需要注意以下:
1) 如果是直接设置数值进去,而数值带小数,比如SetObjPos(10.3, 10.5, 10.7, 10.9),那么内部不会进行四舍五入,都会统一截断为10,所以在尽量不要设置带小数位的浮点数进去,假如需要四舍五入,那么请在调用函数之前设置,xml里面静态配置也是如此。
2)如果是设置表达式进去,那么表达式里面不能出现带小数点的数值,会导致表达式解析失败!比如SetObjPos(“10.5 + father.wdith”,….)会导致解析失败,对应值会一直是0,出现不可预期的结果;如果是一个核合法的表达式,但是里面使用除法操作,可能会出现浮点的运算结果,那么引擎在内部会自动舍去小数点部分,同样不会进行四舍五入,这点也需要特殊注意一下。
这种写法确实是可以……只是不为人所知,在method定义中,如果不指定func和file,那么默认取file为当前的xml文件名加上.lua作为file,func便是method的名字,
比如在control.xml里面,定义了如下的方法:
<method_def>
<CallMethod/>
</method_def>
相当于如下
<method_def>
<CallMethod file=”control.xml.lua” func=”CallMethod”/>
</method_def>
当初这么设计,是因为发现很多时候,file和func都是冗余的,可以通过method的名字和所在的xml推断出来,所以为了方便,便有这种写法。不过话又说回来,这种写法虽然用的不多,但是带来的问题确不少,尤其是涉及到模板合并和继承的地方,让我颇为头疼。不过出于兼容性的考虑,这个特性还是保留了下来,大家如果有需要可以使用这种写法
XlGraphic中对带alpha通道的图片,在加载完毕之后,都是经过预乘(PreMultiply)的,有做过alpha混合的同学应该对预乘有所了解,就是分别对位图的R、G、B三个通道利用对应的Alpha通道,做以下运算:Src = (Src * Alpha)/256。这样在做source alpha混合运算的时候,可以提高运算速度,尤其是在原图会被反复使用做alpha混合的情况下。
只有带alpha通道的图片格式被加载以后,才需要做预乘处理,主要是XLgraphic支持的png格式,和XGP新增支持的32位bitmap和icon这三种格式,而对于jpg、gif这种本身不带alpha通道的位图,就没有必要做预乘运算。
这里需要的注意的是:
1)通过XlGraphic加载的png图片,都已经是经过预乘运算的,所以如果你们想得到没有经过预乘运算的XL_BITMAP_HANDLE,可以使用gdi+自己加载,或者使用我们提供的不经过预乘的加载函数(XLGraphic/XGP)
2)进入xlue渲染链的图片,如果该图片附带alpha通道,那么一定要是经过预乘运算的,现在XLGraphic已经导出一个函数XL_PreMultiplyBitmap,可以用作对图片的预处理
3)XLGraphic内部在做alpha混合时候,不会对未经过预乘的bitmap做自动预乘运算
4)带apha通道的图片而又未经过预乘处理,进入渲染体系后,会导致渲染有误差,但可能不明显
5)预乘运算是不可逆的,假如原图的alpha通道值比较小,那么对应的RGB通道经过预乘以后,可能会成为0,导致该运算不可逆
每个元对象可以配置cursor id,关于cursor有几点需要说明一下:
1)Hostwnd的cursor id优先级最高,如果hostwnd配置或者设置了cursor id,那么会忽略对象上树上所有对象对cursor id的配置
2)对象树上的元对象对cursor的命中策略是基于zorder的,从高到低,并且满足以下条件:
.对象可见
.对象没有被disable
.对象配置了cursor id
.命中测试点落在对象区域(包括被limit的区域)
3)Cursor和光标caret不相同,和焦点没有关系