Архив рубрики: WinAPI

Блокировка кнопки и контекстного меню закрытия консольного окна

Как известно, accoreconsole.exe всегда был и до сих пор остаётся достаточно кривым... Один из неприятных аспектов его поведения, присутствующий по сей день, заключается в том, что если завершать работу приложения кликом мышки по кнопке закрытия консольного окна в верхнем правом углу, либо выбирая соответствующий пункт из контекстного меню консольного окна, то приложение завершает свою работу через задницу - не выполняя код методов Terminate(), а так же код зарегистрированных событий, таких например, как AppDomain.CurrentDomain.ProcessExit.

Однако обозначенная проблема гораздо глубже и не ограничивается рамками кода ваших расширений: при таком способе закрытия AutoCAD так же не выполняет и свой собственный код, который он обычно выполняет при завершении работы приложения (код корректного освобождения ресурсов, сохранения настроек и т.п.). Например, не происходит восстановление настроек в реестре, которые временно были изменены accoreconsole.exe под свои нужды. Это сразу бросается в глаза на напримере переменной FILEDIA, на время работы консольного приложения устанавливается в 0:  при очередном запуске acad.exe для неё приходится вручную восстанавливать значение 1 (в противном случае вместо диалоговых окон AutoCAD будет использовать свою консоль).

Если завершать работу accoreconsole.exe путём вызова команд quit и exit, то завершение работы приложения происходит так, как это должно было происходить (т.е. выполняется весь необходимый код). Однако никто не застрахован от клика мышкой по обозначенной выше кнопке, а пользователи с вероятностью 100% будут клацать как раз именно по ней, когда потребуется завершить работу приложения, потому как такой способ завершения работы - самый простой.

