Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Run Client in Lua Script Completely

5.00/5 (4 votes)
31 Oct 2022CPOL5 min read 5.7K   83  
By implementing a UI library and exporting its necessary components to lua script, we can create a beautiful UI and implement relevant logic in Lua script completely.
I'm the author of a direct UI library named SOUI. I'm going to introduce the recent finished work, with which one can build a client program using Lua script completely. Since soui 4.0, I try to export all of classes to C by building it based on COM technology, and expect it can be called by other languages besides C++. As COM based interfaces had been done, it becomes easy to export those interfaces to lua script as well. Using lua script, you can load program resources, create windows, connect events of UI widgets to event slots wroten in Lua, and so on. You can treat it as a simple browser yet only take up less than 2M space.

Image 1

Introduction

To build a client program, common options may include mfc, wtl, qt, etc. Recently, many companies also choose to use CEF3 to build their client programs. As CEF3 is a browser core exactly, most of the work can be done by JavaScript, which grants it with great flexibility despite its huge resource occupation. Although, some of the new UI frameworks provide script module as well, they can't provide the capability with which a programmer can finish a project based on script only as CEF3 does. I thought, a client framework with complete script capability and slim volume and high efficiency should be a good choice for a client program.

Code Modules

In the source package, we provided soui4.08.sln and soui4.22.sln. Select a favored sln to open and build all of modules, then run SLuaDemo.ex. You will see how it works. 

The whole solution includes eight modules:

  1. utilities4, the module provides some basic tool classes such as SStringA, SStringW, SXml (a wrapper of pugixml).
  2. soui-system-resource, the module is a simple resource container.
  3. soui4, the module is the core module, which provides the direct UI framework.
  4. imgdecoder-gdip, the module using gdiplus to do image decode. There are other alternatives such wic, stb, png in the whole git repo.
  5. render-gdi, the render module, render drawing to bitmap based on GDI api. render-skia is available in the whole git repo, which provides much higher efficiency than gdi.
  6. lua-54, the lua engine. Here, we are using lua 5.4.4.
  7. ScriptModule-lua, the module that exports the used COM interface to lua. We use lua_tinker (https://github.com/zupet/LuaTinker) to export our interface to lua. We update lua_tinker to support x64 and can support call mode such as WINAPI in Visual Studio 2008 (It seems 2015+ doesn't have such a problem).
  8. SLuaDemo, the executable module, which demos how to integrate the above mentioned modules to build a client program and do all of logic in lua script.

The following _tWinMain shows how to start a demo. Those code only composite necessary components to make up the demo, without doing any actual logics.

C++
//
    int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, 
                         LPTSTR lpstrCmdLine, int /*nCmdShow*/)
    {
        HRESULT hRes = OleInitialize(NULL);
        SASSERT(SUCCEEDED(hRes));
        SetDefaultDir();
        int nRet = 0;
        {
            SouiFactory souiFac;
            SComMgr comMgr;
            SApplication *theApp = InitApp(comMgr,hInstance);
            LoadSystemRes(theApp,souiFac);            //load system resource
            LoadLUAModule(theApp,comMgr);             //load lua script module.
            {
                SAutoRefPtr<IScriptModule> script;
                theApp->CreateScriptModule(&script);  //create a lua instance
                script->executeScriptFile("main.lua");//load lua script
                TCHAR szDir[MAX_PATH];
                GetCurrentDirectory(MAX_PATH,szDir);
                SStringA strDir = S_CT2A(szDir);
                nRet = script->executeMain
                (hInstance,strDir.c_str(),NULL);      //execute the main function 
                                                      //defined in lua script
            }
            theApp->Release();
        }
        OleUninitialize();
        return nRet;
    }

All of the actual logic is located in main.lua. Before explaining the code of main.lua, I shall simply explain the resource package and layout XML at first. In soui, all of drawable is defined as ISkinObj.

C++
#undef INTERFACE
#define INTERFACE ISkinObj
DECLARE_INTERFACE_(ISkinObj, IObject)
{
    #include <interface/SobjectApi.h>  //define IObject methods
    STDMETHOD_(void, DrawByState2)(CTHIS_ IRenderTarget * pRT,
    LPCRECT rcDraw, DWORD dwState, BYTE byAlpha) SCONST PURE;
    STDMETHOD_(void, DrawByState)(CTHIS_ IRenderTarget * pRT,
    LPCRECT rcDraw, DWORD dwState) SCONST PURE;
    STDMETHOD_(void, DrawByIndex2)(CTHIS_ IRenderTarget * pRT,
    LPCRECT rcDraw, int iState, BYTE byAlpha) SCONST PURE;
    STDMETHOD_(void, DrawByIndex)(CTHIS_ IRenderTarget * pRT,
    LPCRECT rcDraw, int iState) SCONST PURE;
    STDMETHOD_(SIZE, GetSkinSize)(CTHIS) SCONST PURE;
    STDMETHOD_(int, GetStates)(CTHIS) SCONST PURE;
    STDMETHOD_(BYTE, GetAlpha)(CTHIS) SCONST PURE;
    STDMETHOD_(void, SetAlpha)(THIS_ BYTE byAlpha) PURE;
    STDMETHOD_(void, OnColorize)(THIS_ COLORREF cr) PURE;
    STDMETHOD_(int, GetScale)(CTHIS) SCONST PURE;
    STDMETHOD_(ISkinObj *, Scale)(THIS_ int nScale) PURE;
};

One of the main usages of resource package is to define ISkinObj described by XML and image files.

As we have defined necessary drawable(ISkinObj), we can reference those ISkinObj by name in layout XML. Syntax of soui layout is very similar to Android layout. Soui provides three main layout types including linear layout, gridlayout and anchor layout. Anchor layout is similar to relative layout in Android but is more flexible in my opinion. For example, a blank layout maybe looks like:

