Mobile development: show a small information window using WIN32 API

ShowWin

Sometimes you may need to display a small window to inform the user about what is going on. Although the scripting tools MortScript and nScript provide functions to show dialogs they can not show simple information windows.

ShowWin default colors

ShowWin default colors

ShowWin with a progress bar

ShowWin with a progress bar

ShowWin is nothing special but a nearly full configurable window to be used from cmd line tools. It just uses Win32 API calls, constants and structures as FindWindow, PostMessage, SendMessage, WM_COPYDATA, COPYDATASTRUCT, GetSystemMetrics, GetDesktopWindow, GetWindowRect, CreateWindowEx, ShowWindow, UpdateWindow, INITCOMMONCONTROLSEX, GetDeviceCaps, CreateFontIndirect, GetWindowDC, ReleaseDC, PROGRESS_CLASS, InvalidateRect, BeginPaint, CreatePen, SelectObject, Rectangle, SetBkMode, DrawText, EndPaint, SetTextColor, DeleteObject, GetKeyState and PostQuitMessage.

Basic WIN32 programming

Possibly you never wrote a native C windows application. Come on and dive into the basics. It is always good to know the basics even if one writes DotNet or JAVA code.

Supported arguments

 showWin -t "Text zum Anzeigen" -r 90 -g 80 -b 70 -s 8 -w 200 -h 50 -x 0 -y 0 -rt 200 -gt 20 -bt 20 -ti 10 -progr 30 -align left

 ARGS: 
 option/parameter:                meaning:                default:            limitations:
 -t "Text zum Anzeigen"           text to show            "Installing"        255 chars, no " inside, no line breaks, no tabs
 -r 90                            background color RED    255                    0-255
 -g 80                            background color GREEN    207                    0-255
 -b 70                            background color BLUE    0                    0-255
 -s 8                             font size in points        10                    7-24 points
 -w 200                           window width pixels        460                    100-screenwidth
 -h 50                            window height pixels    40                    menu bar height (ie 26pixels)
 -x 60                            window pos X            12                    0 + system window bordersize
 -y 60                            window pos Y            48                    0 + system taskbar bar height. Using 0;0 does not work nice on WM, win may be below taskbar
 -rt 200                          text color RED            0                    0-255
 -gt 20                           text color GREEN        0                    0-255
 -bt 20                           text color BLUE            0                    0-255

 -align center                    text alignment            left                center|left|right

 -ti 10                           timeout to autoclose    0                    no autoclose, min: 1 (second), max: 3600 = one hour

 -progr 10                        show with progress val    0                    no progressbar, max: 100
                                  the progressbar is appended at bottom of textwindow
 -prval                           update progress bar value                    no default, min=1, max=100

 -kill                            kill existing window, exit app

 -m "new message text"            replace text in window                        see -t

Argument parsing

Fortunately I found some code to split arguments supplied to int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow). But i had to adopt the class to be unicode compatible. Windows CE and Windows Mobile uses Unicode for strings.

You know, that the WinMain is the first function called by the OS when you start an application.

int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPTSTR    lpCmdLine,
                   int       nCmdShow)
{
    LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); 
    LoadString(hInstance, IDC_SHOWWIN, szWindowClass, MAX_LOADSTRING);

    HWND hwndOld=FindWindow(szWindowClass,NULL);
    // kill request?
    if(hwndOld!=NULL){
        if(wcsicmp(lpCmdLine, L"-kill")==0){
            PostMessage(hwndOld, WM_QUIT, 0, 0);
            return 11;
        }
    }

    DEBUGMSG(1, (L"CmdLine parsing #1: \r\n"));
    //command parsing
    struct cmdList *Liste;
    Liste=NULL;
    CmdLineArgs args;
    for (UINT i = 0; i < args.size(); i++){
        DEBUGMSG(1, (L"%20i: '%s'\r\n", i, args[i]));
        append(&Liste, args[i]);
    }
    getOptions(Liste);
    args.~CmdLineArgs();

First the code uses LoadString to load the title and window class from its resources. Then FindWindow is used to look for a previous instance and checks if the only argument is -kill. If so, the previous instance is sent a quit using PostMessage and then the application exits itself.

Now we define a structure (Liste) to hold the arguments. Then we start command line parsing by creating a chained list of arguments. The list is created by using the class CmdLineArgs. We then walk thru the argument list and append each argument to our Liste structure. Using getOptions(Liste) we scan the list for known optional arguments and apply optional values to global variables.