В качестве "лекарства" против обозначенной выше проблемы я блокирую кнопку закрытия консольного окна и соответствующий её пункт контекстного меню:


  1. const uint MF_BYCOMMAND = 0x00000000;
  2. const uint MF_GRAYED = 0x00000001;
  3. const uint SC_CLOSE = 0xF060;
  4. const uint MF_DISABLED = 0x00000002;
  5. [DllImport("kernel32.dll")]
  6. static extern IntPtr GetConsoleWindow();
  7. [DllImport("user32.dll")]
  8. static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
  9. [DllImport("User32.dll", SetLastError = true)]
  10. static extern uint EnableMenuItem(IntPtr hMenu, uint itemId, uint uEnable);
  11. [DllImport("user32.dll")]
  12. static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
  13. ...
  14. // Disable the Close ("X") button and "Close" context menu item of the Console window
  15. IntPtr hwnd = GetConsoleWindow();
  16. IntPtr hmenu = GetSystemMenu(hwnd, false);
  17. uint hWindow = EnableMenuItem(hmenu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
  18. // Also it is possible to delete "Close" context menu item
  19. // instead of disabling it.
  20. // DeleteMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);

Однако по факту я вижу, что кнопка закрытия окна заблокирована, а вот контекстное меню - нет... Конечно, можно попросту вовсе удалить этот пункт из контекстного меню и не заморачиваться на эту тему (в обозначенном выше примере кода это успешно делает последняя закомментированная строчка).

Однако мне всё же интересно: почему не блокируется пункт меню?

Оказалось, что обозначенная проблема свойственна не только accoreconsole.exe, но и любому консольному приложению в Windows 7 x64, а так же в Windows Server 2003. А вот в Windows 10 x64 всё работает корректно...

Т.о. то, что контекстное меню не блокируется в некоторых версиях Windows - очень похоже на баг WinAPI.

 В этой же теме сразу размещаю код примера того, как можно скрывать или отображать консольное окно (например всё тот же accoreconsole.exe):
  1. [DllImport("kernel32.dll")]
  2. static extern IntPtr GetConsoleWindow();
  3. [DllImport("user32.dll")]
  4. static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
  5. const int SW_HIDE = 0;
  6. const int SW_SHOW = 5;
  7. IntPtr hwnd = GetConsoleWindow();
  8. // Hide window
  9. ShowWindow(hwnd, SW_HIDE);
  10. // Show window
  11. ShowWindow(hwnd, SW_SHOW);

Длительная, нередко печальная практика показывает, что лозунг Autodesk касательно данного продукта, к сожалению, выглядит как-то так:
`accoreconsole.exe` - мы заставим Вас работать через задницу!

Блокировка кнопки и контекстного меню закрытия консольного окна

Как известно, accoreconsole.exe всегда был и до сих пор остаётся достаточно кривым... Один из неприятных аспектов его поведения, присутствующий по сей день, заключается в том, что если завершать работу приложения кликом мышки по кнопке закрытия консольного окна в верхнем правом углу, либо выбирая соответствующий пункт из контекстного меню консольного окна, то приложение завершает свою работу через задницу - не выполняя код методов Terminate(), а так же код зарегистрированных событий, таких например, как AppDomain.CurrentDomain.ProcessExit.

Однако обозначенная проблема гораздо глубже и не ограничивается рамками кода ваших расширений: при таком способе закрытия AutoCAD так же не выполняет и свой собственный код, который он обычно выполняет при завершении работы приложения (код корректного освобождения ресурсов, сохранения настроек и т.п.). Например, не происходит восстановление настроек в реестре, которые временно были изменены accoreconsole.exe под свои нужды. Это сразу бросается в глаза на напримере переменной FILEDIA, на время работы консольного приложения устанавливается в 0:  при очередном запуске acad.exe для неё приходится вручную восстанавливать значение 1 (в противном случае вместо диалоговых окон AutoCAD будет использовать свою консоль).

Если завершать работу accoreconsole.exe путём вызова команд quit и exit, то завершение работы приложения происходит так, как это должно было происходить (т.е. выполняется весь необходимый код). Однако никто не застрахован от клика мышкой по обозначенной выше кнопке, а пользователи с вероятностью 100% будут клацать как раз именно по ней, когда потребуется завершить работу приложения, потому как такой способ завершения работы - самый простой.

В качестве "лекарства" против обозначенной выше проблемы я блокирую кнопку закрытия консольного окна и соответствующий её пункт контекстного меню:


  1. const uint MF_BYCOMMAND = 0x00000000;
  2. const uint MF_GRAYED = 0x00000001;
  3. const uint SC_CLOSE = 0xF060;
  4. const uint MF_DISABLED = 0x00000002;
  5. [DllImport("kernel32.dll")]
  6. static extern IntPtr GetConsoleWindow();
  7. [DllImport("user32.dll")]
  8. static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
  9. [DllImport("User32.dll", SetLastError = true)]
  10. static extern uint EnableMenuItem(IntPtr hMenu, uint itemId, uint uEnable);
  11. [DllImport("user32.dll")]
  12. static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
  13. ...
  14. // Disable the Close ("X") button and "Close" context menu item of the Console window
  15. IntPtr hwnd = GetConsoleWindow();
  16. IntPtr hmenu = GetSystemMenu(hwnd, false);
  17. uint hWindow = EnableMenuItem(hmenu, SC_CLOSE, MF_BYCOMMAND | MF_DISABLED | MF_GRAYED);
  18. // Also it is possible to delete "Close" context menu item
  19. // instead of disabling it.
  20. // DeleteMenu(hmenu, SC_CLOSE, MF_BYCOMMAND);

Однако по факту я вижу, что кнопка закрытия окна заблокирована, а вот контекстное меню - нет... Конечно, можно попросту вовсе удалить этот пункт из контекстного меню и не заморачиваться на эту тему (в обозначенном выше примере кода это успешно делает последняя закомментированная строчка).

Однако мне всё же интересно: почему не блокируется пункт меню?

Оказалось, что обозначенная проблема свойственна не только accoreconsole.exe, но и любому консольному приложению в Windows 7 x64, а так же в Windows Server 2003. А вот в Windows 10 x64 всё работает корректно...

Т.о. то, что контекстное меню не блокируется в некоторых версиях Windows - очень похоже на баг WinAPI.

 В этой же теме сразу размещаю код примера того, как можно скрывать или отображать консольное окно (например всё тот же accoreconsole.exe):
  1. [DllImport("kernel32.dll")]
  2. static extern IntPtr GetConsoleWindow();
  3. [DllImport("user32.dll")]
  4. static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
  5. const int SW_HIDE = 0;
  6. const int SW_SHOW = 5;
  7. IntPtr hwnd = GetConsoleWindow();
  8. // Hide window
  9. ShowWindow(hwnd, SW_HIDE);
  10. // Show window
  11. ShowWindow(hwnd, SW_SHOW);

Длительная, нередко печальная практика показывает, что лозунг Autodesk касательно данного продукта, к сожалению, выглядит как-то так:
`accoreconsole.exe` - мы заставим Вас работать через задницу!

Открытие и закрытие консольного окна для GUI-приложения

Работая с GUI приложением иногда бывает удобно в режиме реального времени посмотреть, что оно отправляет себе на консоль (т.е. в потоки stdout и stderr), а порой может возникнуть и желание что-то отправить в поток stdin с клавиатуры. Можно, конечно же, выполнять перенаправление в файлы, но этот вариант не всегда удобен. В данной заметке, на примере AutoCAD, показано, как для GUI-приложения открыть консольное окно, выполнить перенаправление потоков и, после того как консольное окно не будет нужно, закрыть его.


Пример C++ кода, выполняющего открытие консольного окна и перенаправляющие потоки ввода-вывода:

// I open console window for AutoCAD GUI application
BOOL result = AllocConsole();

if (0 == result){
    DWORD errCode = GetLastError();
    LPTSTR msg = NULL;
    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
        | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);

    acutPrintf(_T("\nAllocConsole Error: %s"), msg);
    HeapFree(GetProcessHeap(), 0, msg);
}
else{
    acutPrintf(_T("\nAllocConsole: OK."));
    SetConsoleTitle(L"AutoCAD console window");

    // Disable the close button of the Console window
    HWND hwnd = GetConsoleWindow();
    HMENU hmenu = GetSystemMenu(hwnd, FALSE);
    EnableMenuItem(hmenu, SC_CLOSE, MF_GRAYED);

    // redirecting of the streams
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);
    freopen("CONIN$", "r", stdin);

    // You will see this messages in the console window
    wcout << L"wcout ping..." << endl;
    wcerr << L"wcerr ping..." << endl;
    cout << "cout ping..." << endl;
    cerr << "cerr ping..." << endl;

    // check wcin accessibility
    wcout << L"Press any char: ";
    wchar_t c;
    wcin >> c;
    wcout << L"You pressed the '" << c << "' char." << endl;
}


