Table of Contents
InstallScript language is generally based on C/C++ and InstallScript GUI programming uses Win32 API as its base. Despite that, InstallScript still lacks a lot of Win32 API’s advanced features. Many times, some of its functionality is removed on purpose by producer’s staff to make it more clear and usable for ”dummies” despite the fact that the language syntax can theoretically support it.
InstallScript controls wizard follows the MSI standards and misses a number of standard Windows controls, like an IP Address control, Animation control and many more.
Existing InstallShield controls miss also some control styles which are present in Win32 API, like LBS_NOSEL
– a ListView
control style (we will go over it later in future chapters).
All these missed controls can be created manually with CreateWindowsEx
and be managed by a standard windows messaging mechanism.
A Windows notification catching is another problem.
The WaitOnDialog
is a function which is intended to catch dialog events. In DLG_MSG_ALL
mode, it passes through to user functions only a limited number (about 10%) of censored control notifications based on WM_COMMAND
. The remaining 90% of windows messages probably are considered excessive for “dummies”. No events for static controls (Icons, Images and TextLabels) are allowed because they are automatically set to a disabled state in a resource file and their state cannot be set from the wizard.
Such approach can be found acceptable for Basic MSI projects but not in a pure InstallScript environment. The reason for this is that MSI technology initially intended to support only a limited number of windows features opposite InstallScript which appears to be a fully functional scripting language and actually has very little in common with MSI.
The events shown below are everything I was able to catch in DLG_MSG_ALL
mode:
PushButton, CheckBox, RadioButton | ComboBox | ListBox | |
---|
BN_SETFOCUS | CBN_DROPDOWN | LBN_SELCHANGE | LVN_COLUMNCLICK |
BN_KILLFOCUS | CBN_SELENDOK | LBN_DBLCLK | LVN_HOTTRACK |
BN_CLICKED | CBN_CLOSEUP | | |
BN_DOUBLECLICKED | CBN_SELENDCANCEL | ListView | TreeView |
---|
| | LVN_ITEMCHANGING | TVN_SELCHANGINGA |
| EditBox | LVN_ITEMCHANGED | TVN_SELCHANGEDA |
---|
| EN_UPDATE | LVN_DELETEALLITEMS | NM_DBLCLK |
| EN_CHANGE | LVN_INSERTITEM | |
Windows messages, which remain unhandled by built-in InstallScript functions, can be intercepted in two ways. The first method consists in catching BN_CLICKED
or EN_CHANGE
notification by using a regular built-in InstallScript mechanism of WaitOnDialog
and after that in going manually over a message loop by PeekMessage/GetMessage
functions without leaving the WaitOnDialog
switch/case
block. The second method is more sophisticated and more reliable as well - this is the windows subclassing method. We will show how to use it in the last chapter.
Let’s look over our first basic example. There we create a modal dialog which works like an InstallShield SdShowMsg
function with an addition of a progress bar and a status message.
First of all, you have to create a new dialog of a MessageBox
size with ProgressBar
and StaticText
controls to show status messages.
Our dialog definition in a resource file will look like a definition for an SdShowMsg
dialog.
- Create an empty dialog and resize it to a
MessageBox
dialog size - Set its Resource Identifier = 20009
- Set its style Modal = TRUE
- Set its Other Windows Styles as shown here:
- Create
StaticText
control - Set its Control Identifier = 1303
- Set its stiles to:
- No Prefix = TRUE
- No Text Wrap = TRUE
- Transparent = FALSE
- Create
ProgressBar
control - Set its Control Identifier = 1301
Define dialog and control IDs in an InstallScript header file:
#define CTRL_PROGRESS 1301
#define CTRL_STATIC 1303
#define DLG_ID 20009
Define progress bar message codes (WM_USER
is already defined by InstallShield) and Win32 API functions:
#define PBM_SETPOS (WM_USER+2)
#define PBM_SETSTEP (WM_USER+4)
#define PBM_STEPIT (WM_USER+5)
#define PBM_SETRANGE32 (WM_USER+6)
prototype User32.UpdateWindow(HWND);
This is a ShowProgressDialog
’s main function. The dialog is created by executing this function with bShow=TRUE
. When the function is called a second time with the same dialog name and bShow=FALSE
, the dialog destructs.
The progress bar has tickles ranged from 0 to 100 but this setting can be changed by your wish.
prototype number ShowProgressDialog(string, string, BOOL);
function number ShowProgressDialog(szDlg, szTitle, bShow)
number nResult;
HWND hwndDlg,hCtrl;
begin
if(bShow) then
if(EzDefineDialog( szDlg, "", "", DLG_ID ) = DLG_ERR) then
return -1;
endif;
nResult = WaitOnDialog(szDlg);
while(nResult !=DLG_INIT && nResult >= 0)
nResult = WaitOnDialog(szDlg);
endwhile;
hwndDlg = CmdGetHwndDlg(szDlg);
SdSetDlgTitle(szDlg, hwndDlg, szTitle);
hCtrl = GetDlgItem(hwndDlg, CTRL_PROGRESS);
nResult = SendMessage(hCtrl, PBM_SETRANGE32, 0, 100);
nResult = SendMessage(hCtrl, PBM_SETSTEP, 10, 0);
ShowWindow(hwndDlg, SW_SHOW);
User32.UpdateWindow(hwndDlg);
else
EndDialog(szDlg);
ReleaseDialog(szDlg);
endif;
return 0;
end;
Each time we want to increment the progress bar, we run the function IncrementProgressDialog
.
The progress bar can be updated by sending one of the two messages, PBM_STEPIT
or PBM_SETPOS
.
If we want to set a tickle number directly, there should be sent a PBM_SETPOS
message with a wParam
equal to a tickle number:
SendMessage(hDlg, PBM_SETPOS, nIndex, 0);
prototype number IncrementProgressDialog(string, string);
function number IncrementProgressDialog (szDlg , szMsg)
HWND hDlgCtrl;
begin
hDlgCtrl = GetDlgItem(CmdGetHwndDlg(szDlg), CTRL_PROGRESS);
CtrlSetText(szDlg, CTRL_STATIC, szMsg);
return SendMessage(hDlgCtrl, PBM_STEPIT, 0, 0);
end;
This script shows how the Progress dialog actually works:
szDlg = "ShowDialog";
szTitle = "Status dialog with progress bar";
if(ShowProgressDialog(szDlg,szTitle,TRUE)) then
return -1;
endif;
…
…
IncrementProgressDialog(szDlg,"< progress bar status message >");
…
…
IncrementProgressDialog(szDlg, "<progress />");
…
ShowProgressDialog(szDlg,"",FALSE);
If we try to create a standard ListBox
control by an InstallShield wizard, we won’t see the LBS_NOSEL
style in the “Other Windows Styles” field. An InstallShield team probably thought of style to be excessive and didn’t include it in a styles list. This style could be helpful if we want to create a read-only list box which only shows the information like a multiline read-only EditBox
but with white background color instead of a grey one.
Let’s first define windows API functions and message codes:
prototype HWND User32.CreateWindowExA(int, byref string, byref string,
int, int, int, int, int, HWND, HWND, HWND, pointer);
prototype BOOL User32.DestroyWindow(HWND);
#define LBS_NOSEL 0x4000L
#define LBS_NOTIFY 0x0001L
#define LBS_SORT 0x0002L
#define WS_VSCROLL 0x00200000L
#define LBS_STANDARD (LBS_NOTIFY | LBS_SORT | WS_VSCROLL | WS_BORDER)
This function creates a custom ListBox
control. It must be called from the DLG_INIT
section.
prototype HWND CreateListBoxControl(HWND, int, int, int, int);
function HWND CreateListBoxControl (hwndDlg, nLeft, nTop, nWidth, nHeight)
HWND hwndLB;
begin
hwndLB = User32.CreateWindowExA (0,
LIST_CLASS_NAME, "", WS_TABSTOP| WS_CHILD|LBS_NOSEL|LBS_STANDARD,
nLeft, nTop, nWidth, nHeight, hwndDlg, NULL, NULL, NULL);
ShowWindow (hwndLB, SW_SHOW);
return hwndLB;
end;
This is a complete DLG_INIT
section:
case DLG_INIT:
…
hwndLB = CreateListBoxControl (hwndDlg, 170,50,200,80);
szText = "First line";
SendMessage(hwndLB, LB_ADDSTRING, 0, &szText);
szText = "Second line";
SendMessage(hwndLB, LB_ADDSTRING, 0, &szText);
szText = "Third line";
SendMessage(hwndLB, LB_ADDSTRING, 0, &szText);
Before destroying the dialog, we must manually destroy the ListBox
window first:
User32.DestroyWindow(hwndLB);
This example shows how to use the standard windows IPAddress
control.
The IPAddress
control is not included in InstallShield toolbar view, so it should be created manually with the CreateWindowExA
function.
First let’s define missing definitions of message codes and functions:
prototype HWND User32.CreateWindowExA(int, byref string, byref string,
int, int, int, int, int, HWND, HWND, HWND, pointer);
prototype BOOL User32.DestroyWindow(HWND);
#define WS_EX_LEFT 0x00000000L
#define WS_EX_LTRREADING 0x00000000L
#define WS_EX_CLIENTEDGE 0x00000200L
#define WC_IPADDRESSA "SysIPAddress32"
#define IPM_CLEARADDRESS (WM_USER+100)
#define IPM_SETADDRESS (WM_USER+101)
#define IPM_GETADDRESS (WM_USER+102)
This function is used in the DLG_INIT
section. It creates a dialog child window from a SysIPAddress32
windows class and initializes it with a given IP address string.
prototype HWND CreateIPAddressControl(HWND, int, int, int, int, string);
function HWND CreateIPAddressControl (hwndDlg, nLeft, nTop, nWidth, nHeight, szIP)
HWND hIPControl;
LIST list;
string sz1,sz2,sz3,sz4;
number n1,n2,n3,n4,nIP;
begin
hIPControl = User32.CreateWindowExA
(WS_EX_LEFT|WS_EX_LTRREADING|WS_EX_CLIENTEDGE,
WC_IPADDRESSA, "", WS_CHILD|WS_VISIBLE|WS_TABSTOP,
nLeft, nTop, nWidth, nHeight, hwndDlg, NULL, NULL, NULL);
if(hIPControl=NULL) then
return 0;
endif;
list = ListCreate(STRINGLIST);
StrGetTokens(list,szIP,".");
ListGetFirstString(list,sz1);
ListGetNextString(list,sz2);
ListGetNextString(list,sz3);
ListGetNextString(list,sz4);
ListDestroy(list);
StrToNum(n1,sz1);
StrToNum(n2,sz2);
StrToNum(n3,sz3);
StrToNum(n4,sz4);
nIP = (n1<<24) + (n2<<16) + (n3<<8) + n4;
SendMessage(hIPControl,IPM_CLEARADDRESS,0,0);
SendMessage(hIPControl,IPM_SETADDRESS,0,nIP);
SetFocus(hIPControl);
return hIPControl;
end;
This function receives an IPAddress
control handler and returns the resulting IP address string.
prototype string GetIPAddress(HWND);
function string GetIPAddress(hIPControl)
string sz1,sz2,sz3,sz4, szIP;
number n1,n2,n3,n4,nIP;
begin
SendMessage(hIPControl,IPM_GETADDRESS,0,&nIP);
n1 = (nIP>>24) & 0xff;
n2 = (nIP>>16) & 0xff;
n3 = (nIP>>8) & 0xff;
n4 = nIP & 0xff;
NumToStr(sz1,n1);
NumToStr(sz2,n2);
NumToStr(sz3,n3);
NumToStr(sz4,n4);
szIP = sz1+"."+sz2+"."+sz3+"."+sz4;
return szIP;
end;
Here, we combine these functions together in an InstallScript environment:
case DLG_INIT:
hIPControl = CreateIPAddressControl(hwndDlg, 34,123,160,21,"127.0.0.1");
case NEXT:
szIP = GetIPAddress(hIPControl);
Before destroying the dialog manually, we need to destroy the IP address window:
User32.DestroyWindow(hIPControl );
InstallShield doesn’t have a built in grid control so, in this case, we can use a ListView
control in a LVS_REPORT
mode for showing a grid table.
Create a ListView
control and set its window styles in a control wizard.
All styles except LVS_REPORT
are not mandatory and are selected here for convenience.
ListBox style | Style description |
LVS_SHOWSELALWAYS | A selection, if any, is always shown, even if a control does not have a focus. |
LVS_SORTASCENDING | Item indexes are sorted based on an item text in an ascending order. |
LVS_EDITLABELS | An Item text can be edited in place. |
LVS_NOSORTHEADER | Column headers do not work like buttons. When this style is used in a report view, clicking a column header does not carry out an action such as sorting. |
LVS_SINGLESEL | Only one item at a time can be selected. By default, multiple items may be selected. |
Define message codes which are not defined yet:
#define LVM_SETIMAGELIST (LVM_FIRST + 3)
#define LVM_DELETEALLITEMS (LVM_FIRST + 9)
#define LVM_GETNEXTITEM (LVM_FIRST + 12)
#define LVM_GETITEMTEXT (LVM_FIRST + 45)
#define LVM_SETEXTENDEDLISTVIEWSTYLE (LVM_FIRST + 54)
#define LVS_EX_FULLROWSELECT 0x00000020
#define LVS_EX_SUBITEMIMAGES 0x00000002
#define LVS_EX_GRIDLINES 0x00000001
#define LVS_EX_HEADERDRAGDROP 0x00000010
#define LR_LOADFROMFILE 0x00000010
#define LVIF_IMAGE 0x0002
#define LVIF_STATE 0x0008
#define ILC_COLOR4 0x0004
#define IMAGE_ICON 1
#define LVSIL_SMALL 1
prototype HWND User32.LoadImage(pointer,pointer,int,int,int,int);
prototype HWND comctl32.ImageList_Create(int,int,int,int,int);
prototype int comctl32.ImageList_ReplaceIcon(HWND,int,HWND);
Define a PSTR
structure to reinitialize pointers, like "ptr=&val; val=*ptr
" in C/C++:
typedef PSTR
begin
string str[MAX_PATH];
end;
Here we add a new column to our grid:
prototype number ListViewAddColumn(HWND, number, number, string);
function number ListViewAddColumn(hListWnd, nColID, nColSize, szColName)
LV_COLUMN lvc;
begin
lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
lvc.fmt = LVCFMT_LEFT;
lvc.iSubItem = nColID;
lvc.cx = nColSize;
lvc.pszText = &szColName;
return SendMessage(hListWnd, LVM_INSERTCOLUMN, nColID, &lvc);
end;
Here, we add a new line to our grid. Then, at first main column, we set a text, draw an icon and, finally receive an item’s line number.
Text strings in the main items column should be unique. All newly added lines are automatically sorted so, if we want to know a line’s real number, we need to use the return value from this function.
prototype number ListViewAddItem(HWND, number, number,string);
function number ListViewAddItem(hListWnd, nItemID, nImageID, szItemName)
LVITEM lvit;
begin
lvit.mask = LVIF_TEXT|LVIF_IMAGE;
lvit.iItem = nItemID;
lvit.iImage = nImageID;
lvit.iSubItem = 0;
lvit.pszText = &szItemName;
return SendMessage(hListWnd, LVM_INSERTITEM, 0, &lvit);
end;
Here, we set a text for secondary columns.
To place a text, we need to specify a main item’s line number and a column number of our subitem.
prototype number ListViewAddSubItem(HWND, number, number, string);
function number ListViewAddSubItem(hListWnd, nItemID, nSubItemID, szSubItemName)
LVITEM lvit;
begin
lvit.mask = LVIF_TEXT;
lvit.iItem = nItemID;
lvit.iSubItem = nSubItemID;
lvit.pszText = &szSubItemName;
return SendMessage(hListWnd, LVM_SETITEM, 0, &lvit);
end;
Here, we retrieve a main item’s text for the selected line.
prototype number ListViewGetSelectedItemText(HWND, byref string);
function number ListViewGetSelectedItemText(hListWnd, szItemName)
string szListText[MAX_PATH];
LVITEM lvit;
number ret;
PSTR pointer pstr;
begin
ret = SendMessage(hListWnd, LVM_GETNEXTITEM, -1, LVIS_SELECTED);
if(ret > -1) then
lvit.mask = LVIF_TEXT;
lvit.iItem = ret;
lvit.iSubItem = 0;
lvit.pszText = &szListText;
lvit.cchTextMax = MAX_PATH;
SendMessage(hListWnd, LVM_GETITEMTEXT, ret, &lvit);
pstr = lvit.pszText;
szItemName = pstr->str;
return ret;
else
szItemName = "";
return -1;
endif;
end;
Create columns and add lines for the ListView
control.
Here, we use one icon for the first 10 lines and another icon for the remaining lines.
prototype number ListViewInitialize(HWND);
function number ListViewInitialize(hwndList)
number ret, nIdx;
string szIdx;
begin
SendMessage (hwndList, LVM_SETEXTENDEDLISTVIEWSTYLE, 0,
LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES|LVS_EX_HEADERDRAGDROP);
SendMessage (hwndList, LVM_DELETEALLITEMS, 0, 0);
ret = ListViewAddColumn (hwndList,0,80,"Main Column");
ret = ListViewAddColumn (hwndList,1,80,"Sec Column #1");
ret = ListViewAddColumn (hwndList,2,80,"Sec Column #2");
for nIdx = 0 to 19
NumToStr(szIdx,nIdx);
if (nIdx < 10) then
ret = ListViewAddItem(hwndList,0,0,"Key #"+szIdx);
else
ret = ListViewAddItem(hwndList,0,1,"Key #"+szIdx);
endif;
ListViewAddSubItem(hwndList,ret,1,"Value1 #"+szIdx);
ListViewAddSubItem(hwndList,ret,2,"Value2 #"+szIdx);
endfor;
return 0;
end;
Here, we create and initialize an image list and load there 2 icons.
prototype number ListViewCreateImageList(HWND,string,string);
function number ListViewCreateImageList (hwndList, szImageFile1, szImageFile2)
HWND img1,img2,himl;
begin
img1 = LoadImage(NULL,&szImageFile1,IMAGE_ICON,16,16,LR_LOADFROMFILE);
img2 = LoadImage(NULL,& szImageFile2,IMAGE_ICON,16,16,LR_LOADFROMFILE);
himl = ImageList_Create(16,16,ILC_COLOR4,2,0);
ImageList_ReplaceIcon(himl, -1, img1);
ImageList_ReplaceIcon(himl, -1, img2);
return SendMessage(hwndList, LVM_SETIMAGELIST, LVSIL_SMALL, himl);
end;
This example shows how to use the code below:
case INIT_DLG:
…
hwndList = GetDlgItem(hwndDlg,nListID);
ListViewInitialize(hwndList);
ListViewCreateImageList(hwndList,SUPPORTDIR^"icon1.ico",SUPPORTDIR^"icon2.ico");
case NEXT:
ret = ListViewGetSelectedItemText(hwndList,szListText);
if(ret < 0) then
else
endif;
The following example shows how to use _ pop-up balloon tips in Edit controls.
Balloon tips may contain a title, message and icon (information, warning, etc…).
In order to use this API functionality, you must have Comclt32.dll of version 6.0 from the latest Microsoft SDK.
This is a definition part.
#define EM_SHOWBALLOONTIP 0x1503
#define EM_HIDEBALLOONTIP 0x1504
#define TTI_NONE 0
#define TTI_INFO 1
#define CP_ACP 0 // default to ANSI code page
#define MB_PRECOMPOSED 0x00000001 // use precomposed chars
typedef EDITBALLOONTIP
begin
number cbStruct;
number pszTitle;
number pszText;
number ttiIcon;
end;
Here, we receive a title and message strings
, translate them to Unicode and send as a message to an Edit control. The EM_SHOWBALLOONTIP
message makes balloon tip to be shown.
prototype number ShowBalloonTip(HWND,string,string);
function number ShowBalloonTip(hWndCtrl, szTitle, szMsg)
EDITBALLOONTIP baloon;
string szBalTitleBuf[MAX_PATH], szBalTextBuf[MAX_PATH];
pointer pBalTitleBuf, pBalTextBuf;
begin
pBalTextBuf = &szBalTextBuf;
pBalTitleBuf = &szBalTitleBuf;
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,&szMsg,
StrLengthChars(szMsg)+1, pBalTextBuf, MAX_PATH);
MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED,&szTitle,
StrLengthChars(szTitle)+1, pBalTitleBuf, MAX_PATH);
baloon.cbStruct = SizeOf(baloon);
baloon.pszText = pBalTextBuf;
baloon.pszTitle = pBalTitleBuf;
baloon.ttiIcon = TTI_INFO;
return SendMessage(hWndCtrl,EM_SHOWBALLOONTIP,0,&baloon);
end;
This function closes the previously shown balloon tip. It may also be closed by simply clicking on it.
prototype number HideBalloonTip(HWND);
function number HideBalloonTip(hWndCtrl)
begin
return SendMessage(hWndCtrl, EM_HIDEBALLOONTIP, 0, 0);
end;
In the previous chapters, we obtained all missed InstallScript functionality by simply sending windows messages. The receiving of windows messages is a bit more complex task. We are going to solve it by using a windows subclassing mechanism. InstallScript unfortunately doesn’t support function pointers, so we will write the entire message handling code in an external C/C++ DLL and afterwards replace an InstallScript native windows message handling procedure with our C/C++ function.
In our C/C++ code in the first switch
/case
block, we catch WM_CTLCOLORSTATIC
event in order to color read-only edit box to the white color.
In second and third blocks, we try to catch the WM_HELP
and WM_SETFOCUS
messages for the Edit box and send them back to the InstallScript. Thus we notify the InstallScript control that some action is being happening right now. It will make him to send its own notification back to the WaitOnDialog
function.
This trick allows us to run the ShowBalloonTip
function from the InstallScript.
Set the DS_CONTEXTHELP
style in a dialog styles wizard in order to use the WM_HELP
feature.
The following C/C++ code should be compiled to a simple C/C++ DLL without ATL and MFC libraries.
#include "stdafx.h"
#include <stdio.h>
#include <process.h>
static WNDPROC oldstaticproc;
static int nCtrlID;
long __stdcall StaticProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_CTLCOLORSTATIC: if(GetDlgCtrlID((HWND)lParam) == nCtrlID)
{
SetBkMode((HDC)wParam, OPAQUE);
return (INT_PTR)(HBRUSH)GetStockObject(DC_BRUSH);
}
case WM_COMMAND:
if(LOWORD(wParam) == nCtrlID)
{
if(HIWORD(wParam) == EN_SETFOCUS)
{
SendMessage(hwnd,WM_COMMAND,MAKEWPARAM
(nCtrlID, EN_CHANGE),
(LPARAM)GetDlgItem(hwnd, nCtrlID));
}
else if(HIWORD(wParam) == EN_KILLFOCUS)
{
SendMessage(GetDlgItem(hwnd, nCtrlID),
EM_HIDEBALLOONTIP,0,0);
}
}
case WM_HELP:
LPHELPINFO lphlp = (LPHELPINFO)lParam;
if(wParam == 0 && lphlp != 0)
{
if(GetDlgCtrlID((HWND)lphlp->hItemHandle) == nCtrlID)
{
SendMessage(hwnd,WM_COMMAND,
MAKEWPARAM(nCtrlID,EN_CHANGE),
(LPARAM)GetDlgItem(hwnd, nCtrlID));
}
}
}
return CallWindowProc(oldstaticproc, hwnd, msg, wParam, lParam);
}
extern "C" __declspec(dllexport) void SubClassWindowProc(HWND hWnd,int argCtrlID)
{
oldstaticproc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC, (LONG)StaticProc);
nCtrlID = argCtrlID;
}
This is a prototype definition for an exported DLL method. It should be placed before the WaitOnDialog
code block. Pay attention to the “cdecl
” parameter.
prototype cdecl subclass.SubClassWindowProc(HWND,int);
Here, we replace the original InstallScript windows messaging procedure with our custom C/C++ function at the initialization time and execute ShowBalloonTip
when the Edit control receives focus or when a user clicks on the Edit control with a question mark.
UseDLL(SUPPORTDIR^"subclass.dll");
…
nID = WaitOnDialog(szDlg);
switch(nID)
case INIT_DLG:
…
hwndDlg = CmdGetHwndDlg( szDlg );
hwndEdit = GetDlgItem(hwndDlg,1303);
subclass.SubClassWindowProc(hwndDlg,1303);
case 1303:
ShowBalloonTip(hwndEdit,"EditBox Title","Tool tip help message");
…
endswitch;
…
UnUseDLL(SUPPORTDIR^"subclass.dll");
The following code samples contain all the features discussed in the above chapters:
- 28th February, 2011: Initial post
- 30th April, 2011: Article updated