class CmdLineArgs : public std::vector<TCHAR*>
{
public:
    CmdLineArgs ()
    {
        // Save local copy of the command line string, because
        // ParseCmdLine() modifies this string while parsing it.
        TCHAR* cmdline = GetCommandLine();
        m_cmdline = new TCHAR [_tcslen (cmdline) + 1];
        if (m_cmdline)
        {
            _tcscpy (m_cmdline, cmdline);
            ParseCmdLine(); 
        }
    }
    ~CmdLineArgs()
    {
        delete []m_cmdline;
    }
...

getOptions is a chain of if/else if blocks that tests for known options and applies values to global variables:

void getOptions(struct cmdList *l){
    struct cmdList *liste;
    liste=l;
    if(l==NULL)
        return;
    int iVal;
    do{
        DEBUGMSG(1, (L"%s\r\n", liste->value));
        if(wcsicmp(liste->value, L"-t")==0){        // message text
            if(liste->next != NULL){
                liste=liste->next;
                wsprintf(szMessageText, L"%s", liste->value);
            }
        }
        else if(wcsicmp(liste->value, L"-m")==0){        // message text
            if(liste->next != NULL){
                liste=liste->next;
                wsprintf(szMessageTextNew, L"%s", liste->value);
            }
        }
        else if(wcsicmp(liste->value, L"-r")==0){    // rgb r value
            if(liste->next != NULL){
                liste=liste->next;
                iVal=_wtoi(liste->value);
                if(iVal!=0)
                    backcolorRed=iVal;
            }
        }
...
        liste=liste->next;
    }while(liste != NULL);
}

Inter process communication

Now that we have read all arguments, we can test if we need to update the text or progress value of an existing instance:

    // already running?
    if(hwndOld!=NULL){
        //check if new message text?
        if(wcslen(szMessageTextNew) > 0){
            myMsg _mymsg;
            memset(&_mymsg,0,sizeof(myMsg));
            wsprintf( _mymsg.szText, L"%s", szMessageTextNew );
            _mymsg.iVal=0;    //the text message identifier
            //prepare WM_COPYDATA
            COPYDATASTRUCT copyData;
            copyData.dwData=1234;
            copyData.cbData=sizeof(myMsg);
            copyData.lpData=&_mymsg;
            SendMessage(hwndOld, WM_COPYDATA, (WPARAM)NULL, (LPARAM)&copyData);
        }
        if(iProgressValNew!=-1){
            myMsg _mymsg;
            memset(&_mymsg,0,sizeof(myMsg));
            wsprintf( _mymsg.szText, L"%i", iProgressValNew );
            _mymsg.iVal=1;    //the progress message identifier
            //prepare WM_COPYDATA
            COPYDATASTRUCT copyData;
            copyData.dwData=1234;
            copyData.cbData=sizeof(myMsg);
            copyData.lpData=&_mymsg;
            SendMessage(hwndOld, WM_COPYDATA, (WPARAM)NULL, (LPARAM)&copyData);
        }
        ShowWindow(hwndOld, SW_SHOWNORMAL);
        return -1;
    }

To let the ‘old’ window update its text or progress bar we need to use inter-process communication. The simplest one supporting custom data (text, progress value) I found was using the WM_COPYDATA message. To use that, we have to define a structure that holds our data (here myMsg is used) and then assign the filled data structure to lpData of a COPYDATASTRUCT variable. Then we send the data to the windows handle of the existing instance using SendMessage. The asynchronous PostMessage does not work, the data must be available on the sender side when the message is received by the target window. SendMessage will block until the message has been delivered and so the data can be transfered between sender and target. Finally the previous instance will be shown and the actual launched will quit (return -1;).

On the receiver side (same application but second instance) we have to decode the WM_COPYDATA message.

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
...
    switch (message) 
    {
...
        case WM_COPYDATA:
            copyData=(COPYDATASTRUCT*)lParam;
            myMsg _mymsg;
            if(copyData->dwData==1234)    //that's right
            {
                memcpy(&_mymsg, copyData->lpData, sizeof(myMsg));
            }
            if(_mymsg.iVal==0){        //text message
                if(wcslen(_mymsg.szText)>0)
                    wcscpy(szMessageText, _mymsg.szText);
                GetClientRect(hWnd, &rect);
                InvalidateRect(hWnd, &rect, TRUE);
            }
            else if(_mymsg.iVal==1){        //progress message
                if(wcslen(_mymsg.szText)>0)
                    wcscpy(szTemp, _mymsg.szText);
                iProgressVal=_wtoi(szTemp);
                SendMessage(hProgress, PBM_SETPOS, iProgressVal, 0);
            }
            break;

The above code shows how we get the data back from the message lParam parameter. The structure myMsg knows actually two types of data: a progress value or a new text. Depending on the message type we either update the global variable szMessageText or iProgressVal. After changing the text we inform the OS that our window needs to be updated (painted again). If a new progress value has been received we just need to send the progress bar the new value using SendMessage(hProgress, PBM_SETPOS, iProgressVal, 0);.

Adopt to available screen size

Back to our application winMain startup code. The next code lines query the device for screen size and xxx

    //client size
    int maxX = GetSystemMetrics(SM_CXSCREEN);
    int maxY = GetSystemMetrics(SM_CYSCREEN);        //640 ??
    int borderSize = GetSystemMetrics(SM_CXBORDER);
    int minSize = GetSystemMetrics(SM_CYMENU);
    RECT rectMax;
    GetWindowRect(GetDesktopWindow(), &rectMax);
    if(xWidth<100 || xWidth>maxX)    // secure width setting
        xWidth=maxX-2*borderSize;
    if(yHeight<minSize)
        yHeight=minSize+2*borderSize;

    if(xPos<borderSize)    //secure x pos
        xPos=borderSize;
    if(yPos<rectMax.top)    //secure y pos
        yPos=rectMax.top;

    //progressBar is attached to bottom of window
    if(bUseProgress){
        //extend window
        xProgressWidth=xWidth;
        yHeight+=yProgressHeight;
        yProgress=yHeight-yProgressHeight;

    }

SM_CXSCREEN and SM_CYSCREEN let us know the width and height of the screen in pixels. As we want to limit the window creation to usual values, I also query the system value of border width (SM_CXBORDER) and the menu height (SM_CYMENU).
Using GetWindowRect we query for the maximum client area of the ‘desktop’ window. Then we adjust the given width and height value to usable values. We do the same for the x and y position of the window.

If a progressbar is to be used (determined by parsing the command line arguments), we need to extend the specified window at the bottom to reserve place for the bar.
The remainder of winMain is standard and initializes the window and starts the message loop.

    // Perform application initialization:
    if (!InitInstance(hInstance, nCmdShow)) 
    {
        return FALSE;
    }

    HACCEL hAccelTable;
    hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_SHOWWIN));

    // Main message loop:
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0)) 
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return (int) msg.wParam;