Результат выглядит следующим образом:

После того, как консольное окно станет ненужным, его можно закрыть. Однако его нельзя закрывать обычным кликом мышки по кнопке "X" в верхнем правом углу, иначе это приведёт к аварийному завершению работы GUI-приложения (в нашем примере - AutoCAD). Для того, чтобы пользователь случайно не нажал этой кнопки, в обозначенном выше коде мы делаем её недоступной.

Закрытие консольного окна выполняем так же программно:


BOOL result = FreeConsole();

if (0 == result){
    DWORD errCode = GetLastError();
    LPTSTR msg = NULL;
    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
        | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);

    acutPrintf(_T("\nFreeConsole Error: %s"), msg);
    HeapFree(GetProcessHeap(), 0, msg);
}
else{
    // redirecting of the streams
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);
    freopen("CONIN$", "r", stdin);
}





Пример создания именованных объектов ядра ОС в разных пространствах имён

Маленькая шпаргалка-пример на тему совместного использования именованных объектов ядра несколькими процессами. Показан вариант размещения именованных объектов ядра в глобальном, локальном и приватном пространствах имён.

Исходный код примера:

#include <Windows.h>
#include <iostream>
#include <aclapi.h>
#include <exception>
#include <tchar.h>

using namespace std;
void PrintErrorMsg(LPCTSTR prefix);

