Implement a fancy UI for nsis

We have known windows standard installation UI for years, and the good thing is we are so fimilar with the UI, we can even click blindly without meeting problem. However, client applications are getting more beautiful UIs, should we apply the same thing to the client installers?

Let’s take a look of tasks that an installer usually do:

1. let users to make selections of features

2. extract files to a proper location

3. setting up running environment, say install runtime, prepare shortcuts, etc

4. compress files to save download/copy time

5. more?

Basiclly, that’s all. In another side, the setup work is quite different in different applications: some software may depend resources from network, it will do download during installation; some software may want to display information from a website, it will embed a web browser inside installer UI.

Nsis is a good installation system which supports kinds of plugins. However, it requires a dev to know the nsis internal deeply, especially when a dev wants to implement a UI completely different from the standard installation UI.

As the result, we can see numbers of dev do like this:

1. it implements a fancy UI in C++, employing kinds of UI library

2. it has to implement decompression logic, usually from a zip file

3. it has to implement a packer that prepares the zip file, and maybe append the zip file into the installer exe.

4. it has to implement kinds of setup code so that the installer can do the real installation work, for example, create service, create shortcut, etc.

Is there any shortcoming if we do like this?

Besides the UI, actually most of the work are  re-inventing the wheel, they are already part of an existing installation system or a plugin of that installation system. More still, an installer may keep changing inside a product lifecycle, and most of them are only to change installer UI layout, or to adjust minor installation flow. It’s a nightmare to handle these in C++.

Back to Nsis, we should take a look of its existing installation flow before continuing the journey.

image

We write nsis script similar to this:

!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH

Section Install
DetailPrint “installing…”
SectionEnd

Function .onInit
FunctionEnd

The compiled installer will call the page code one by one, until instfiles page, it will create a workthread to do the reall install job which is described in “sections”.

Let’s take a look at the nsis page

4.5.4 Page
custom [creator_function] [leave_function]  [/ENABLECANCEL]
  OR
internal_page_type [pre_function] [show_function] [leave_function] [/ENABLECANCEL]

After that, take a look at nsis source code

