diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b59514..dba9d89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,24 @@
All notable project changes are documented here. The project follows semantic versioning after the first generally available release.
+## [0.4.0-rc4] - 2026-08-20
+
+### Added
+
+- Original blue shield/cleanup application icon embedded at 16–256 pixel sizes for Explorer, the title bar, taskbar, and UAC display.
+- The transparent high-resolution icon source and multi-resolution ICO are included in the open-source package.
+
+### Fixed
+
+- RU/EN switching now redraws the dialog atomically, preventing labels, buttons, and some list headers from appearing blank until a later repaint.
+- The scan-complete status and count summary at the bottom are immediately regenerated in the selected language.
+- Product, profile, and certificate selections are preserved while translated lists are rebuilt.
+
+### Validation
+
+- Automated GUI probing confirmed that all affected captions, the status, and the product/license/certificate summary change from Russian to English immediately.
+- Both 16- and 32-pixel icon resources load successfully from the release EXE; core tests and the Release x86 build pass.
+
## [0.4.0-rc3] - 2026-08-20
### Changed
diff --git a/CryptoProCleanup.vcxproj b/CryptoProCleanup.vcxproj
index feba097..1f97d9b 100644
--- a/CryptoProCleanup.vcxproj
+++ b/CryptoProCleanup.vcxproj
@@ -38,5 +38,7 @@
+
+
diff --git a/README.en.md b/README.en.md
index ebfc246..ca032f5 100644
--- a/README.en.md
+++ b/README.en.md
@@ -5,7 +5,7 @@
[](LICENSE)
[](#system-requirements)
-> **Status: 0.4.0 RC3.** A user successfully completed full removal on a live Windows 10 x64 system. Detection of products and the complete license plus public-certificate export are now confirmed on a real connected Windows 7 x86 disk. RC3 adds responsive background scanning with live indication.
+> **Status: 0.4.0 RC4.** A user successfully completed full removal on a live Windows 10 x64 system. Detection of products and the complete license plus public-certificate export are confirmed on a real connected Windows 7 x86 disk. RC4 adds a recognizable application icon and fixes complete repaint plus bottom-summary translation when switching RU/EN.
An unofficial portable utility for backing up license identifiers and public certificates, controlled removal of installed CryptoPro products, and rescue from a disconnected Windows 7 SP1 through Windows 11 installation.
@@ -22,6 +22,7 @@ The utility deliberately preserves Windows certificate stores, hardware tokens,
## Features
- native portable Unicode Win32 GUI with RU/EN languages and no .NET dependency;
+- original multi-resolution application icon for Explorer, taskbar, window chrome, and UAC;
- resizable/maximizable native window with adaptive tables, system-DPI scaling, and Windows visual styles;
- verified 32/64-bit product discovery by publisher and MSI/EXE metadata;
- complete license display, clipboard copy, and confidential backup;
diff --git a/README.md b/README.md
index 8d1cc2b..3a207bb 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
[](LICENSE)
[](#системные-требования)
-> **Статус: релиз-кандидат 0.4.0 RC3.** Полное удаление успешно проверено пользователем на живой Windows 10 x64. На реальном подключённом диске Windows 7 x86 подтверждены обнаружение продуктов, полный номер лицензии и экспорт открытых сертификатов. RC3 добавляет отзывчивое фоновое сканирование с живой индикацией.
+> **Статус: релиз-кандидат 0.4.0 RC4.** Полное удаление успешно проверено пользователем на живой Windows 10 x64. На реальном подключённом диске Windows 7 x86 подтверждены обнаружение продуктов, полный номер лицензии и экспорт открытых сертификатов. RC4 добавляет узнаваемую иконку и исправляет полную перерисовку и нижний итог при переключении RU/EN.
Неофициальная portable-утилита для резервного копирования лицензий и открытых сертификатов, контролируемого удаления установленных продуктов CryptoPro и спасения данных из отключённой Windows 7 SP1–Windows 11.
@@ -27,6 +27,7 @@
## Возможности
- один нативный x86 EXE без .NET/PowerShell и внешнего runtime;
+- оригинальная многоразмерная иконка приложения для Проводника, панели задач, окна и UAC;
- изменяемое и максимизируемое окно с адаптивными таблицами, системным DPI-масштабированием и нативным современным оформлением;
- поиск продуктов по подтверждённому издателю в 32- и 64-битных ветках реестра;
- RU/EN интерфейс, выбор продуктов и локальных профилей;
diff --git a/assets/CryptoProCleanup.ico b/assets/CryptoProCleanup.ico
new file mode 100644
index 0000000..86f0fec
Binary files /dev/null and b/assets/CryptoProCleanup.ico differ
diff --git a/assets/app-icon-source.png b/assets/app-icon-source.png
new file mode 100644
index 0000000..eab329c
Binary files /dev/null and b/assets/app-icon-source.png differ
diff --git a/docs/RELEASE_NOTES_0.4.0-rc4.md b/docs/RELEASE_NOTES_0.4.0-rc4.md
new file mode 100644
index 0000000..b813074
--- /dev/null
+++ b/docs/RELEASE_NOTES_0.4.0-rc4.md
@@ -0,0 +1,18 @@
+# КриптоПро Очистка 0.4.0 RC4
+
+В этом релизе у программы появилась собственная узнаваемая иконка, а переключение русского и английского интерфейса стало полностью синхронным.
+
+## Иконка
+
+- оригинальный знак «защитный щит + безопасная очистка» не копирует фирменную символику CryptoPro;
+- в EXE встроены размеры от 16×16 до 256×256 для Проводника, панели задач, заголовка окна и UAC;
+- прозрачный исходник PNG и многоразмерный ICO входят в архив исходников.
+
+## Исправление RU/EN
+
+- все элементы окна теперь обновляются одной атомарной перерисовкой;
+- подписи, кнопки и заголовки первых столбцов больше не остаются временно пустыми;
+- нижний статус `Ready. No changes were made.` и строка `Products found…` сразу переводятся на выбранный язык;
+- отмеченные продукты, профили и сертификаты сохраняют свои флажки при перестроении таблиц.
+
+Алгоритмы сканирования, удаления, экспорта сертификатов и защиты закрытых ключей не изменялись.
diff --git a/docs/TEST_MATRIX.md b/docs/TEST_MATRIX.md
index 7552654..6717d63 100644
--- a/docs/TEST_MATRIX.md
+++ b/docs/TEST_MATRIX.md
@@ -42,6 +42,7 @@ For each VM retain the initial JSON report, final JSON report, cleanup log, prod
- 2026-08-20: user testing of 0.4.0 RC2 on the real connected Windows 7 x86 disk confirmed product detection, the correct complete license identifier, and successful public-certificate export. RC3 moves this scan to a responsive background worker with continuous progress indication.
- 2026-08-20: read-only synthetic disconnected-Windows fixture passed with 2 confirmed CryptoPro products, 4 license candidates, 1 inactive profile, and 7 public certificates from `AppData\Roaming\Microsoft\SystemCertificates\My\Certificates`; both `SOFTWARE` and `SYSTEM` temporary mounts unloaded successfully.
- 2026-08-20: RC2 implementation passed unit tests, the non-destructive live integration scan, and MSVC C++ static analysis with no warnings. No `CryptoProCleanup_Offline_*` registry mounts remained after testing; sensitive hive/certificate fixture copies were removed.
+- 2026-08-20: RC4 automated GUI probing confirmed immediate RU-to-EN translation of affected labels/buttons, status, and the product/license/certificate summary. Embedded 16- and 32-pixel icon resources loaded successfully from the Release EXE.
- The running Windows directory is rejected as an offline target. A destructive disconnected-Windows VM test is still pending.
- 2026-08-19: MSVC C++ static analysis passed with no warnings. Offline scanning also compares the actual `SOFTWARE` and `SYSTEM` hive file identities so a filesystem alias to the running Windows installation is rejected.
- Destructive scenarios and the remaining OS/CSP matrix, including CSP 4.x, are still mandatory before marking a build generally available.
diff --git a/scripts/package.ps1 b/scripts/package.ps1
index 85a9cbd..98c8a05 100644
--- a/scripts/package.ps1
+++ b/scripts/package.ps1
@@ -22,7 +22,7 @@ Copy-Item -LiteralPath (Join-Path $projectRoot 'docs') -Destination $packageRoot
$sourceStage = Join-Path $packageRoot 'source'
New-Item -ItemType Directory -Path $sourceStage -Force | Out-Null
-foreach ($directory in @('.github', 'src', 'tests', 'scripts', 'docs')) { Copy-Item -LiteralPath (Join-Path $projectRoot $directory) -Destination $sourceStage -Recurse }
+foreach ($directory in @('.github', 'assets', 'src', 'tests', 'scripts', 'docs')) { Copy-Item -LiteralPath (Join-Path $projectRoot $directory) -Destination $sourceStage -Recurse }
foreach ($file in @('.gitattributes', '.gitignore', 'CryptoProCleanup.sln', 'CryptoProCleanup.vcxproj', 'CryptoProCleanupTests.vcxproj', 'README.md', 'README.en.md', 'LICENSE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'SECURITY.md')) { Copy-Item -LiteralPath (Join-Path $projectRoot $file) -Destination $sourceStage }
$sourceZip = Join-Path $packageRoot "CryptoProCleanup-$Version-source.zip"
diff --git a/src/cleanup.hpp b/src/cleanup.hpp
index 2e2bddf..178702b 100644
--- a/src/cleanup.hpp
+++ b/src/cleanup.hpp
@@ -10,7 +10,7 @@
namespace cpc {
-constexpr wchar_t kVersion[] = L"0.4.0-rc3";
+constexpr wchar_t kVersion[] = L"0.4.0-rc4";
enum class Language { Russian, English };
enum class RiskLevel { Normal, High };
diff --git a/src/gui.cpp b/src/gui.cpp
index e19953c..31d735f 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -28,6 +28,12 @@ struct OfflineProgressMessage {
int percent = 0;
};
+enum class SummaryKind {
+ None,
+ LiveScan,
+ OfflineScan
+};
+
struct AppState {
HWND window = nullptr;
Language language = Language::English;
@@ -49,6 +55,8 @@ struct AppState {
bool busy = false;
bool offlineScanRunning = false;
std::thread offlineScanThread;
+ SummaryKind summaryKind = SummaryKind::None;
+ Language summaryLanguage = Language::English;
};
struct ConfirmState {
@@ -244,6 +252,56 @@ void AddLogLine(AppState& state, const std::wstring& line) {
if (!state.logPath.empty()) AppendLog(state.logPath, line);
}
+std::wstring LiveScanSummary(const AppState& state) {
+ std::wostringstream summary;
+ summary << Tr(state.language, L"Найдено продуктов: ", L"Products found: ") << state.scan.products.size()
+ << Tr(state.language, L", лицензий: ", L", licenses: ") << state.scan.licenses.size()
+ << Tr(state.language, L", открытых сертификатов: ", L", public certificates: ")
+ << state.scan.certificates.size();
+ return summary.str();
+}
+
+std::wstring OfflineScanSummary(const AppState& state) {
+ if (!state.offline.valid)
+ return Tr(state.language, L"Офлайн-система не распознана.",
+ L"Offline system was not recognized.");
+ std::wostringstream summary;
+ summary << Tr(state.language, L"Офлайн: продуктов ", L"Offline: products ") << state.offline.scan.products.size()
+ << Tr(state.language, L", лицензий ", L", licenses ") << state.offline.scan.licenses.size()
+ << Tr(state.language, L", профилей ", L", profiles ") << state.offline.scan.profiles.size()
+ << Tr(state.language, L", открытых сертификатов ", L", public certificates ") << state.offline.scan.certificates.size()
+ << Tr(state.language, L", подтверждённых целей ", L", verified targets ") << state.offline.targets.size()
+ << (state.offline.cleanupCapable ? Tr(state.language, L", очистка доступна", L", cleanup available")
+ : Tr(state.language, L", только спасение данных", L", rescue only"));
+ return summary.str();
+}
+
+void RefreshLocalizedSummary(AppState& state) {
+ if (state.summaryKind == SummaryKind::None || !state.logPath.empty()) return;
+ const bool live = state.summaryKind == SummaryKind::LiveScan;
+ const std::wstring summary = live ? LiveScanSummary(state) : OfflineScanSummary(state);
+ const auto& warnings = live ? state.scan.warnings : state.offline.scan.warnings;
+ std::wstring logText = summary;
+ if (!warnings.empty() && state.summaryLanguage != state.language) {
+ logText += L"\r\n" + Tr(state.language,
+ L"Примечание: предупреждения ниже сформированы до переключения языка. Для их перевода повторите сканирование.",
+ L"Note: the warnings below were generated before the language switch. Scan again to translate them.");
+ }
+ for (const auto& warning : warnings) logText += L"\r\n" + warning;
+ SetWindowTextW(GetDlgItem(state.window, IDC_LOG), logText.c_str());
+ if (live) {
+ SetText(state.window, IDC_STATUS,
+ Tr(state.language, L"Готово. Изменения не выполнялись.",
+ L"Ready. No changes were made."));
+ } else if (state.offline.valid) {
+ SetText(state.window, IDC_STATUS,
+ Tr(state.language, L"Офлайн-сканирование завершено. Изменения не выполнялись.",
+ L"Offline scan completed. No changes were made."));
+ } else {
+ SetText(state.window, IDC_STATUS, summary);
+ }
+}
+
std::wstring DefaultBackupFolder() {
PWSTR raw = nullptr;
std::wstring folder;
@@ -483,7 +541,11 @@ void UpdateTabVisibility(AppState& state) {
for (const int id : offlineControls) ShowWindow(GetDlgItem(state.window, id), selected == 2 ? SW_SHOW : SW_HIDE);
}
+void ReadSelections(AppState& state);
+void ReadOfflineSelections(AppState& state);
+
void ApplyLanguage(AppState& state) {
+ SendMessageW(state.window, WM_SETREDRAW, FALSE, 0);
const bool ru = state.language == Language::Russian;
const std::wstring applicationName = ru ? L"КриптоПро Очистка" : L"CryptoPro Cleanup Utility";
const std::wstring versionedName = applicationName + L" " + kVersion;
@@ -544,6 +606,10 @@ void ApplyLanguage(AppState& state) {
if (!state.scan.products.empty() || !state.scan.profiles.empty() || !state.scan.certificates.empty()) PopulateLists(state);
if (state.offline.valid) PopulateOfflineLists(state);
UpdateTabVisibility(state);
+ RefreshLocalizedSummary(state);
+ SendMessageW(state.window, WM_SETREDRAW, TRUE, 0);
+ RedrawWindow(state.window, nullptr, nullptr,
+ RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_UPDATENOW);
}
void SetBusy(AppState& state, bool busy) {
@@ -810,23 +876,15 @@ void ReadOfflineSelections(AppState& state) {
void FinishOfflineScan(AppState& state, OfflineScanResult&& result) {
state.offline = std::move(result);
PopulateOfflineLists(state);
- std::wostringstream summary;
if (state.offline.valid) {
SetText(state.window, IDC_OFFLINE_PATH, state.offline.windowsDirectory);
- summary << Tr(state.language, L"Офлайн: продуктов ", L"Offline: products ") << state.offline.scan.products.size()
- << Tr(state.language, L", лицензий ", L", licenses ") << state.offline.scan.licenses.size()
- << Tr(state.language, L", профилей ", L", profiles ") << state.offline.scan.profiles.size()
- << Tr(state.language, L", открытых сертификатов ", L", public certificates ") << state.offline.scan.certificates.size()
- << Tr(state.language, L", подтверждённых целей ", L", verified targets ") << state.offline.targets.size()
- << (state.offline.cleanupCapable ? Tr(state.language, L", очистка доступна", L", cleanup available")
- : Tr(state.language, L", только спасение данных", L", rescue only"));
- UpdateProgress(state, Tr(state.language, L"Офлайн-сканирование завершено. Изменения не выполнялись.", L"Offline scan completed. No changes were made."), 100);
- } else {
- summary << Tr(state.language, L"Офлайн-система не распознана.", L"Offline system was not recognized.");
- UpdateProgress(state, summary.str(), 100);
}
- AddLogLine(state, summary.str());
- for (const auto& warning : state.offline.scan.warnings) AddLogLine(state, warning);
+ state.summaryKind = SummaryKind::OfflineScan;
+ state.summaryLanguage = state.language;
+ SendDlgItemMessageW(state.window, IDC_PROGRESS, PBM_SETPOS, 100, 0);
+ RefreshLocalizedSummary(state);
+ RedrawWindow(state.window, nullptr, nullptr,
+ RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
SetBusy(state, false);
if (!state.offline.valid || (state.offline.scan.products.empty() && state.offline.scan.certificates.empty()))
ShowOfflineDiagnostics(state);
@@ -970,13 +1028,12 @@ void DoScan(AppState& state) {
SetWindowTextW(GetDlgItem(state.window, IDC_LOG), L"");
state.scan = ScanSystem(state.language, [&](const std::wstring& message, int percent) { UpdateProgress(state, message, percent); });
PopulateLists(state);
- std::wostringstream summary;
- summary << Tr(state.language, L"Найдено продуктов: ", L"Products found: ") << state.scan.products.size()
- << Tr(state.language, L", лицензий: ", L", licenses: ") << state.scan.licenses.size()
- << Tr(state.language, L", открытых сертификатов: ", L", public certificates: ") << state.scan.certificates.size();
- AddLogLine(state, summary.str());
- for (const auto& warning : state.scan.warnings) AddLogLine(state, warning);
- UpdateProgress(state, Tr(state.language, L"Готово. Изменения не выполнялись.", L"Ready. No changes were made."), 100);
+ state.summaryKind = SummaryKind::LiveScan;
+ state.summaryLanguage = state.language;
+ SendDlgItemMessageW(state.window, IDC_PROGRESS, PBM_SETPOS, 100, 0);
+ RefreshLocalizedSummary(state);
+ RedrawWindow(state.window, nullptr, nullptr,
+ RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW);
SetBusy(state, false);
}
@@ -1120,6 +1177,15 @@ INT_PTR CALLBACK MainDialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM
state = reinterpret_cast(lParam);
state->window = dialog;
SetWindowLongPtrW(dialog, DWLP_USER, reinterpret_cast(state));
+ HINSTANCE module = GetModuleHandleW(nullptr);
+ HICON largeIcon = reinterpret_cast(LoadImageW(
+ module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
+ GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR | LR_SHARED));
+ HICON smallIcon = reinterpret_cast(LoadImageW(
+ module, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
+ GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR | LR_SHARED));
+ if (largeIcon) SendMessageW(dialog, WM_SETICON, ICON_BIG, reinterpret_cast(largeIcon));
+ if (smallIcon) SendMessageW(dialog, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon));
SendDlgItemMessageW(dialog, IDC_PROGRESS, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
HWND language = GetDlgItem(dialog, IDC_LANGUAGE);
ComboBox_AddString(language, L"Русский");
@@ -1198,6 +1264,8 @@ INT_PTR CALLBACK MainDialogProc(HWND dialog, UINT message, WPARAM wParam, LPARAM
switch (LOWORD(wParam)) {
case IDC_LANGUAGE:
if (HIWORD(wParam) == CBN_SELCHANGE && !state->busy) {
+ ReadSelections(*state);
+ ReadOfflineSelections(*state);
state->language = ComboBox_GetCurSel(GetDlgItem(dialog, IDC_LANGUAGE)) == 0 ? Language::Russian : Language::English;
ApplyLanguage(*state);
}
diff --git a/src/resource.h b/src/resource.h
index 8286997..c45ae90 100644
--- a/src/resource.h
+++ b/src/resource.h
@@ -4,6 +4,8 @@
#define IDD_TYPED_CONFIRM 102
#define IDD_LICENSES 103
+#define IDI_APP_ICON 201
+
#define IDC_TITLE 1001
#define IDC_DISCLAIMER 1002
#define IDC_PRODUCTS_LABEL 1003
diff --git a/src/resources.rc b/src/resources.rc
index 280acc7..b2d7d32 100644
--- a/src/resources.rc
+++ b/src/resources.rc
@@ -3,6 +3,8 @@
#include
#include "resource.h"
+IDI_APP_ICON ICON "../assets/CryptoProCleanup.ico"
+
IDD_MAIN DIALOGEX 0, 0, 760, 610
STYLE DS_SETFONT | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CLIPCHILDREN
EXSTYLE WS_EX_APPWINDOW