int main(int argc, TCHAR *argv[])
try {
    setlocale(LC_ALL, "Russian");

    HANDLE hGlobalMutex = CreateMutex(NULL, FALSE, L"Global\\G_Mutex");
    if (NULL == hGlobalMutex) {
        PrintErrorMsg(L"Global Mutex: ");
    }
    else {
        // Check the reason of success
        DWORD errCode = GetLastError();
        if (ERROR_ALREADY_EXISTS == errCode) {
            wcout << L"Named global mutex opened." << endl;
        }
        else {
            wcout << L"Named global mutex created." << endl;
        }
    }

    HANDLE hLocalMutex = CreateMutex(NULL, FALSE, L"Local\\L_Mutex");
    if (NULL == hLocalMutex) {
        PrintErrorMsg(L"Local Mutex: ");
    }
    else {
        // Check the reason of success
        DWORD errCode = GetLastError();
        if (ERROR_ALREADY_EXISTS == errCode) {
            wcout << L"Named local mutex opened." << endl;
        }
        else {
            wcout << L"Named local mutex created." << endl;
        }
    }

    LPCTSTR boundaryName = L"BushmanBoundary";
    HANDLE hBoundary = CreateBoundaryDescriptor(boundaryName, 0);
    HANDLE hNamespase = NULL;
    HANDLE hMutex = NULL;
    if (NULL == hBoundary) {
        PrintErrorMsg(L"CreateBoundaryDescriptor: ");
    }
    else {
        wcout << L"Boundary descriptor created." << endl;

        BYTE sid[SECURITY_MAX_SID_SIZE];
        ZeroMemory(sid, sizeof(sid));

        DWORD cbSID = sizeof(sid);
        BOOL result = CreateWellKnownSid(WinWorldSid, NULL, &sid, &cbSID);
        if (0 == result) {
            PrintErrorMsg(L"CreateWellKnownSid: ");
        }
        else {
            wcout << L"SID for boundary descriptor created." << endl;

            result = AddSIDToBoundaryDescriptor(&hBoundary, &sid);
            if (0 == result) {
                PrintErrorMsg(L"AddSIDToBoundaryDescriptor: ");
            }
            else {
                wcout << L"SID added to boundary descriptor." << endl;

                SECURITY_ATTRIBUTES sa = { 0 };
                sa.nLength = sizeof(sa);
                sa.bInheritHandle = FALSE;
                sa.lpSecurityDescriptor = NULL; // use the default value

                LPCTSTR ns = L"Local\\BushNamespace";

                hNamespase = CreatePrivateNamespace(&sa, hBoundary,
                    ns);
                if (NULL == hNamespase) {
                    // Check the reason of failure
                    DWORD errCode = GetLastError();
                    if (ERROR_ALREADY_EXISTS == errCode) {
                        hNamespase = OpenPrivateNamespace(hBoundary, ns);
                        if (NULL == hNamespase) {
                            PrintErrorMsg(L"OpenPrivateNamespace: ");
                        }
                        else {
                            wcout << L"Namespace opened." << endl;
                        }
                    }
                    else {
                        PrintErrorMsg(L"CreateNamespace: ");
                    }
                }
                else {
                    wcout << L"Namespace created." << endl;
                }
                if (NULL != hNamespase) {

                    hMutex = CreateMutex(NULL, FALSE,
                        L"Local\\BushNamespace\\BushMutex");

                    if (NULL == hMutex) {
                        PrintErrorMsg(L"CreateNamespaceMutex: ");
                    }
                    else {
                        // Check the reason of success
                        DWORD errCode = GetLastError();
                        if (ERROR_ALREADY_EXISTS == errCode) {
                            wcout << L"Named mutex in private namespace opened."
                                << endl;
                        }
                        else {
                            wcout << L"Named mutex in private namespace created."
                                << endl;
                        }
                    }
                }              
            }
        }
    }

    wcout << L"Press any char for exit..." << endl;
    wchar_t c;
    wcin >> c;

    if (NULL != hGlobalMutex) {
        CloseHandle(hGlobalMutex);
        hGlobalMutex = NULL;
    }

    if (NULL != hLocalMutex) {
        CloseHandle(hLocalMutex);
        hLocalMutex = NULL;
    }

    if (NULL == hMutex) {
        CloseHandle(hMutex);
        hMutex = NULL;
    }
    if (NULL != hNamespase) {
        ClosePrivateNamespace(hNamespase, 0);
        hNamespase = NULL;
    }
    if (NULL != hBoundary) {
        DeleteBoundaryDescriptor(hBoundary);
        hBoundary = NULL;
    }
}
catch (...) {
    wcerr << L"Unknown error." << endl;
    return 1;
}

void PrintErrorMsg(LPCTSTR prefix) {
    DWORD errCode = GetLastError();
    LPTSTR msg = NULL;
    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);
    wcerr << prefix << msg << endl;
    HeapFree(GetProcessHeap(), 0, msg);
}


На скрине результат консольного вывода двух разных процессов:

