像寫網頁一樣做客戶端界面可能是很多客戶端開發的理想。
做好一個可以實現和用戶交互的動態網頁應該包含兩個部分:使用html做網頁的布局,使用腳本如vbscript,javascript做用戶交互的邏輯。當需求變化時,只需要在服務端把相關代碼調整一下,用戶即可看到新的內容(界面)。
傳統的客戶端程序開發流程和網頁開發可能完全不同。
首先是界面的布局,在老式的界面布局過程中,程序員先在界面上放好各種控件,然后需要自己通過相應的代碼來維護界面在不同狀態下控件的顯示狀態及位置。當界面中元素很多時,單純布局的代碼可能就會非常的復雜。
界面做好了,程序員需要增加代碼響應界面中UI元素對應的邏輯。如邏輯比較固定,一旦做好就不需要變化時,這樣的實現的程序效率可能更高。然而做客戶端的人可能都經歷過修改界面的問題。簡單的控件坐標調整還好辦,如果程序的界面風格甚至整個程序的運行邏輯都要變化時,使用傳統的開發方式實現的客戶端程序更新起來會非常困難,有時甚至變得不可能實現。
近年來,在各種新興的界面開發庫中使用XML來描述布局已經得到了廣泛的應用。使用XML描述布局的基本出發點應該和使用HTML描述網頁布局類似,關鍵是解決界面元素的批量創建及元素間位置的自動布局及UI大小變化后的UI元素自適應。然而到目前為止在UI庫中使用腳本來實現邏輯控制的還不多見,據我所知,在流行的UI庫中只有qt, bolt這兩個項目支持。無論是QT還是Bolt,這兩個都是重量級的UI庫,而且QT中使用腳本(Bolt不了解)的方式也和網頁開發想去甚遠。
盡管在DuiEngine時代的代碼庫中就包含了一個LUA腳本模塊,但那個時候的腳本模塊也僅限於簡單的LUA腳本和C++控件代碼之間簡單的相互調用,還沒有形成一個清晰的應用模式。
經過2015年春節期間對腳本模塊的重構,終於在SOUI中實現了和網頁開發基本一樣的界面開發流程。
先看一個SOUI DEMO中100%使用LUA腳本實現的小游戲效果:
下面我們再看看實現上述效果用到的代碼:
在SOUI中,腳本模塊和其它如渲染模塊等一樣是使用類似插件形式實現的,當然首先需要我們在程序的入口加載LUA腳本模塊:
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR /*lpstrCmdLine*/, int /*nCmdShow*/) { //必須要調用OleInitialize來初始化運行環境 HRESULT hRes = OleInitialize(NULL); SASSERT(SUCCEEDED(hRes)); // LoadLibrary(L"E:\\soui.taobao\\richedit\\Debug\\riched20.dll"); int nRet = 0; SComMgr *pComMgr = new SComMgr; { //... //定義一個唯一的SApplication對象,SApplication管理整個應用程序的資源 SApplication *theApp=new SApplication(pRenderFactory,hInstance); #ifdef DLL_CORE //加載LUA腳本模塊,注意,腳本模塊只有在SOUI內核是以DLL方式編譯時才能使用。 bLoaded=pComMgr->CreateScrpit_Lua((IObjRef**)&pScriptLua); SASSERT_FMT(bLoaded,_T("load interface [%s] failed!"),_T("scirpt_lua")); theApp->SetScriptFactory(pScriptLua); #endif//DLL_CORE //加載全局資源描述XML theApp->Init(_T("xml_init")); { //創建並顯示使用SOUI布局應用程序窗口,為了保存窗口對象的析構先於其它對象,把它們縮進一層。 CMainDlg dlgMain; dlgMain.Create(GetActiveWindow(),0,0,800,600); dlgMain.GetNative()->SendMessage(WM_INITDIALOG); dlgMain.CenterWindow(); dlgMain.ShowWindow(SW_SHOWNORMAL); nRet=theApp->Run(dlgMain.m_hWnd); } //應用程序退出 delete theApp; //... } exit: delete pComMgr; OleUninitialize(); return nRet; }
其次我們需要在XML布局中的SOUI節點下增加一個script的子節點:
<SOUI trCtx="dlg_main" title="SOUI-DEMO version:%ver%" bigIcon="LOGO:32" smallIcon="LOGO:16" width="600" height="400" appWnd="1" margin="5,5,5,5" resizable="1" translucent="1" alpha="255"> <skin> <!--局部skin對象--> <gif name="gif_penguin" src="gif:gif_penguin"/> <apng name="apng_haha" src="apng:apng_haha"/> </skin> <style> <!--局部style對象--> <class name="cls_edit" ncSkin="_skin.sys.border" margin-x="2" margin-y="2" /> </style> <script src="lua:lua_test"> <!--當沒有指定src屬性時從cdata段中加載腳本--> <![CDATA[ function on_init(args) SMessageBox(0,T "execute script function: on_init", T "msgbox", 1); end function on_exit(args) SMessageBox(0,T "execute script function: on_exit", T "msgbox", 1); end function onEvtTest2(args) SMessageBox(0,T "onEvtTest2", T "msgbox", 1); return 1; end function onEvtTstClick(args) local txt3=SStringW(L"append",-1); local sender=toSWindow(args.sender); sender:GetParent():CreateChildrenFromString(L"<button pos=\"0,0,150,30\" on_command=\"onEvtTest2\">lua btn 中文</button>"); sender:SetVisible(0,1); return 1; end ]]> </script> <root class="cls_dlg_frame" cache="1" on_init="on_init" on_exit="on_exit"> <caption pos="0,0,-0,30" show="1" font="adding:8"> <icon pos="10,8" src="LOGO:16"/> <text class="cls_txt_red">SOUI-DEMO version:%ver%</text> <imgbtn id="1" skin="_skin.sys.btn.close" pos="-45,0" tip="close" animate="1"/> <imgbtn id="2" skin="_skin.sys.btn.maximize" pos="-83,0" animate="1" /> <imgbtn id="3" skin="_skin.sys.btn.restore" pos="-83,0" show="0" animate="1" /> <imgbtn id="5" skin="_skin.sys.btn.minimize" pos="-121,0" animate="1" /> <imgbtn name="btn_menu" skin="skin_btn_menu" pos="-151,2" animate="1" /> </caption> <other/> </root> </SOUI>
在script節點中,可以使用src屬性來指定腳本所在的資源文件,也可以將腳本直接使用cdata寫在script節點中,但是src中指定的腳本優先。
注意,上述XML中我們還在root結點中增加了兩個新的屬性:on_init, on_exit,這兩個屬性告訴程序在界面初始化完成及界面銷毀前需要執行的兩個腳本函數。
實現了上面兩步,在SOUI中使用腳本的准備工作已經就緒。
為了實現上圖的跑馬機效果,首先我們需要在一個界面中使用XML實現基本的界面元素布局:
<?xml version="1.0" encoding="utf-8"?> <include> <window size="full,full" name="game_wnd" on_size="on_canvas_size" id="300"> <text pos="0,0,-0,@30" colorBkgnd="#cccccc" colorText="#ff0000" align="center">SOUI + LUA 跑馬機</text> <window pos="0,[0,-0,-50"> <window pos="0,0,-64,-0" name="game_canvas" clipClient="1" colorBkgnd="#ffffff"> <!--比賽場地--> <gifplayer name="player_1" float="1" skin="gif_horse"> <text pos="0,%1" colorText="rgb(255,0,0)" font="size:20">1</text> </gifplayer> <gifplayer name="player_2" float="1" skin="gif_horse"> <text pos="0,0" colorText="rgb(255,0,0)" font="size:20">2</text> </gifplayer> <gifplayer name="player_3" float="1" skin="gif_horse"> <text pos="0,0" colorText="rgb(255,0,0)" font="size:20">3</text> </gifplayer> <gifplayer name="player_4" float="1" skin="gif_horse"> <text pos="0,0" colorText="rgb(255,0,0)" font="size:20">4</text> </gifplayer> <gifplayer name="flag_win" float="1" skin="gif_win" show="0" id="400"/> <text pos="|0,0">賠率:</text> <text pos="[0,0,@20,[0" colorText="#ff0000" name="txt_rate">4</text> <hr pos="-1,0,-0,-0" mode="vertical" colorLine="#ff0000"/> </window> <window pos="[0,0,-0,-0"> <window pos="0,%12.5,@64,@64" offset="0,-0.5" skin="img_coin" id="1" tip="下注1號馬" on_command="on_bet">0</window> <window pos="0,%37.5,@64,@64" offset="0,-0.5" skin="img_coin" id="2" tip="下注2號馬" on_command="on_bet">0</window> <window pos="0,%62.5,@64,@64" offset="0,-0.5" skin="img_coin" id="3" tip="下注3號馬" on_command="on_bet">0</window> <window pos="0,%87.5,@64,@64" offset="0,-0.5" skin="img_coin" id="4" tip="下注4號馬" on_command="on_bet">0</window> </window> </window> <window pos="10,[5,-0,-0" name="game_toolbar"> <button name="btn_run" pos="0,|0,@100,@30" offset="0,-0.5" tip="run the game" on_command="on_run">run</button> <text pos="]-5,0,@-1,-0" offset="-1,0">現有金幣:</text> <text pos="-64,0,@64,-0" name="txt_coins" align="center">100</text> </window> </window> </include>
在上述XML中,我們使用gifplayer控件定義了4匹馬,但是我們並沒有使用pos屬性定義它的位置,因為它的位置是在變化的,我們需要使用腳本在控制。注意上述XML中為幾個按鈕控件指定的on_command屬性。on_command屬性是窗口的點擊事件屬性,指定后可以執行腳本中對應的函數。
事實上在SOUI中每一個事件都定義了一個在XML中可以映射到腳本腳本函數的屬性,這里簡單介紹一下這個屬性在哪里實現的:
class SOUI_EXP EventCmd : public TplEventArgs<EventCmd> { SOUI_CLASS_NAME(EventCmd,L"on_command") public: EventCmd(SObject *pSender):TplEventArgs<EventCmd>(pSender){} enum{EventID=EVT_CMD}; };
通過上面代碼,可以發現,這個屬性實際上就是這個事件類在SOUI中的字符串名字。
下面我們看看實現這個跑馬機真正使用的LUA腳本代碼:
win = nil; tid = 0; gamewnd = nil; gamecanvas = nil; players = {}; flag_win = nil; coins_all = 100; --現有資金 coins_bet = {0,0,0,0} --下注金額 bet_rate = 4; --賠率 prog_max = 200; --最大步數 prog_all = {0,0,0,0} --馬匹進度 function on_init(args) --初始化全局對象 win = toHostWnd(args.sender); gamewnd = win:GetRoot():FindChildByNameA("game_wnd",-1); gamecanvas = gamewnd:FindChildByNameA("game_canvas",-1); flag_win = gamewnd:FindChildByNameA("flag_win",-1); players = { gamecanvas:FindChildByNameA("player_1",-1), gamecanvas:FindChildByNameA("player_2",-1), gamecanvas:FindChildByNameA("player_3",-1), gamecanvas:FindChildByNameA("player_4",-1) }; --布局 on_canvas_size(nil); math.randomseed(os.time()); --SMessageBox(0,T "execute script function: on_init", T "msgbox", 1); end function on_exit(args) --SMessageBox(0,T "execute script function: on_exit", T "msgbox", 1); end function on_timer(args) if(gamewnd ~= nil) then local rcCanvas = gamecanvas:GetWindowRect2(); local heiCanvas = rcCanvas:Height(); local widCanvas = rcCanvas:Width(); local rcPlayer = players[1]:GetWindowRect2(); local wid = rcPlayer:Width(); local hei = rcPlayer:Height(); local win_id = 0; for i = 1,4 do local prog = prog_all[i]; if(prog<prog_max) then prog = prog + math.random(0,5); prog_all[i] = prog; local rc = players[i]:GetWindowRect2(); rc.left = rcCanvas.left + (widCanvas-wid)*prog/prog_max; players[i]:Move2(rc.left,rc.top,-1,-1); else win_id = i; local rc = players[i]:GetWindowRect2(); rc.left = rcCanvas.left + (widCanvas-wid); players[i]:Move2(rc.left,rc.top,-1,-1); end end if win_id ~= 0 then gamewnd:FindChildByNameA("btn_run",-1):FireCommand(); coins_all = coins_all + coins_bet[win_id] * 4; gamewnd:FindChildByNameA("txt_coins",-1):SetWindowText(T(coins_all)); coins_bet = {0,0,0,0}; local rcPlayer = players[win_id]:GetWindowRect2(); local szFlag = flag_win:GetDesiredSize(rcPlayer); rcPlayer.right = rcPlayer.left + szFlag.cx; rcPlayer.bottom = rcPlayer.top + szFlag.cy; rcPlayer:OffsetRect(-szFlag.cx,-szFlag.cy/3); flag_win:Move(rcPlayer); flag_win:SetVisible(1,1); flag_win:SetUserData(win_id); for i= 1,4 do gamewnd:FindChildByID(i,-1):SetWindowText(T("0")); end end end end function on_bet(args) if tid ~= 0 then return 1; end local btn = toSWindow(args.sender); if coins_all >= 10 then id = btn:GetID(); coins_bet[id] = coins_bet[id] + 10; coins_all = coins_all -10; btn:SetWindowText(T(coins_bet[id])); gamewnd:FindChildByNameA("txt_coins",-1):SetWindowText(T(coins_all)); end return 1; end function on_canvas_size(args) if win == nil then return 0; end local rcCanvas = gamecanvas:GetWindowRect2(); local heiCanvas = rcCanvas:Height(); local widCanvas = rcCanvas:Width(); local szPlayer = players[1]:GetDesiredSize(rcCanvas); local wid = szPlayer.cx; local hei = szPlayer.cy; local rcPlayer = CRect(0,0,wid,hei); local interval = (heiCanvas - hei*4)/5; rcPlayer:OffsetRect(rcCanvas.left,rcCanvas.top+interval); for i = 1, 4 do local rc = rcPlayer; rc.left = rcCanvas.left + (widCanvas-wid)*prog_all[i]/prog_max; rc.right = rc.left+wid; players[i]:Move(rc); rcPlayer:OffsetRect(0,interval+hei); end local win_id = flag_win:GetUserData(); if win_id ~= 0 then local rcPlayer = players[win_id]:GetWindowRect2(); local szFlag = flag_win:GetDesiredSize(rcPlayer); flag_win:Move2(rcPlayer.left-szFlag.cx,rcPlayer.top-szFlag.cy/3,-1,-1); end return 1; end function on_run(args) local btn = toSWindow(args.sender); if tid == 0 then prog_all = {0,0,0,0}; on_canvas_size(nil); tid = win:setInterval("on_timer",200); btn:SetWindowText(T"stop"); flag_win:SetVisible(0,1); else win:clearTimer(tid); btn:SetWindowText(T"run"); tid = 0; end return 1; end function on_btn_select_cbx(args) local btn = toSWindow(args.sender); local cbxwnd = btn:GetWindow(2);--get previous sibling local cbx = toComboboxBase(cbxwnd); cbx:SetCurSel(-1); end
在on_init中,我們獲得必須的幾個UI控件,並保存到LUA的全局變量中。
在on_canvas_size中,我們根據UI大小,自動調整4匹馬的位置。
然后在幾個按鈕的事件響應函數中響應用戶操作。
至此一個簡單的跑馬機效果就完成了。
也許有人覺得這個示例太簡單,但請想象一下,采用類似的方法,以后UI及邏輯都可以在服務器控件,客戶端就像網頁一樣每天都可以從服務器更新界面會是什么場景,那時客戶端可真就成了客戶了。
后記:在SOUI中實現和網頁開發類似的腳本功能算是SOUI 2015年非常重要一次更新,本來早就想寫一篇這樣的博客來介紹,無奈由於原來使用的lua_tinker 0.5c版本導出C++類到LUA還有這樣那樣的問題,一直沒有找到合適的解決方案。
雖然也有比較成熟的方法如,luabind, tolua++等,但是總覺得太龐大,不適合SOUI這個非常輕量級的UI庫。
這期間也嘗試了如luawrapper, fflua,以及網上找到的別人修改的各版本lua_tinker,但都不理想。
使用其它導出方法就不說了,我也沒有深入。使用lua_tinker主要的問題就是導出的子類不能正常調用基類的方法,這一點很頭痛,雖然用變通的方法可以獲得調用基類方法的能力,但是看起來有點背叛了OOP的思想。
直到今天才搞明白不能調用基類成員最主要的問題來自C++對象的多繼承,明白了這個問題才能規避問題,才敢把這個LUA模塊拿出來講解,也順便把SOUI中使用的LUA內核和5.1升級到了5.2.3。