Inside InitInstance() there is a call to myRegisterClass(). As I like to have a ‘backdoor’ to quit the ShowWin app, I added CS_DBLCLKS. Without that style attribute the window will otherwise not get double click messages!:

ATOM MyRegisterClass(HINSTANCE hInstance, LPTSTR szWindowClass)
{
    WNDCLASS wc;

    hBackcolor = CreateSolidBrush(RGB(backcolorRed,backcolorGreen,backcolorBlue));

    wc.style         = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInstance;
    wc.hIcon         = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_SHOWWIN));
    wc.hCursor       = 0;
    wc.hbrBackground = (HBRUSH) hBackcolor;// GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName  = 0;
    wc.lpszClassName = szWindowClass;

    return RegisterClass(&wc);
}

In InitInstance we apply some special wishes for the window z-order: WS_EX_ABOVESTARTUP and WS_EX_TOPMOST. The code also does not use window defaults for size and position as these would result in a maximized window but we want to show only a small window.

    hWnd = CreateWindowEx( 
        WS_EX_TOPMOST | WS_EX_ABOVESTARTUP,    //exStyle
        szWindowClass,    //wndClass
        NULL, //L"Installer",    //title
        WS_VISIBLE, //dwStyle
        xPos,    // CW_USEDEFAULT,  //x
        yPos,    //CW_USEDEFAULT,  //y    
        xWidth,  //CW_USEDEFAULT,  //width
        yHeight, //CW_USEDEFAULT,  //height
        NULL,    //hParent
        NULL,    //hMenu
        hInstance,
        NULL
        );