В Process Explorer видим следующую картину (см. выделенное красным):


Смотрим WinObj, запустив её с правами администратора:



Ещё немного по теме локализованных ресурсов

В дополнение к предыдущей заметке...


Нейтральная локаль, как и все остальные, определяется в ресурсах EXE или DLL и имеет код 0x000. Я думаю, что зачастую имеет смысл определять более обобщённые варианты ресурсов, дабы не дублировать их для каждой конкретной локали, например:
  • ru вместо ru-RU
  • en вместо en-US, en-GB и т.п.
Если специфичная локаль (en-US или en-GB) не будет найдена в ресурсах модуля, то будет выполнена дополнительная попытка найти более обобщённый вариант локали: en. Однако обратное не верно: если задан поиск локали en, а в ресурсах вместо неё присутствуют более конкретные локали (en-US или en-GB), то они будут проигнорированы.

В качестве примера в файле resources.txt определим текстовые ресурсы с таким набором локализаций:
  • нейтральная
  • ru
  • en-US

; // ***** resources.txt *****
; // This is the header section.

MessageIdTypedef=DWORD

SeverityNames=(Success=0x0:STATUS_SEVERITY_SUCCESS
Informational=0x1:STATUS_SEVERITY_INFORMATIONAL
Warning=0x2:STATUS_SEVERITY_WARNING
Error=0x3:STATUS_SEVERITY_ERROR
)

FacilityNames=(System=0x0:FACILITY_SYSTEM
Runtime=0x2:FACILITY_RUNTIME
Stubs=0x3:FACILITY_STUBS
Io=0x4:FACILITY_IO_ERROR_CODE
)

; // The neutral locale.
LanguageNames=(Neutral=0x000:MSG00000)

; // This locale will be used for any RU-based locale.
LanguageNames=(ru=0x019:MSG00019)

; // This locale will be used only for en-US locale, instead
; // of any EN-based locale.
LanguageNames=(en_US=0x409:MSG00409)

; // The following are message definitions.

MessageId=0x1
Severity=Success
Facility=System
SymbolicName=MSG_HELLO
Language=en_US
USA locale only (en-US).
.

Language=ru
Любая RU-основанная локаль (RU).
.

Language=Neutral
Neutral locale.
.



Далее разными способами программно пытаемся получить локализованный вариант нашего текста:


#include <Windows.h>
#include <tchar.h>
#include <iostream>
#include <exception>
#include "resources.h"
using namespace std;
/*
MSDN resources:
FormatMessage function:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms679351%28v=vs.85%29.aspx
Message Text Files:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996906%28v=vs.85%29.aspx
Sample Message Text File:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996907%28v=vs.85%29.aspx
Message Compiler (MC.exe):
https://msdn.microsoft.com/en-us/library/windows/desktop/aa385638%28v=vs.85%29.aspx
*/
// Our function uses the WinAPI mechanism for notifying of error
void PrintMessage(DWORD);

int wmain(int argc, LPTSTR argv[])
try {
    // For right displaying of Cyrillic chars in the console window
    setlocale(LC_ALL, "Russian");

    wcout << L"For neutral locale:" << endl;
    DWORD nl = MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL);
    PrintMessage(nl);

    TCHAR x[LOCALE_NAME_MAX_LENGTH];
   
    DWORD du = GetUserDefaultLCID();   
    int n = GetUserDefaultLocaleName(x, LOCALE_NAME_MAX_LENGTH);
    if (0 != n) {
        wcout << L"For user default locale <" << x << L">:" << endl;
        PrintMessage(du);
    }   
       
    DWORD ds = GetSystemDefaultLCID();
    n = GetSystemDefaultLocaleName(x, LOCALE_NAME_MAX_LENGTH);
    if (0 != n) {
        wcout << L"For system default locale <" << x << L">:" << endl;
        PrintMessage(ds);
    }

    // Any RU-based locale (defined in the resources)
    wcout << L"For RU locale:" << endl;
    DWORD ru = MAKELANGID(LANG_RUSSIAN, SUBLANG_NEUTRAL);
    PrintMessage(ru);

    // Specific locale (NOT defined in the resources)
    // At this case will be used more common locale: RU
    wcout << L"For ru-RU locale:" << endl;
    DWORD ru_ru = MAKELANGID(LANG_RUSSIAN, SUBLANG_RUSSIAN_RUSSIA);
    PrintMessage(ru_ru);

    // Any EN-based locale (defined in the resources)
    wcout << L"For EN locale:" << endl;
    DWORD en = MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL);
    PrintMessage(en);

    // Specific locale (defined in the resources)
    wcout << L"For en-US locale:" << endl;
    DWORD en_us = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
    PrintMessage(en_us);

    wcout << L"Press any char for exit..." << endl;
    wchar_t c;
    wcin >> c;
}
catch (...) {
    wcerr << L"Unknown exception." << endl;
    return 1;
}

