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 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
.NET 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);
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"));
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 ()
{
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){ if(liste->next != NULL){
liste=liste->next;
wsprintf(szMessageText, L"%s", liste->value);
}
}
else if(wcsicmp(liste->value, L"-m")==0){ if(liste->next != NULL){
liste=liste->next;
wsprintf(szMessageTextNew, L"%s", liste->value);
}
}
else if(wcsicmp(liste->value, L"-r")==0){ 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:
if(hwndOld!=NULL){
if(wcslen(szMessageTextNew) > 0){
myMsg _mymsg;
memset(&_mymsg,0,sizeof(myMsg));
wsprintf( _mymsg.szText, L"%s", szMessageTextNew );
_mymsg.iVal=0; COPYDATASTRUCT copyData;
copyData.dwData=1234;
copyData.cbData=sizeof(myMsg);
copyData.lpData=&_mymsg;
SendMessage(hwndOld, WM_COPYDATA, (WPARAM)NULL, (LPARAM)©Data);
}
if(iProgressValNew!=-1){
myMsg _mymsg;
memset(&_mymsg,0,sizeof(myMsg));
wsprintf( _mymsg.szText, L"%i", iProgressValNew );
_mymsg.iVal=1; COPYDATASTRUCT copyData;
copyData.dwData=1234;
copyData.cbData=sizeof(myMsg);
copyData.lpData=&_mymsg;
SendMessage(hwndOld, WM_COPYDATA, (WPARAM)NULL, (LPARAM)©Data);
}
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
transferred 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) {
memcpy(&_mymsg, copyData->lpData, sizeof(myMsg));
}
if(_mymsg.iVal==0){ if(wcslen(_mymsg.szText)>0)
wcscpy(szMessageText, _mymsg.szText);
GetClientRect(hWnd, &rect);
InvalidateRect(hWnd, &rect, TRUE);
}
else if(_mymsg.iVal==1){ 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:
int maxX = GetSystemMetrics(SM_CXSCREEN);
int maxY = GetSystemMetrics(SM_CYSCREEN); int borderSize = GetSystemMetrics(SM_CXBORDER);
int minSize = GetSystemMetrics(SM_CYMENU);
RECT rectMax;
GetWindowRect(GetDesktopWindow(), &rectMax);
if(xWidth<100 || xWidth>maxX) xWidth=maxX-2*borderSize;
if(yHeight<minSize)
yHeight=minSize+2*borderSize;
if(xPos<borderSize) xPos=borderSize;
if(yPos<rectMax.top) yPos=rectMax.top;
if(bUseProgress){
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.
if (!InitInstance(hInstance, nCmdShow))
{
return FALSE;
}
HACCEL hAccelTable;
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_SHOWWIN));
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; 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, szWindowClass, NULL, WS_VISIBLE, xPos, yPos, xWidth, yHeight, NULL, NULL, 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:
hdc=GetWindowDC(hWnd);
iDevCap=GetDeviceCaps(hdc, LOGPIXELSY); lfHeight = -((long)fontHeight * (long)iDevCap) / 72L;
GetObject (GetStockObject (SYSTEM_FONT), sizeof (LOGFONT), (PTSTR) &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){
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);
GetClientRect(hWnd, &rect);
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.
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);
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, -1, &rect,
dwTextalign | DT_END_ELLIPSIS | DT_EXTERNALLEADING | DT_VCENTER );
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.