一个通用的进度条代码段,使用 Mutex 进行进程间通信






4.70/5 (7投票s)
本文解释的进度条片段可以从不同的正在运行的程序调用或更新。
像用叉子吃进度条一样
进度条即使在开发阶段也是一个有用的工具。我们在测试新的反恶意软件引擎时,意识到我们需要一个通用/万能的进度条来让测试人员更容易了解情况,这样他们就知道有多少组件正在更新。
在本文中,我将解释如何开发一个进度条,该进度条显示自动更新机制的进度,并且可以同时在三个不同的进程上工作。这就像用叉子吃(进度)条一样——不舒服,但无论如何都很美味……
引言
在反恶意软件引擎的开发过程中,我们收到了QA团队的反馈,这使我们意识到在自动更新机制中需要使用进度条。然而,我们的自动更新发生在不同的进程中,如下图所示。
因此,我接受了挑战,目标是开发一个能够
1. 直观地(通过进度条)和文本上(通过文本消息)显示进度。
2. 如果在已有实例运行时调用此片段,则正在运行的实例将根据调用新实例时使用的命令行参数进行更新。使用互斥体是实现此目标的方法。
3. 如果使用特定的命令行参数,能够将进度标记为已完成。
4. 极简。由于我们为Windows开发,我使用的是Win32应用程序,不使用MFC或.NET等框架。
下面的gif展示了它的工作原理
轻柔地自动更新我:“阶段”以及如何编码它们
我们的自动更新是通过以下步骤/状态完成的,如下图所示
让我们看看这个过程是如何翻译成枚举的
typedef enum AutoUpdate_Stage
{
AUS_DownloadStarts = 1,
AUS_DownloadCompleted =2,
AUS_StopOldVersion =3,
AUS_StartNewVersion =4,
AUS_DeleteOldVersion =5,
AUS_Completed =6
};
数据结构
然后,我定义了将在运行进程和片段之间发送的消息结构。
typedef struct _MyMessageData
{
AutoUpdate_Stage stage;
wchar_t msg[80];
} MyMessageData;
msg 项只是更新的文本描述(例如,“文件 xxx 的下载已完成”)。
消息结构
如果存在现有实例,则从片段的新实例发送到现有实例的消息结构使用MyMessageData
结构以及COPYDATASTRUCT
进行组合。后者在此案例中是必需的,因为与进程内发送消息的方式不同,我们发送的是指向我们希望包含在消息中的数据的指针。我们的指针在不同进程之间将无法工作,因为指针中存储的地址在生成它的进程之外将无效。有几种解决方案可以解决这个问题,我选择了一种Win32 API提供的方法:COPYDATASTRUCT
。
Win32的数据复制数据交换允许使用WM_COPYDATA
消息在两个应用程序之间进行信息交换。发送应用程序将要传输的信息打包到一个数据结构中,该数据结构包含一个指向COPYDATASTRUCT
结构的指针,并组合了一个私有数据结构(MyMessageData
)。我们打包的数据来自命令行参数,因此使用了szArgList。
szArgList[1]
- 包含消息
szArgList[2]
- 包含状态(作为整数)。
MyMessageData data;
COPYDATASTRUCT cds;
cds.dwData = (ULONG_PTR)nullptr; // function identifier
cds.cbData = sizeof( data ); // size of data
cds.lpData = &data; // data structure
data.stage= (AutoUpdate_Stage)_wtol(szArgList[2]);
wcscpy_s(data.msg,szArgList[1]);
使用命令行参数和互斥体
在我解释之前,如果你想了解更多关于互斥体的信息,下面的文章可能会有所帮助。
继续,这里的想法很不寻常……当调用片段时,它需要一些命令行参数。它还使用一个互斥体,因此在片段已经运行时(创建互斥体失败),它将参数传递给正在运行的实例,结果是正在运行的实例做出响应。换句话说:第二阶段到第六阶段(也是最后阶段),实际上是传递给已运行实例的消息。
下面是使用的代码片段:
static bool reg_singlaton()
{
g_Singlaton = ::CreateMutex(NULL,TRUE,SG_MUTEX_NAME);
switch (::GetLastError())
{
case ERROR_SUCCESS:
// Process was not running already
return true;
break;
case ERROR_ALREADY_EXISTS:
// Process is running already
return false;
break;
default:
// Error occured, not sure whether process is running already.
return false;
break;
}
}
然后,我们在退出时使用以下函数
static void unreg_singlaton() { if (g_Singlaton) { ::CloseHandle(g_Singlaton); g_Singlaton = NULL; } }
发送消息
接下来,将消息发送到正在运行的实例。你可能会想,我们如何获得它的句柄呢?嗯,同样,有几种方法可以解决这个问题。在我的例子中,由于我的片段是一个使用静态类名的静态程序,我调用
HWND hWnd = FindWindowEx(NULL, NULL,window_class_name, NULL);
然后使用hWnd
在实际发送消息的代码中。
注意:建议通过断言IsWindow(hWnd)
为true来验证我们是否获得了有效的HWND
。
SendMessage( hWnd,
WM_COPYDATA,
(WPARAM)(HWND) hWnd,
(LPARAM) (LPVOID) &cds );
使用“var”按钮将变量或类名包装在<code>
标签中,如this
。
接收消息
消息通过WM_COPYDATA
Windows消息接收。当此类消息到达时,我们使用以下代码来解包数据并获取新实例最初打包和发送的原始数据。
case WM_COPYDATA:
PCOPYDATASTRUCT pcdc;
MyMessageData *data;
pcdc = (PCOPYDATASTRUCT) lParam;
data = (MyMessageData *)pcdc->lpData;
更新片段的窗口
我写了一个小程序来更新片段的窗口标题,以显示收到的数据。
此代码将无法运行,除非语言模式(请参阅项目 -> 属性 -> C / C++ -> 语言设置)设置为C++20。
void SG_UpdateWindow(HWND hwnd, LPWSTR Messsage, int Stage)
{
std::wstring Msg = std::format(L"{} {}",Messsage,Stage);
SetWindowText(hwnd,Msg.c_str());
}
以编程方式执行片段
我使用以下函数通过命令行参数从测试应用程序执行片段。有更好的方法(例如,使用CreateProcess总是比ShellExecute或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));
}
测试应用程序的工作原理
现在,使用我们的片段非常容易,我们的测试应用程序展示了如何做到这一点
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");
}
用户界面
有几个与用户界面相关的挑战。
我们希望拥有自己的Win32 API对话框程序,尺寸为512 x 80,并且具有DPI(每英寸点数)感知能力。
我们希望对话框看起来像下面的图片,使用确切的颜色、字体和布局。
具有自定义颜色标题栏的部分更具挑战性。为了简短起见,技巧是创建一个无边框的对话框,然后手动绘制一个假的标题栏和一个假的阴影。
这是如何完成的
这主要是辛苦的工作,但它不像你想象的那么复杂:你需要处理创建的以下部分,以及从头到尾处理对话框。
对话框的创建
在创建对话框WM_CREATE
时,我们执行以下操作
- 我们在需要时(即当它作为命令行参数的一部分传递时)设置静态文本控件。
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. 我们初始化进度条
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. 我们通过向进度条发送适当的消息来初始化其属性。
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. 我们设置一个计时器,并每秒更新我们的进度条。但是,当新阶段开始时,进度条的刻度将“跳”到相对位置。
SetTimer(hwnd, TIMER_ID, 1000, NULL);
请注意,在计时器事件期间,我们检查该阶段是否是最后一个阶段,在这种情况下,我们通过调用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;
}
WM_NCCALCSIZE 事件
在WM_NCCALCSIZE
事件之后,我们获得一个指向定位的指针,并使用以下代码块将其左右移动。
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;
WM_PAINT 事件
在每个WM_PAINT 事件期间,我们执行以下操作
- 绘制背景
bg_brush = (HBRUSH)(COLOR_WINDOW);
FillRect(hdc, &ps.rcPaint, bg_brush);
2. 绘制边框
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. 绘制标题栏背景
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. 绘制标题栏(假标题栏)
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;
// Add padding on the left
int text_padding = 10; // There seems to be no good way to get this offset
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. 绘制假阴影
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);
WM_NCHITTEST 事件
我们还响应WM_NCHITTEST 事件。我们需要它,因为在某些情况下,我们将对话框的hwnd
与DefWindowProc()
一起使用来返回“命中”
// DefWindowProc uses our dialog's hwnd for resizing areas
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;
}
其余情况手动处理,因为在这些情况下NCCALCSIZE 会干扰顶部命中区域的检测,使我们别无选择,只能手动调整它。
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 感知
至于DPI感知,我们使用以下函数
static int win32_dpi_scale(int value, UINT dpi)
{
return (int)((float)value * dpi / DENOMINATOR);
}
我们将DENOMINATOR
定义为
#define DENOMINATOR 96 // The denominator of the ratio as a fraction
我们也响应DPI变化
case WM_DPICHANGED:
{
UINT dpi = HIWORD(wParam);
reposition_window(dpi, SWP_NOZORDER | SWP_NOMOVE);
} break;
重新定位窗口
使用以下函数进行窗口的重新定位(或定位)。
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);
// Reposition the dialog
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);
}
函数reposition_child_ctrl() 如下
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;
// Reposition static control
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);
// Update the font for static control
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);
// Repostition the progressbar control
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);
}
混淆
在没有特定需求的情况下,我玩了一下TinyObfuscate 并“混淆”了一个const字符串(实际上不可能,所以用了引号……)。
SG_MUTEX_NAME
定义为
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")
它代表
SGM_{488E611B-1395-4747-BCC9-35718A752F1C}
所以这并不是真正的混淆……