void PrintMessage(DWORD localeId) {
    DWORD msgId = MSG_HELLO;
    LPTSTR buffer = NULL;
    HMODULE hModule = GetModuleHandle(NULL);

    DWORD result = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
        hModule, msgId, localeId, (LPTSTR)&buffer, 0, NULL);

    if (0 == result) {
        msgId = GetLastError();
        FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
            FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
            NULL, msgId, 0, (LPTSTR)&buffer, 0, NULL);
    }
    wcout << buffer << endl;
    HeapFree(GetProcessHeap(), 0, buffer);
}


Результат работы кода выглядит следующим образом:



Об использовании функции FormatMessage из WinAPI для расшифровки своих кодов ошибок

В WinAPI многие функции, в случае неудачного завершения своей работы, возвращают код ошибки, получить который можно при помощи функции GetLastError(). Само по себе полученное числовое значение не даёт чёткого представления о причине сбоя. Для того, чтобы понять, что же именно произошло, необходимо получить строковое сообщение, соответствующее возвращённому коду ошибки. В WinAPI существует функция FormatMessage которая, помимо заложенных в ней возможностей касающихся форматирования строк, может использоваться для получения текстового описания ошибки по её коду. Однако, использовать обозначенный механизм получения описаний ошибок можно не только для кодов системных ошибок, но так же и для кодов ошибок, определяемых вами...


Обозначенный механизм получения строкового сообщения об ошибках можно использовать в любых приложениях или программных библиотеках. Каждый EXE и DLL может содержать свой собственный набор ресурсов, определяющий локализованные варианты сообщений об ошибках. В своём программном коде можно использовать системные коды ошибок, уже определённые в составе WinAPI, в дополнение к своим кодам (дабы повторно не определять то, что уже и так имеется в системе). Для этого при вызове функции FormatMessage используется комбинация флагов FORMAT_MESSAGE_FROM_SYSTEM и FORMAT_MESSAGE_FROM_HMODULE (см. ниже комментарии в коде). Эта комбинация сообщает, что если информация об указанном коде ошибки не будет найдена в нашем модуле (DLL или EXE), то следует выполнить повторный поиск, но уже в системных ресурсах операционной системы. Т.о. в своих ресурсах можно ограничиться определением лишь того, что отсутствует в системных ресурсах. Коды системных ошибок определены в заголовке WinError.h.

Внимание!
Обозначенная выше комбинация флагов требует, чтобы в вашем модуле присутствовал ресурс с текстовыми сообщениями об ошибках. В случае его отсутствия функция FormatMessage сгенерирует ошибку с сообщением о том, что в указанном модуле отсутствуют ресурсы. Если вы используете только системные коды ошибок, то указывать флаг FORMAT_MESSAGE_FROM_HMODULE не нужно.

Формат описания текстовых файлов, содержащих локализованные сообщения об ошибках опубликован в MSDN здесь. Пример такого файла так же присутствует в MSDN. На основе сформированного текстового файла, при помощи утилиты MC.EXE генерируется набор файлов, необходимых для встраивания (в наш EXE или DLL файл) ресурсов, содержащих локализованные сообщения об ошибках.

Пример
Предположим, что в своих функциях мы хотим использовать обозначенный механизм для сообщения об ошибках. При этом хотим использовать преимущественно системные коды ошибок, но для ряда случаем определить и свои, т.к. в системных не нашли подходящего сообщения. Кроме того, мы хотим реализовать две локализации этих ресурсов: английскую и русскую.

Примечание
Порядок поиска локализованных ресурсов достаточно ясно изложен в описании параметра dwLanguageId функции FormatMessage.