XML
<soui>
   <!-- define a root window that includes 2 children layout from left to right -->
   <root width="400" height="300" layout="hbox" >
       <window name="child1" size="100,-2" colorBkgnd="@color/red"/>
       <window name="child2" size="100,-2" colorBkgnd="@color/blue"/>
   </root>
</soui>

All resource files are indexed in uires.idx file.
After defined UI layout, we can write corresponding logic code in main.lua.

Lua
function onBtnDialog(e) --show about dialog
    slog("onBtnDialog");
    local btnSender = toSWindow(e:Sender());
    local hParent = btnSender:GetHostHwnd();
    local souiFac = CreateSouiFactory();
    local dialog = souiFac:CreateHostDialog(T"layout:dlg_test");
    souiFac:Release()
    local ret = dialog:DoModal(hParent);
    dialog:Release();
end

function on_toggle(e) --animtion slide the left pane
    slog("on toggle");
    local hostWnd = GetHostWndFromObject(e:Sender());
    local leftPane = hostWnd:FindIChildByName(L"pane_left",-1);

    local toggle = toSWindow(e:Sender());
    local isChecked = toggle:IsChecked();
    local theApp = GetApp();
    local anim;
    if(isChecked == 1) then
        slog("on toggle true".. isChecked);
        anim = theApp:LoadAnimation(T"anim:slide_show");
    else
        slog("on toggle false".. isChecked);
        anim = theApp:LoadAnimation(T"anim:slide_hide");
    end
    leftPane:SetAnimation(anim);
    anim:Release();
end

function main(hinst,strWorkDir,strArgs)
    slog("main start");
    local souiFac = CreateSouiFactory();
    local resProvider = souiFac:CreateResProvider(1);-- create a resource
                                                     -- package from a file path
    InitFileResProvider(resProvider,strWorkDir .."\\uires");--init resource
                                                     -- package from file
    local theApp = GetApp();
    local resMgr = theApp:GetResProviderMgr();
    resMgr:AddResProvider(resProvider,T"uidef:xml_init"); -- add the resource
                                                          -- package to program
    resProvider:Release();

    local hostWnd = souiFac:CreateHostWnd(T"LAYOUT:dlg_main"); --define a host
                                                      -- window using layout file
     --defined in dlg_main. host window is a win32 native hwnd, which contains
                                                      -- all of direct ui widgets.
    local hwnd = GetActiveWindow();
    hostWnd:Create(hwnd,0,0,0,0); --create the host window
    hostWnd:ShowWindow(1); --1==SW_SHOWNORMAL
    initListview(hostWnd);

    --...

    local btnDialog = hostWnd:FindIChildByNameA("btn_dialog",-1); -- find command
                                                      -- button named "btn_dialog"
    LuaConnect(btnDialog,10000,"onBtnDialog");        -- connect event of commend
                                                      -- to responding function
                                                      -- "onBtnDialog"

    local btnSwitch = hostWnd:FindIChildByNameA("tgl_left",-1); -- find switch
                                                      -- button named "tgl_left"
    LuaConnect(btnSwitch,10000,"on_toggle"); -- connect event of commend
                                             -- to responding function "on_toggle"

    souiFac:Release(); -- release souiFac object

    slog("main done");
    return theApp:Run(hostWnd:GetHwnd()); --run the app until event loop broken.
end

From the above lua script, we can see the basic steps including:

  1. Create a resprovider object and initialize it with file path or other source depending on which resprovider type was created. Resource type can be a file path, a DLL, or a zip file, etc. Then add it to the application.
  2. Create a host window object, and initialize it with a resource name defined in the previous resource package. Then call hostWnd:create to create a hwnd and call hostWnd:ShowWindow to show.
  3. Find child by name and connect its event to responding functions.

OK, that's all!

How to Export a C++ Object to Lua

We using lua_tinker to do the work. Similar code may include luabind, tolua++, etc. I'm not familiar with the other packages. In using lua_tinker, I found it may do wrong things if I try to export complicated classes such as a class has multiple base classes. Fortunately, we had implemented all interface using COM, and lua_tinker support COM is good enough to export them to lua. If you want to know which class or API can be used in lua script, look into the source code of module scriptmodule-lua. You will see code like:

C++
BOOL ExpLua_IObjRef(lua_State *L)
{
    try{
        lua_tinker::class_add<IObjRef>(L,"IObjRef");
        lua_tinker::class_def<IObjRef>(L,"AddRef",&IObjRef::AddRef);
        lua_tinker::class_def<IObjRef>(L,"Release",&IObjRef::Release);
        lua_tinker::class_def<IObjRef>(L,"OnFinalRelease",&IObjRef::OnFinalRelease);

        return TRUE;
    }catch(...)
    {
        return FALSE;
    }
}

Using the code, we exported a COM interface named IObjRef that includes three methods. Thus, if we got a IObjRef object and stored it to variable obj, we can call obj:AddRef to add reference to this object.

Full soui4 Repository

In the source package, only used modules were presented here. One of the important ideas of soui is to provide replaceable modules for external module look like plugins. There are many functional similar modules you can find in the full git repo. We store soui4 in github, https://github.com/soui4/soui.

Conclusion and Points of Interest

In this article, I'm not going to sell soui4 to you. You can replace soui4 in any other corresponding modules, only if they can provide similar functions. What I'm try to sell is the idea that you can run all logic in script and make it look like a mini browser. Although soui4 is not free for commercial usage (pay a bit of coin), I think you will find many useful points if you look into the code carefully. Thank you very much! Any suggestions are welcome!

Note that I had mentioned the term of COM many times. I should apologize for it because what I had used is not exactly COM but COM kind of technology.

History

  • 31st October, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)