The purpose of the article is to explain an idea I had: a universal Progress Bar snippet which can be called or updated from different running programs or processes, in order to display smoothly the progress of any procedure that is handled by several different processes.
Like Eating a Progress Bar with a Fork
Progress bar is a handy tool even during the development phase. When we were testing our new Anti Malware engine, we realized we need a generic / universal Progress Bar to make things easier for our tester, so they know when one of the quite a few components is updating itself.
In this article I explain how to develop a Progress Bar showing the progress of an Auto Update mechanism and working on three different processes at the same time. It’s like eating a (Progress) Bar with a fork – it’s not comfortable, but it’s still delicious no matter what…
Introduction
During the development of an anti-malware engine, we received feedback from the QA team which made us realize that we need to utilize a Progress Bar during the Auto Update mechanism. However, our Auto Update takes place in different processes as illustrated in the figure below.
I therefor accepted the challenge and aimed to develop a snippet that will be able to:
1. Show the progress visually (via a progress bar), and textually (via a textual message).
2. If this snippet is called while it is already running, the running instance will get updated as per the command line arguments used when calling the new instance. Using a mutex was the way to achieve that goal.
3. Be able to mark the progress as completed if a certain command line argument is used.
4. Be minimalistic. Since we develop for Windows, I worked on a Win32 application without using frameworks such as MFC or .NET.
The following gif shows how it works:
AutoUpdate me softly: the "stages" and how to code them
Our auto update is performed by taking the following steps / states illustrated in the figure below:
Let’s look at this process as it is translated into an enum:
typedef enum AutoUpdate_Stage
{
AUS_DownloadStarts = 1,
AUS_DownloadCompleted =2,
AUS_StopOldVersion =3,
AUS_StartNewVersion =4,
AUS_DeleteOldVersion =5,
AUS_Completed =6
};
The Data Structure
I then defined the structure of the message that will be sent between the running processes and the snippet.
typedef struct _MyMessageData
{
AutoUpdate_Stage stage;
wchar_t msg[80];
} MyMessageData;
The msg item is just a textual description of the update (i.e. "The download of file xxx has completed").
The Message Structure
The structure of the message sent from a new instance of the snippet to an existing instance, if there is one, is composed using the MyMessageData
struct, as well as the COPYDATASTRUCT
. The latter is needed in this case, as unlike the way messages can be sent within a process, we send a pointer to the data we wish to include in the message. Our pointer won't work among different processes, as the address stored in the pointer won't be valid outside the process that generated it. There are several solutions to tackle this, and I chose one that is provided by Win32 API. COPYDATASTRUCT
.
The Win32 Data Copy data exchange, allows information exchange between two applications, using the WM_COPYDATA
message. The sending application packs the information to be transferred into a data structure that includes a pointer to the COPYDATASTRUCT
structure, with a private data structure (MyMessageData
), combined. The data we pack comes from the command line argument, hence the use of szArgList.
szArgList[1]
- contains the message
szArgList[2]
- contains the state (as integer).
MyMessageData data;
COPYDATASTRUCT cds;
cds.dwData = (ULONG_PTR)nullptr; cds.cbData = sizeof( data ); cds.lpData = &data; data.stage= (AutoUpdate_Stage)_wtol(szArgList[2]);
wcscpy_s(data.msg,szArgList[1]);
Using Command Line Parameters and Mutex
Before I explain, if you want to know more about Mutex, the following article might help.
Moving forward, the idea here is unusual... When the snippet is called, it requires some command line parameters. It also uses a Mutex, so in the event the snippet is already running, (creating the mutex fails), it passes the running instance the parameters, and as a result, the running instance responds. In other words: the 2nd stage, till the 6th and last, are, in fact, messages passed to the already running instance.
Below is the code snippet used:
static bool reg_singlaton()
{
g_Singlaton = ::CreateMutex(NULL,TRUE,SG_MUTEX_NAME);
switch (::GetLastError())
{
case ERROR_SUCCESS:
return true;
break;
case ERROR_ALREADY_EXISTS:
return false;
break;
default:
return false;
break;
}
}
Then we use the following function upon exiting:
static void unreg_singlaton()
{
if (g_Singlaton)
{
::CloseHandle(g_Singlaton);
g_Singlaton = NULL;
}
}
Sending the message
Next, the message is sent to the running instance. You might wonder how do we obtain a handle to it. Well, again, there are several ways to tackle this. In my case, since my snippet is a static program that uses a static Class name, I call:
HWND hWnd = FindWindowEx(NULL, NULL,window_class_name, NULL);
and then use hWnd
in the code that actually sends the message.
Note: It is also recommended to verify that we got a valid HWND
, by asserting that IsWindow(hWnd)
is true.
SendMessage( hWnd,
WM_COPYDATA,
(WPARAM)(HWND) hWnd,
(LPARAM) (LPVOID) &cds );
Use the "var" button to wrap variable or class names in <code> tags like this
.
Receiving the message
The message is received via the WM_COPYDATA
Windows message. When such type of message arrives, we use the following code to unpack the data and get the original data that was originally packed and sent by the new instance:
case WM_COPYDATA:
PCOPYDATASTRUCT pcdc;
MyMessageData *data;
pcdc = (PCOPYDATASTRUCT) lParam;
data = (MyMessageData *)pcdc->lpData;
Updating the snippet's window
I wrote a small function that updates the snippet's window title with the data received.
This code won't run unless the Language mode, (see Project -> Properties -> C / C++ -> Language settings are set to C++ 20.
void SG_UpdateWindow(HWND hwnd, LPWSTR Messsage, int Stage)
{
std::wstring Msg = std::format(L"{} {}",Messsage,Stage);
SetWindowText(hwnd,Msg.c_str());
}
Executing the snippet programmatically
I used the following function to execute the snippet from the test app, via the command line arguments. There are better ways (for example, using CreateProcess is always better than ShellExecute or ShellExecuteEx.
int Run(LPWSTR Program, LPWSTR Params)
{
SHELLEXECUTEINFO info = {0};
info.cbSize = sizeof(SHELLEXECUTEINFO);
info.fMask = SEE_MASK_NOCLOSEPROCESS;
info.hwnd = NULL;
info.lpVerb = NULL;
info.lpFile = Program;
info.lpParameters = Params;
info.lpDirectory = NULL;
info.nShow = SW_SHOW;
info.hInstApp = NULL;
return(ShellExecuteEx(&info));
}
How the Test App works
Now, its quite easy to use our snippet, and our test appliation shows how:
int main()
{
if(Run((LPWSTR)EXE_PATH,(LPWSTR)L"\"New version is downloading\" 1 \"Code Project demo - by Michael Haephrati.\""))
{
wprintf(L"(1) Success\n");
}
else
{
wprintf(L"Error %d\n",GetLastError());
}
Sleep(3000);
if(Run((LPWSTR)EXE_PATH,(LPWSTR)L"\"Updater is terminating old version\" 2"))
{
wprintf(L"(2) Success\n");
}
else
{
wprintf(L"Error %d\n",GetLastError());
}
Sleep(5000);
if (Run((LPWSTR)EXE_PATH, (LPWSTR)L"\"Now starting new version\" 3"))
{
wprintf(L"(3) Success\n");
}
else
{
wprintf(L"Error %d\n",GetLastError());
}
Sleep(5000);
if (Run((LPWSTR)EXE_PATH, (LPWSTR)L"\"Doing some cleanup\" 4"))
{
wprintf(L"(4) Success\n");
}
else
{
wprintf(L"Error %d\n",GetLastError());
}
Sleep(5000);
if (Run((LPWSTR)EXE_PATH, (LPWSTR)L"\"More work...\" 5"))
{
wprintf(L"(5) Success\n");
}
else
{
wprintf(L"Error %d\n",GetLastError());
}
Sleep(3000);
if(Run((LPWSTR)EXE_PATH,(LPWSTR)L"end 6"))
{
wprintf(L"(6) Success\n");
}
else
{
wprintf(L"Error %d\n",GetLastError());
}
system("pause");
}
User Interface
There were several User Interface related challenges.
We wanted to have our own Win32 API Dialog based program, with the size of 512 x 80, and DPI (Dots Per Inch) awareness.
We wanted the dialog to look like the image below, using the exact colors, font and layout.
The challenging part was to have a custom color title bar. To keep the story short, the trick is to create a frameless dialog, and then manually draw a fake title bar with a fake shadow.
Here is how its done:
It’s mostly hard work, but it’s not as complex as you might imagine: you need to handle the following parts of the creation, as well as handling of the dialog from start to end.
The Creation of the Dialog
Upon creation of the Dialog WM_CREATE
, we do the following:
- We set the Static Text control, when needed (i.e. when it was passed as part of the Command Line arguments).
LPTSTR CreateParam = (LPTSTR)(((LPCREATESTRUCT)lParam)->lpCreateParams);
LPTSTR StaticText = (CreateParam) ? (LPTSTR)CreateParam : (LPTSTR)TEXT("");
g_DialogInfo.hwnds.StaticText = CreateWindowEx(0,WC_STATIC,StaticText,WS_CHILD | WS_VISIBLE,
0, 0, 0, 0,
hwnd,
NULL,
((LPCREATESTRUCT)lParam)->hInstance,
NULL);
2. We initialize the Progress Bar:
g_DialogInfo.hwnds.ProgressBar = CreateWindowEx(0,
PROGRESS_CLASS,
TEXT(""),
WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
0, 0, 0, 0,
hwnd,
NULL,
GetModuleHandle(NULL),
NULL);
3. We initialize the attributes of the Progress Bar, by sending the appropriate messages to it.
SendMessage(g_DialogInfo.hwnds.ProgressBar, PBM_SETSTEP, 1, 0);
SendMessage(g_DialogInfo.hwnds.ProgressBar, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
SendMessage(g_DialogInfo.hwnds.ProgressBar, PBM_SETBARCOLOR, 0, (LPARAM)PROGRESSBAR_BARCOLOR);
SendMessage(g_DialogInfo.hwnds.ProgressBar, PBM_SETBKCOLOR, 0, (LPARAM)PROGRESSBAR_BKCOLOR);
4. We set up a timer, and we progress our Progress Bar every second. However, when a new stage starts, the Progress Bar gauge will “jump” to the relative position.
SetTimer(hwnd, TIMER_ID, 1000, NULL);
Note that during the Timer event, we check if the stage is the last one, and in this case, we terminate the dialog by calling PostQuitMessage.
case WM_TIMER:
{
if (g_DialogInfo.progress < TOP_VALUE)
{
SendMessage(g_DialogInfo.hwnds.ProgressBar, PBM_STEPIT, 0, 0);
g_DialogInfo.progress++;
}
else
{
PostQuitMessage(0);
}
break;
}
The WM_NCCALCSIZE event
Following the WM_NCCALCSIZE event, we obtain a pointer to the positioning and move it left and right using the following code block:
LPNCCALCSIZE_PARAMS ncParams = (LPNCCALCSIZE_PARAMS)lParam;
ncParams->rgrc[0].top -= 1;
ncParams->rgrc[0].left -= 1;
ncParams->rgrc[0].bottom += 1;
ncParams->rgrc[0].right += 1;
The WM_PAINT event
During each WM_PAINT event, we do the following:
- Paint the background
bg_brush = (HBRUSH)(COLOR_WINDOW);
FillRect(hdc, &ps.rcPaint, bg_brush);
2. Paint the border
HDC dc = GetDC(handle);
HBRUSH border_brush = (has_focus) ? CreateSolidBrush(RGB(0, 120, 215)) : (HBRUSH)(COLOR_WINDOW);
RECT wr;
GetClientRect(hwnd, &wr);
wr.left += 1;
wr.right -= 1;
wr.bottom -= 1;
FrameRect(hdc, &wr, border_brush);
DeleteObject(border_brush);
ReleaseDC(hwnd, dc);
3. Painting the Title Bar’s background
COLORREF title_bar_color = (has_focus) ? RGB(0, 120, 215) : RGB(255, 255, 255);
HBRUSH title_bar_brush = CreateSolidBrush(title_bar_color);
RECT title_bar_rect = win32_titlebar_rect(handle);
FillRect(hdc, &title_bar_rect, title_bar_brush);
4. Painting the Title Bar (the fake one)
LOGFONT logical_font;
HFONT old_font = NULL;
if (SUCCEEDED(SystemParametersInfoForDpi(SPI_GETICONTITLELOGFONT, sizeof(logical_font), &logical_font, false, GetDpiForWindow(handle))))
{
HFONT theme_font = CreateFontIndirect(&logical_font);
old_font = (HFONT)SelectObject(hdc, theme_font);
}
wchar_t title_text_buffer[255] = { 0 };
int buffer_length = sizeof(title_text_buffer) / sizeof(title_text_buffer[0]);
GetWindowTextW(handle, title_text_buffer, buffer_length);
RECT title_bar_text_rect = title_bar_rect;
int text_padding = 10; title_bar_text_rect.left += text_padding;
DTTOPTS draw_theme_options = { sizeof(draw_theme_options) };
draw_theme_options.dwFlags = DTT_TEXTCOLOR;
draw_theme_options.crText = has_focus ? RGB(238, 238, 237) : RGB(127, 127, 127);
HTHEME theme = OpenThemeData(handle, L"\x57\x49\x4e\x44\x4f\x57");
DrawThemeTextEx(
theme,
hdc,
0, 0,
title_text_buffer,
-1,
DT_VCENTER | DT_SINGLELINE | DT_WORD_ELLIPSIS,
&title_bar_text_rect,
&draw_theme_options
);
if (old_font) SelectObject(hdc, old_font);
CloseThemeData(theme);
5. Painting the fake shadow
static const COLORREF shadow_color = RGB(100, 100, 100);
COLORREF fake_top_shadow_color = has_focus ? shadow_color : RGB(
(GetRValue(title_bar_color) + GetRValue(shadow_color)) / 2,
(GetGValue(title_bar_color) + GetGValue(shadow_color)) / 2,
(GetBValue(title_bar_color) + GetBValue(shadow_color)) / 2
);
HBRUSH fake_top_shadow_brush = CreateSolidBrush(fake_top_shadow_color);
RECT fake_top_shadow_rect = win32_fake_shadow_rect(handle);
FillRect(hdc, &fake_top_shadow_rect, fake_top_shadow_brush);
DeleteObject(fake_top_shadow_brush);
EndPaint(handle, &ps);
The WM_NCHITTEST event
We also respond to the WM_NCHITTEST event. We need that because there are are cases where we use our dialog’s hwnd
, alongside with DefWindowProc()
to return the ‘hit’
LRESULT hit = DefWindowProc(hwnd, message, wParam, lParam);
switch (hit)
{
case HTNOWHERE:
case HTRIGHT:
case HTLEFT:
case HTTOPLEFT:
case HTTOP:
case HTTOPRIGHT:
case HTBOTTOMRIGHT:
case HTBOTTOM:
case HTBOTTOMLEFT:
{
return hit;
}
The rest of the cases are dealt manually, because in these cases NCCALCSIZE is messing with the detection of the top hit area, leaving us no choice but to manually adjusting it.
UINT dpi = GetDpiForWindow(hwnd);
int frame_y = GetSystemMetricsForDpi(SM_CYFRAME, dpi);
int padding = GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi);
POINT cursor_point = { 0 };
cursor_point.x = LOWORD(lParam);
cursor_point.y = HIWORD(lParam);
ScreenToClient(hwnd, &cursor_point);
if (cursor_point.y > 0 && cursor_point.y < frame_y + padding)
{
return HTCAPTION;
}
DPI Awareness
As for DPI awareness, we use the following function:
static int win32_dpi_scale(int value, UINT dpi)
{
return (int)((float)value * dpi / DENOMINATOR);
}
We define DENOMINATOR
as:
#define DENOMINATOR 96 // The denominator of the ratio as a fraction
We respond to DPI changes as well
case WM_DPICHANGED:
{
UINT dpi = HIWORD(wParam);
reposition_window(dpi, SWP_NOZORDER | SWP_NOMOVE);
} break;
Repositioning the Window
Repositioning (or positioning) the Window is done using the following function:
static void reposition_window(UINT dpi, UINT Action)
{
SIZE sz = { win32_dpi_scale(WIDTH, dpi), win32_dpi_scale(HEIGHT, dpi) };
RECT DesktopRect;
SystemParametersInfo(SPI_GETWORKAREA, NULL, &DesktopRect, NULL);
RECT WindowRect;
GetWindowRect(g_DialogInfo.hwnds.MainDialog, &WindowRect);
SetWindowPos(g_DialogInfo.hwnds.MainDialog, HWND_TOPMOST,
DesktopRect.right - sz.cx - LEFT_BORDER,
DesktopRect.bottom - sz.cy - TOP_BORDER,
sz.cx, sz.cy,
Action);
reposition_child_ctrl(dpi);
}
The function reposition_child_ctrl() is as follow:
static void reposition_child_ctrl(UINT dpi)
{
RECT clientRect;
GetWindowRect(g_DialogInfo.hwnds.MainDialog, &clientRect);
int clientWidth = clientRect.right - clientRect.left;
int clientHeight = clientRect.bottom - clientRect.top;
RECT tbRect = win32_titlebar_rect(g_DialogInfo.hwnds.MainDialog);
int titlebar_height = tbRect.bottom - tbRect.top;
SetWindowPos(g_DialogInfo.hwnds.StaticText, 0,
LEFT_BORDER, MulDiv(TOP_BORDER, dpi, DENOMINATOR) + titlebar_height,
clientWidth - (2 * LEFT_BORDER), MulDiv(STATIC_HEIGHT, dpi, DENOMINATOR),
SWP_NOZORDER | SWP_FRAMECHANGED);
HFONT hFont;
LOGFONT lf = {};
HDC hdc = GetDC(g_DialogInfo.hwnds.MainDialog);
lf.lfHeight = -(MulDiv(12, dpi, DENOMINATOR));
lf.lfWeight = FW_NORMAL;
lf.lfQuality = PROOF_QUALITY;
const TCHAR* fontName = TEXT("\x4d\x53\x20\x53\x68\x65\x6c\x6c\x20\x44\x6c\x67");
string_cpy(lf.lfFaceName, fontName);
hFont = CreateFontIndirect(&lf);
ReleaseDC(g_DialogInfo.hwnds.MainDialog, hdc);
SendMessage(g_DialogInfo.hwnds.MainDialog, WM_SETFONT, (WPARAM)hFont, TRUE);
SendMessage(g_DialogInfo.hwnds.StaticText, WM_SETFONT, (WPARAM)hFont, TRUE);
SetWindowPos(g_DialogInfo.hwnds.ProgressBar, 0,
LEFT_BORDER, MulDiv((TOP_BORDER + CTRL_BORDER + STATIC_HEIGHT),
dpi, DENOMINATOR) + titlebar_height,
clientWidth - (2 * LEFT_BORDER), MulDiv(PRGBAR_HEIGHT, dpi, DENOMINATOR),
SWP_NOZORDER);
}
Obfuscation
With no specific need, I played around with TinyObfuscate and "obfuscated" a const string (which isn't really possible, hence the quotation marks...).
SG_MUTEX_NAME
is defined as
TEXT("\x53\x47\x4d\x5f\x7b\x34\x38\x38\x45\x36\x31\x31\x42\x2d\x31\x33\x39\x35\x2d\x34\x37\x34\x37\x2d\x42\x43\x43\x39\x2d\x33\x35\x37\x31\x38\x41\x37\x35\x32\x46\x31\x43\x7d")
which stands for
SGM_{488E611B-1395-4747-BCC9-35718A752F1C}
so that's not really obfuscation...