В каталоге проекта создаём новый текстовый файл в кодировке ANSI или Unicode (т.е. UTF-16, не путать с UTF-8). Если используется Notepad++, то кодировка Unicode там обозначена как UCS-2 LE BOM.

Заполняем файл содержимым, например:

; // ***** Sample.mc *****
; // This is the header section.

MessageIdTypedef=DWORD

SeverityNames=(Success=0x0:STATUS_SEVERITY_SUCCESS
Informational=0x1:STATUS_SEVERITY_INFORMATIONAL
Warning=0x2:STATUS_SEVERITY_WARNING
Error=0x3:STATUS_SEVERITY_ERROR
)

FacilityNames=(System=0x0:FACILITY_SYSTEM
Runtime=0x2:FACILITY_RUNTIME
Stubs=0x3:FACILITY_STUBS
Io=0x4:FACILITY_IO_ERROR_CODE
)

LanguageNames=(English=0x409:MSG00409)
LanguageNames=(Russian=0x419:MSG00419)

; // The following are message definitions.

MessageId=0x1
Severity=Error
Facility=Runtime
SymbolicName=MSG_BAD_COMMAND
Language=English
You have chosen an incorrect command.
.

Language=Russian
Вы выбрали неправильную команду.
.

MessageId=0x2
Severity=Warning
Facility=Io
SymbolicName=MSG_BAD_PARM1
Language=English
Cannot reconnect to the server.
.

Language=Russian
Не удаётся подключиться к серверу.
.

MessageId=0x3
Severity=Success
Facility=System
SymbolicName=MSG_STRIKE_ANY_KEY
Language=English
Press any key to continue . . . %0
.

Language=Russian
Нажмите любую клавишу для продолжения . . . %0
.

Внимание!
Последней строкой текстового файла, содержащего локализованные сообщения об ошибках, обязательно должна быть пустая строка, иначе MC.EXE будет ругаться на инвалидный контент.

В меню Все Программы -> Visual Studio 2013 -> Visual Studio Tools открываем консоль Visual Studio: VS2013 x64 Cross Tools Command Prompt. Переходим в каталог проекта и запускаем программу MC.EXE с соответствующим набором опций:

  • Если содержимое текстового файла использует кодировку ANSI:
    mc.exe -a sample.mc
  • Если содержимое текстового файла использует кодировку UTF-16:
    mc.exe -u sample.mc
В результате работы обозначенной выше команды, в каталоге проекта появятся новые файлы:
  • MSG00409.bin
  • MSG00419.bin
  • Sample.h
  • Sample.rc
Добавляем в наш проект заголовочный файл Sample.h и ресурсный файл Sample.rc. Сгенерированный утилитой MC.EXE заголовочный файл выглядит следующим образом:

 // ***** Sample.mc *****
 // This is the header section.
 // The following are message definitions.
//
//  Values are 32 bit values laid out as follows:
//
//   3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
//   1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
//  +---+-+-+-----------------------+-------------------------------+
//  |Sev|C|R|     Facility          |               Code            |
//  +---+-+-+-----------------------+-------------------------------+
//
//  where
//
//      Sev - is the severity code
//
//          00 - Success
//          01 - Informational
//          10 - Warning
//          11 - Error
//
//      C - is the Customer code flag
//
//      R - is a reserved bit
//
//      Facility - is the facility code
//
//      Code - is the facility's status code
//
//
// Define the facility codes
//
#define FACILITY_SYSTEM                  0x0
#define FACILITY_STUBS                   0x3
#define FACILITY_RUNTIME                 0x2
#define FACILITY_IO_ERROR_CODE           0x4


//
// Define the severity codes
//
#define STATUS_SEVERITY_WARNING          0x2
#define STATUS_SEVERITY_SUCCESS          0x0
#define STATUS_SEVERITY_INFORMATIONAL    0x1
#define STATUS_SEVERITY_ERROR            0x3


//
// MessageId: MSG_BAD_COMMAND
//
// MessageText:
//
// You have chosen an incorrect command.
//
#define MSG_BAD_COMMAND                  ((DWORD)0xC0020001L)

//
// MessageId: MSG_BAD_PARM1
//
// MessageText:
//
// Cannot reconnect to the server.
//
#define MSG_BAD_PARM1                    ((DWORD)0x80040002L)