BOOL CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_INITDIALOG || uMsg == WM_NOTIFY_OUTER_NEXT)
{
// ….

mystrcpy(g_tmp,g_caption);
GetNSISString(g_tmp+mystrlen(g_tmp),this_page->caption);
my_SetWindowText(hwndDlg,g_tmp);

#ifdef NSIS_SUPPORT_CODECALLBACKS
// custom page or user used abort in prefunc
if (ExecuteCodeSegment(this_page->prefunc, NULL) || !this_page->dlg_id) {
goto nextPage;
}
#endif //NSIS_SUPPORT_CODECALLBACKS

    {
int ret=DialogBox(g_hInstance,MAKEINTRESOURCE(IDD_INST+dlg_offset),0,DialogProc);
#if defined(NSIS_SUPPORT_CODECALLBACKS) && defined(NSIS_CONFIG_ENHANCEDUI_SUPPORT)
ExecuteCallbackFunction(CB_ONGUIEND);
#endif

Nsis uses the messge loop provided by DialogBox, and our prefunc can be called in WM_INITDIALOG.

So if we provide customized prefunc, and provide our own message loop. we are able to overwrite the UI completely.

Page custom dui_create
Function dui_create
${DUI_RegisterInitFunc} OnDuiInit
${DUI_RegisterDeinitFunc} OnDuiDeinit

SetOutPath “$PLUGINSDIR\res”
file /r res\*.*

dui::Init /NOUNLOAD “$PLUGINSDIR\”
dui::Run /NOUNLOAD
dui::Uninit
FunctionEnd

Is it very similar to a normal client’s WinMain code?

So far, we have bridged a ui message loop into nsis  ui thread. Next, we need to enable the the scripting ability for the UI, so that we can use script to handle UI event.

Windows GUI is event driven, uses message queue to mange events. Similar to Windows OS, UI libraries are also event driven, but usually use callbacks to allow applications to handle events, so they provide registration functions. Take the libray I’m using as an example:

virtual void RegisterLButtonDownEvent(boost::function<void(void)> event);
virtual void RegisterLButtonUpEvent(boost::function<void(void)> event);
virtual void RegisterRButtonUpEvent(boost::function<void(void)> event);
virtual void RegisterHoverEvent(boost::function<void(void)> event);
virtual void RegisterLeaveEvent(boost::function<void(void)> event);
virtual void RegisterEnterKeyEvent(boost::function<void(void)> event);
virtual void RegisterLButtonDbCEvent(boost::function<void(void)> event);
virtual void RegisterEditChangeEvent(boost::function<void(void)> event);

If we are writing a client application  in C++, we may write code like this:

pEle->RegisterLButtonDownEvent(boost::bind(&MainWindow::_OnLButtonDown_ButtonClose,this));

It’s straight, we can registrate all handles in this way, and handle all events we are interested.

Let us check the nsis plugin interface:

__declspec(dllexport)
void _cdecl exported_func(
HWND hwndParent,\
int string_size,
TCHAR *variables,
stack_t **stacktop,    extra_parameters *extra);

typedef struct {
exec_flags_type *exec_flags;
int (NSISCALL *ExecuteCodeSegment)(int, HWND);
void (NSISCALL *validate_filename)(TCHAR *);
int (NSISCALL *RegisterPluginCallback)(HMODULE, NSISPLUGINCALLBACK); // returns 0 on success, 1 if already registered and < 0 on errors
} extra_parameters;

The most paramter “extra” exposes a usefull function “ExecuteCodeSegment”, it allows C++ code to call nsis script. Before we can call script code, we need a function inside script which provides the ablity to get a function address inside nsis script.

GetFunctionAddress
user_var(output) function_name

Gets the address of the function and stores it in the output user variable. This user variable then can be passed to Call or Goto. Note that if you Goto an address which is the output of GetFunctionAddress, your function will never be returned to (when the function you Goto’d to returns, you return instantly).

Function func
  DetailPrint "function"
FunctionEnd

Section
  GetFunctionAddress $0 func
  Call $0
SectionEnd

 

An important note about GetFunctionAddress is: it always +1 to the function offset. As the result we should do –1 before call a nsis function.

int Scripting::_ExecuteCode(int pos, HWND hwndProgress)
{
return ExecuteCodeSegment_(pos – 1,hwndProgress);
}

OK, we have known how to call nsis function, and also know how to registrate a ui event handler, we can connect them now.

// this is the api exported to nsis script

NSISAPI(RegisterEvent)
{
EXDLL_INIT();
int win_id = popint();
std::wstring ctrl_id = popstring();
std::wstring event_id = popstring();
int func_offset=popint();
global.script_.RegisterEvent((void*)win_id,ctrl_id,event_id,func_offset);
}

// This is the implement of the registratin.

void Scripting::RegisterEvent(void* win_id, std::wstring ctrl_id, std::wstring event_id, int func_offset)
{
CUIMainWindowEx* pthis = (CUIMainWindowEx*)win_id;

CBaseElementCtl *pEle = pthis->_elementManager.Search(ctrl_id);
if(pEle)
{
if (!_wcsicmp(event_id.c_str(),L”LDown”))
{
pEle->RegisterLButtonDownEvent(boost::bind(&Scripting::_ExecuteUICode,this,func_offset,(HWND)NULL));
}
else if (!_wcsicmp(event_id.c_str(),L”LUp”))
{
pEle->RegisterLButtonUpEvent(boost::bind(&Scripting::_ExecuteUICode,this,func_offset,(HWND)NULL));
}
else if (!_wcsicmp(event_id.c_str(),L”LDbClick”))
{
pEle->RegisterLButtonDbCEvent(boost::bind(&Scripting::_ExecuteUICode,this,func_offset,(HWND)NULL));
}
else if (!_wcsicmp(event_id.c_str(),L”RUp”))
{
pEle->RegisterRButtonUpEvent(boost::bind(&Scripting::_ExecuteUICode,this,func_offset,(HWND)NULL));
}
else if (!_wcsicmp(event_id.c_str(),L”EditChange”))
{
pEle->RegisterEditChangeEvent(boost::bind(&Scripting::_ExecuteUICode,this,func_offset,(HWND)NULL));
}
}
}

int Scripting::_ExecuteUICode(int pos, HWND hwndProgress)
{
if (pending_logic_call_==0)
{
base::Threads::Get(Threads::UI)->PostTask(boost::bind(&Scripting::_ExecuteCode,this,pos,hwndProgress));
}
return 0;
}

Inside the wrapper, it will call the nsis script finally.

After we can handle UI event, we should also allow scripts to query/change the UI elements’ attributes, like size, postion, title, etc.

Getting/Setting attributes is very easy, we can simply export functions.

!macro _DUI_SetEnable WIN_ID CTRL_ID VALUE
dui::SetEnable /NOUNLOAD ${WIN_ID} ${CTRL_ID} ${VALUE}
!macroend
!define DUI_SetEnable ‘!insertmacro _DUI_SetEnable’

; the return value will be pushed to stack
!macro _DUI_GetEnable WIN_ID CTRL_ID
dui::SetEnable /NOUNLOAD ${WIN_ID} ${CTRL_ID}
!macroend
!define DUI_GetEnable ‘!insertmacro _DUI_GetEnable’

WIN_ID represents the native C++ windows handle(instance), CTRL_ID represents the widget id which is defined in a xml.

Although the code pieces attached here is few, we do have finished introducing how to handle event from scripts, and how to control UI elements from scripts. Please allow me attach part of the installer code, so that we can have a global view of the whole installer.

; main panel
Function On_id_install_LUp
${DUI_SetVisible} $cur_win id_panel_main 0
${DUI_SetVisible} $cur_win id_panel_inst 1

${DUI_GetCheck} $cur_win id_shortcut
pop $desktop_shortcut

${DUI_GetCheck} $cur_win id_taskbar
pop $pin_taskbar

${DUI_GetCheck} $cur_win id_autorun
pop $auto_run

${DUI_LogicCall} Install
FunctionEnd

Function On_id_run_LUp
; for demo purpose, run uninstaller
Exec ‘”$INSTDIR\uninst.exe”‘

call On_id_close_LUp
FunctionEnd

; ui init logic
Function OnDuiInit
pop $cur_win
${DUI_GetWindowHwnd} $cur_win
pop $cur_hwnd

; init options
strcpy $desktop_shortcut 1
strcpy $pin_taskbar 1
strcpy $auto_run 1

;border
${DUI_OnEvent} $cur_win id_min LUp
${DUI_OnEvent} $cur_win id_close LUp
; main panel
${DUI_OnEvent} $cur_win id_install LUp
${DUI_OnEvent} $cur_win id_userinstall LUp
${DUI_OnEvent} $cur_win id_accept LUp
${DUI_OnEvent} $cur_win id_openurl LUp

${DUI_SetEnable} $cur_win id_install 0
${DUI_SetEnable} $cur_win id_userinstall 0

; user install panel
${DUI_OnEvent} $cur_win id_installnow LUp
${DUI_OnEvent} $cur_win id_backmain LUp
${DUI_OnEvent} $cur_win id_installpath EditChange
${DUI_OnEvent} $cur_win id_browse LUp

${DUI_SetCheck} $cur_win id_shortcut $desktop_shortcut
${DUI_SetCheck} $cur_win id_taskbar $pin_taskbar
${DUI_SetCheck} $cur_win id_autorun $auto_run

; finish panel
${DUI_OnEvent} $cur_win id_complete LUp
${DUI_OnEvent} $cur_win id_run LUp

${DUI_SetVisible} $cur_win id_panel_userdef 0
${DUI_SetVisible} $cur_win id_panel_inst 0
${DUI_SetVisible} $cur_win id_panel_complete 0
FunctionEnd

Because all scripts are actually executed inside nsis stub exe file, and it uses a few global variables to store script execution state, we have two choices for this:

1. carry out installation in the same thread as ui, the problem is, the UI loses response when installing. This is unacceptable;

2. carry out instalation in another thread, but nsis can not support to executes script parallely in two threads

I chose option 2, as a workaround, stop event dispatch when the intallation thread is busy.

!macro _DUI_LogicCall FUNCTION
Push $0
GetFunctionAddress $0 ${FUNCTION}
dui::LogicCall /NOUNLOAD $0
Pop $0
!macroend
!define DUI_LogicCall ‘!insertmacro _DUI_LogicCall’

void Scripting::LogicCall(int func_offset)
{
if (logic_thread_)
{
pending_logic_call_ ++;
logic_thread_->AddJob(boost::bind(&Scripting::_ExecuteLogicCode,this,func_offset,(HWND)NULL),INFINITE);
}
}

int Scripting::_ExecuteLogicCode(int pos, HWND hwndProgress)
{
int ret = _ExecuteCode(pos,hwndProgress);
pending_logic_call_–;
return ret;
}

After moving the installation logic in a seperated thread, the UI thread can continue handling windows GUI message (but not foreward to script), the animation can continue playing  normally.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s