After all this stuff the window class is registered and the window will be created using our settings. Now the magic starts and the windows message proc is called by the message loop. The first message we will see is WM_CREATE.

        case WM_CREATE:
            //do font calculation
            hdc=GetWindowDC(hWnd);
            iDevCap=GetDeviceCaps(hdc, LOGPIXELSY);    //pixels per inch
            lfHeight = -((long)fontHeight * (long)iDevCap) / 72L;
            GetObject (GetStockObject (SYSTEM_FONT), sizeof (LOGFONT), (PTSTR) &logfont) ;
            //    HFONT hf = CreateFontIndirect(&logfont);
            logfont.lfHeight=lfHeight;
            hFont=CreateFontIndirect(&logfont);
            ReleaseDC(NULL,hdc);

            DEBUGMSG(1, (L"Create hWnd=%i\r\n", hWnd));
            if(iTimeOut>0)
                startThread(hWnd);

            if(bUseProgress){
                //progressBar
                hProgress = CreateWindowEx(0, PROGRESS_CLASS, NULL,
                                WS_CHILD | WS_VISIBLE,
                                xProgress, yProgress, xProgressWidth, yProgressHeight,
                                hWnd, NULL, g_hInst, NULL);
                SendMessage(hProgress, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
                SendMessage(hProgress, PBM_SETPOS, iProgressVal, 0);
            }
            break;

The above tries to calculate the right size for a LOGFONT structure for the text drwan in the window. We need to know the number of pixels per inch of the screen – the dots-per-inch resolution. The font size argument you can use on the command line is to be given in points. A point is a 1/72 of an inch (or given at 72 dpi). We calculate the logical font height by relating the screen dpi and the font size dpi. Then the code gets the LOGFONT structure of the system font and applies the new logical font size. Finally the global variable hFont is initialized with the logfont structure.

If an optional timeout value was supplied via the cmd line, the line startThread() will be executed. That starts a background thread that will post a quit message to the message loop when the timeout value is reached:

DWORD myThread(LPVOID lpParam){
    BOOL bExit=FALSE;
    HWND hwndMain=(HWND)lpParam;
    DWORD dwWaitResult=0;
    int iCountSeconds=0;
    DEBUGMSG(1, (L"myThread hWndMain=%i\r\n", hwndMain));
    do{
        dwWaitResult = WaitForSingleObject(hStopThread, 1000);
        switch(dwWaitResult){
            case WAIT_OBJECT_0:
                bExit=TRUE;
                break;
            case WAIT_TIMEOUT:
                iCountSeconds++;
                if(iCountSeconds>=iTimeOut)
                {
                    PostMessage(hwndMain, WM_QUIT, 99, iTimeOut);
                    bExit=TRUE;
                }
                break;
        }
    }while(!bExit);
    return 0;
}

I am using WaitForSingleObject() here to be able to stop the thread by setting a named event.

Back in WM_CREATE the last lines are to create a progressBar, if the optional argument for a progress bar was used.

The next message of importance is WM_PAINT. All drawing of the window is done within the WM_PAINT handler.

        case WM_PAINT:
            hdc = BeginPaint(hWnd, &ps);

            // TODO: Add any drawing code here...
            GetClientRect(hWnd, &rect);
            //shrink text area if progressbar is there
            if(bUseProgress && hProgress!=NULL){
                rect.bottom-=yProgressHeight;
            }

the above resized the drawing rectangle for text if a progress bar is used.

Next we draw some black rectangles to give the window a simple drop shadow effect.

            //draw rectangle
            myPen = CreatePen(PS_SOLID, 1, RGB(0,0,0));
            oldPen = (HPEN)SelectObject(hdc,myPen);
            SelectObject(hdc, hBackcolor);
            Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
            //shrinkRect(&rect, 1);
            //Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
            //a drop shadow
            rect.right-=1;rect.bottom-=1;
            Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
            rect.right-=1;rect.bottom-=1;
            Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);
            SelectObject(hdc, oldPen);

Then we set the background mode to transparent and draw the text and finally restore font and text color.

            SetBkMode(hdc, TRANSPARENT);
            oldTextColor = SetTextColor(hdc, RGB(fontColorRed, fontColorGreen, fontColorBlue));
            hfOld=(HFONT)SelectObject(hdc, hFont);
            DrawText(hdc, 
                szMessageText,    //text to draw
                -1,                //length of text
                &rect, 
                dwTextalign | DT_END_ELLIPSIS | DT_EXTERNALLEADING | DT_VCENTER // | DT_SINGLELINE        //text formatting
                );

            EndPaint(hWnd, &ps);
            SelectObject(hdc, hfOld);
            SetTextColor(hdc, oldTextColor);

            DeleteObject(hFont);
            break;

the backdoor to quit

You can quit ShowWin by calling it with ‘-kill’. You can also end ShowWin by double clicking inside the window with the CAPS Lock key being toggled:

        case WM_LBUTTONDBLCLK:
            vkShift=GetKeyState(VK_CAPITAL);
            if( (vkShift & 0x80) == 0x80 || (vkShift & 0x01) == 0x01 ){
                if(MessageBox(hWnd, L"Exit?", L"showWin", MB_OKCANCEL)==IDOK)
                    DestroyWindow(hWnd);
            }
            break;

MessageBox shows a verification dialog and DestroyWindow exits the application.

The End

As we are running a background thread it is a good idea to stop the thread before the application ends. The below code shows the SetEvent call that releases the background thread’s WaitForSingleObject() call.

void stopThread(){
    if(hStopThread==NULL)
        SetEvent(hStopThread);
}
.......
        case WM_DESTROY:
            stopThread();
            PostQuitMessage(0);
            break;
...

Source code download

Full source code available at GitHub.

 

Leave a Reply