//
// MessageId: MSG_STRIKE_ANY_KEY
//
// MessageText:
//
// Press any key to continue . . . %0
//
#define MSG_STRIKE_ANY_KEY               ((DWORD)0x00000003L)


Подключаем заголовочный файл Sample.h в коде исходников и используем его. В обозначенном ниже примере мы определяем функцию isValidCommandIndex, использующую "родной" механизм WinAPI для оповещения о возникновении ошибок:

#include<Windows.h>
#include <iostream>
#include "Sample.h"
using namespace std;
/*
MSDN resources:

FormatMessage function:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms679351%28v=vs.85%29.aspx

Message Text Files:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996906%28v=vs.85%29.aspx

Sample Message Text File:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996907%28v=vs.85%29.aspx

Message Compiler (MC.exe):
https://msdn.microsoft.com/en-us/library/windows/desktop/aa385638%28v=vs.85%29.aspx

*/

// Our function uses the WinAPI mechanism for notifying of error
BOOL isValidCommandIndex(int index){
  if (index < 0){
    SetLastError(MSG_BAD_COMMAND);  // the identifier from the Sample.h
    return FALSE;
  }
  else{
    SetLastError(ERROR_SUCCESS);
    return TRUE;
  }
}

int wmain(int argc, TCHAR* argv[])
{
  // Getting the readable Cyrillic chars in the console window for ANSI and 
  // Unicode encodings...
  setlocale(LC_ALL, "Russian");
  
  int command_index = -1;
  BOOL result = isValidCommandIndex(command_index);
  if (!result){    
    DWORD errCode = GetLastError(); // Get the last error at once

    LCID langId = MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL);
    // Also you can try such variants, for getting the messages with other 
    // localizations:
    // LCID langId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
    // LCID langId = MAKELANGID(LANG_RUSSIAN, SUBLANG_RUSSIAN_RUSSIA);

    PTCHAR message = NULL;

    // HANDLE of EXE or DLL which contains the resource with the error messages
    // which we are to get. In our case this is current EXE, therefore it is
    // possible to point the NULL value of HANDLE for the FormatMessage 
    // function.
    HANDLE handle = NULL;

    int tchars_count = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
      FORMAT_MESSAGE_FROM_SYSTEM | /* Search in the system resources if the
                                   message will not be found in our module. It
                                   allows to use the system error codes in our
                                   code additionally to defined by us error
                                   codes. */
     FORMAT_MESSAGE_FROM_HMODULE |
     FORMAT_MESSAGE_IGNORE_INSERTS, handle, errCode, langId, (PTCHAR)&message,
     0, NULL);
    if (0 == tchars_count){
      // In the Watch[1-4] window it is possible to get the last error code 
      // through the '$err,hr' (without quotes) name. Add this name into the 
      // Watch[1-4] window if you need.
      DWORD lastErrorCode = GetLastError();
      if (lastErrorCode != ERROR_SUCCESS){
        FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
          FORMAT_MESSAGE_FROM_SYSTEM |
          FORMAT_MESSAGE_IGNORE_INSERTS, NULL, lastErrorCode, langId,
          (PTCHAR)&message, 0, NULL);
      }
    }
    wcout << message;
    LocalFree(message);
  }  
  // *******************************************
  wchar_t c;
  wcout << L"Нажмите любую клавишу для выхода..." << endl;
  wcin >> c;
  return 0;
}

Компилируем наш код и запускаем его. В консоли получаем следующий вывод:

Вы выбрали неправильную команду.
Нажмите любую клавишу для выхода...

Если указать английскую локализацию (см. комментарии в коде), то сообщение об ошибке будет на английском:

You have chosen an incorrect command.
Нажмите любую клавишу для выхода...

Как видим, текст сообщения об ошибке соответствует тому, который мы определили в составе ресурсов нашего EXE файла.

Где это может пригодиться?
Например, если созданные вами функции могут использоваться сторонними разработчиками, имеющими опыт работы сWinAPI, то задействование стандартного механизма оповещения об ошибках, предоставляемого операционной системой Windows, для них будет делом привычным. Вам достаточно будет лишь сообщить в документации о том, что для получения кода ошибки и его расшифровки следует использовать стандартный механизм WinAPI: функции GetLastError и FormatMessage.