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

Создан cribs.red-bee.ru

Закинул на red-bee.ru несколько страниц с заметками, которые могут оказаться интересными при разработке различного рода административных утилит. Опубликованы здесь. Материал не претендует на абсолютную истину (не исключено, что какие-то задачи можно было решить и более простым путём).

Быстрый способ проверить наличие запущенного процесса acad.exe или accoreconsole.exe

То, что предоставлено в данной заметке не является документированным способом и получено на основе анализирования состава мьютексов, создаваемых и используемых AutoCAD в процессе своей работы. Способ применим как к acad.exe, так и к accoreconsole.exe. Проверялся с AutoCAD 2009-2016 x64, а так же с AutoCAD 2008 x86, запущенном в Windows x64. Решение продемонстрировано в двух вариантах: C++ и C#.

В .NET получить процессы по имени можно при помощи статического метода Process.GetProcessesByName("acad"). Однако, если переименовать любой EXE файл в acad.exe, то  будучи запущенным, он тоже попадёт в выборку, возврашаемую этим методом, хотя на самом деле это не процесс AutoCAD.

Конечно же, приведённый в этой заметке код не является "серебрянной пулей", т.к. всегда можно программно создать Mutex с указанным в нашем коде именем... Однако, обозначенный мною вариант проверки не удастся обмануть простым переименованием любого EXE файла в acad.exe или accoreconsole.exe, поэтому он, как мне кажется, имеет право на жизнь...

Как это сделать на C++:


/*  © Andrey Bushman, 2015
    http://bushman-andrey.blogspot.ru/2015/11/acadexe-accoreconsoleexe.html


    This is the quick way of checking is any AutoCAD launched (acad.exe
    or accoreconsole.exe) or not. I've checked my code for the usual
    AutoCAD 2009-2016 x64. But I haven't their x86 versions, I haven't
    older AutoCAD versions, and I haven't their vertical products,
    therefore I can't check my code for these versions.

 
    Additionally, Alexander Rivilis checked this code for AutoCAD 2008 x86
    which was launched in Windows x64.


    NOTE
    Visual C++ Redistributable for Visual Studio (your version) must be
    installed on the target computers.
*/

#include<Windows.h>
#include<iostream>
#include<exception>
#include<string>
#include<tchar.h>
#include<strsafe.h>

using namespace std;

#define UNEXPECTED_EXCEPTION 1
#define UNKNOWN_ERROR 2

BOOL IsLaunchedAnyAutoCAD();

int wmain(int argc, wchar_t *argv[])
try {
    // setlocale(LC_ALL, "Russian");

    SetConsoleTitle(TEXT("Is any AutoCAD launched?"));

    BOOL result = IsLaunchedAnyAutoCAD();

    if (result) {
        wcout << L"Any AutoCAD is launched." << endl;
    }
    else {
        wcout << L"Any AutoCAD is not launched." << endl;
    }   

    wcout << L"Press 'x' for exit..." << endl;
    wchar_t c;
    wcin >> c;
}
catch (exception ex) {
    wcerr << ex.what() << endl;
    return UNEXPECTED_EXCEPTION;
}
catch(...){
    wcerr << L"Unknown error." << endl;
    return UNKNOWN_ERROR;
}

BOOL IsLaunchedAnyAutoCAD() {
    BOOL result = FALSE;
    LPCTSTR anyAcadMutexName = TEXT(
        // This works for AutoCAD 2008 and newer (I haven't older
        // AutoCAD versions, therefore I can't check it for them).
        "Global\\8C84DAD6-9865-400e-A6E3-686A61C16968"

        // This is for AutoCAD 2009 and newer
        // "Local\\AcadProfileStorage_54519085-6DDA-4070-BB93-3A095D7E1140"
        );
    HANDLE hAnyAcadMutex = OpenMutex(READ_CONTROL, FALSE, anyAcadMutexName);
    if (NULL != hAnyAcadMutex) {
        result = TRUE;
        CloseHandle(hAnyAcadMutex);
    }
    return (result);
}



Как это сделать на C#:


/*  © Andrey Bushman, 2015
    http://bushman-andrey.blogspot.ru/2015/11/acadexe-accoreconsoleexe.html

    This is the quick way of checking is any AutoCAD launched (acad.exe
    or accoreconsole.exe) or not. I've checked my code for the usual
    AutoCAD 2009-2016 x64. But I haven't their x86 versions, I haven't
    older AutoCAD versions, and I haven't their vertical products,
    therefore I can't check my code for these versions.


    Additionally, Alexander Rivilis checked this code for AutoCAD 2008 x86
    which was launched in Windows x64.
*/
using System;
using System.Threading;

namespace Bushman.Sandbox.AutoCAD
{
    class Program
    {
        static Boolean IsAnyAutoCadLaunched()
        {
            try
            {
                Mutex m = Mutex.OpenExisting(
                    // This works for AutoCAD 2008 and newer (I haven't older
                    // AutoCAD versions, therefore I can't check it for them).
                    "Global\\8C84DAD6-9865-400e-A6E3-686A61C16968"

                    // This is for AutoCAD 2009 and newer
                    // "Local\\AcadProfileStorage_54519085-6DDA-4070-BB93-3A095D7E1140"
                    );
                m.Close();
                return true;
            }
            catch
            {
                return false;
            }
        }


        static void Main(string[] args)
        {
            String msg = IsAnyAutoCadLaunched() ? "Any AutoCAD is launched." :
                "Any AutoCAD is not launched.";

            Console.WriteLine(msg);

            Console.WriteLine("Press any key for exit...");
            Console.ReadKey();
        }
    }
}




Открытие и закрытие консольного окна для 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.

Подписаться на изменение DependecyProperty

Часто бывает, что есть некий потомок DependencyObject, у него есть DependencyProperty, а вот события сообщающего о том, что свойство изменилось нет. Я с такой ситуацией столкнулся при попытках отследить изменение актуальной ширины DataGridColumn. Ну и под катом, как это все можно провернуть.

Для примера, я воспользуюсь вот таким классом:
class MyClass : DependencyObject
{
    public int MyProperty
    {
        get { return (int)GetValue(MyPropertyProperty); }
        set { SetValue(MyPropertyProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty MyPropertyProperty =
        DependencyProperty.Register("MyProperty"typeof(int), typeof(MyClass), new PropertyMetadata(0));
}
Ну и как мы можем узнать об изменении этого свойства? До очень просто, вот так:
static void Main(string[] args)
{
    MyClass m = new MyClass();
    m.MyProperty = 10; // До подписывания
    PropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(MyClass.MyPropertyProperty, typeof(MyClass));
    descriptor.AddValueChanged(m, new EventHandler((sender, e) => Console.WriteLine(((MyClass)sender).MyProperty)));
    m.MyProperty = 20;
    Console.ReadKey();
}
Ну и вот так это выглядит:
Первое изменение свойства прошло тихонечко, а на второе уже сработал вывод. 

Вставка HTML в буфер обмена

При работе с буфером обмена из C# выяснилось, что вставить туда HTML так, чтобы программы типа Word распознавали его не как текст, а именно как HTML не совсем просто, под катом пример.


Собственно, для вставки HTML в буфер, его нужно специальным образом подготовить. Вот таким методом:
public static string PrepareHtmlToClippboard(stringhtml)
{
    Encoding enc = Encoding.UTF8;

    string begin = "Version:0.9rnStartHTML:{0:000000}rnEndHTML:{1:000000}"
        + "rnStartFragment:{2:000000}rnEndFragment:{3:000000}rn";

    string html_begin = "rnrn"
        + "
        + " content="text/html; charset=" + enc.WebName + "">rn"
        + "HTML clipboardrn
rnrn"
        + "";

    string html_end = "rn
rnrn";

    string begin_sample = String.Format(begin, 0, 0, 0, 0);

    intcount_begin = enc.GetByteCount(begin_sample);
    intcount_html_begin = enc.GetByteCount(html_begin);
    intcount_html = enc.GetByteCount(html);
    intcount_html_end = enc.GetByteCount(html_end);

    string html_total = String.Format(
        begin
        , count_begin
        , count_begin + count_html_begin + count_html + count_html_end
        , count_begin + count_html_begin
        , count_begin + count_html_begin + count_html
        ) + html_begin + html + html_end;

    return html_total;

}
Ну и вот так это можно использовать:
DataObject dataObj = new DataObject();
dataObj.SetData(DataFormats.Html, PrepareHtmlToClippboard("здесь HTML"));
dataObj.SetData(DataFormats.Text, "здесь текст, если приемник не понимает html");

Clipboard.SetDataObject(dataObj, true);
Собственно все.

Пишем монитор pulseaudio для lxpanel

С момента написания статьи об xmonad кое-что поменялось и я перешёл на использование openbox. Поменялись и панели, теперь я использую lxpanel, который разрабатывается для lxde, легковесного рабочего стола, основанного на openbox. Отображаемые lxpanel элементы являются плагинами, динамически загружаемыми библиотеками. Сегодня мы напишем свой плагин для lxpanel.
Так как в стандартной поставке lxpanel виджет для регулировки громкости не очень хорошо (скорее очень нехорошо) отображает текущий уровень громкости, то я решил написать свой плагин, который исправит ситуацию.
Эта статья будет состоять из двух частей. Первая будет посвящена работе с pulseaudio (читается [pʌlsˈɔːdɪəu] пaлсодыо), а вторая -- lxpanel.


На скриншоте выше результат можно разглядеть слева.
Писать мы будем на C. Да, именно на C, так как заголовочный файл lxpanel является ярким представителем того, как можно писать на C несовместимый с C++ код.

Внимание, весь код, приведённый в этой статье распространяется по лицензии GNU GPL версии 3 и выше!

Монитор Pulseaudio

Начиная работать с pulseaudio натыкаешься на, мягко говоря, скудную документацию, основу которой составляет сгенерированная doxygen`ом. Из документации мы узнаём, что у pulseaudio есть два типа API: simple и asynchronous. Беглый взгляд на simple API позволяет понять, что придётся использовать асинхронный API.
Основным элементом для работы с асинхронным API pulseaudio является контекст, представленный типом pa_context. Контекст представляет собой мост между приложением и демоном pulseaudio. Чтобы контекст работал, ему нужен mainloop. Pulseaudio представляет возможность использовать три варианта реализации основного цикла:
Из перечисленных вариантов нам больше всего подходит последний, так как lxpanel написана на GTK+ 2 и уже имеет запущенный GMainLoop. Остальные варианты приведут либо к зависанию lxpanel, либо к сложной и запутанной архитектуре.
Так как на машине может быть несколько звуковых устройств, то их надо как-то различать. Pulseaudio использует для этого понятие sink (не знаю, как правильно перевести). Sink`и имеют свой идентификатор или индекс, который является порядковым номером, начиная с нуля.
Итак, на основе полученной информации мы уже можем накидать интерфейс, который мы будем использовать для получения информации от pulseaudio.
#include <stdint.h>

#define PAMON_UNUSED(var) (void)var

typedef struct Pamon Pamon;

typedef enum
{
PAMON_ERROR_CONNECTION_FAILED,
PAMON_ERROR_INVALID_SINK
} PamonError;

typedef void (* PamonVolumeChangedCallback)(uint32_t current_volume_percents, void * userdata);
typedef void (* PamonErrorCallback)(PamonError error, void * userdata);

typedef struct
{
uint32_t sink_id;
PamonVolumeChangedCallback volume_callback;
PamonErrorCallback error_callback;
void * userdata;
} PamonInitData;

Pamon * pamonInit(PamonInitData * init_data);

void pamonSetSinkId(Pamon * pamon, uint32_t id);

int pamonStart(Pamon * pamon);

void pamonClose(Pamon * pamon);

const char * pamonTranslateError(PamonError error);
Немного поясню. Тип PamonError используется для сообщения об ошибке. Значение PAMON_ERROR_CONNECTION_FAILED говорит о том, что соединиться с демоном pulseaudio не удалось, а PAMON_ERROR_INVALID_SINK сообщает о неверном идентификаторе sink`а. Функция pamonTranslateError переводит идентификатор ошибки в понятный человеку вид, в строку.
Фунции pamonInit и pamonClose соответственно инициализируют и закрывают соединение с демоном pulseaudio. Для инициализации соединения в функцию pamonInit передаётся структура PamonInitData содержащая идентификатор sink`а и колбеки для уведомления о смене громкости и об ошибке. Кроме того структура  PamonInitData содержит поле userdata, которое будет передано в колбеки.
Назначение функции pamonSetSinkId должно быть ясно без пояснений.
Используя этот интерфейс можно написать консольное приложение, которое будет выводить текущий уровень громкости.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <glib.h>
#include "pamon.h"

#define APP_NAME "pamon"
#define DEFAULT_FORMAT "%i%%"

typedef struct
{
char * format;
uint32_t sink;
int print_help;
} Options;

static void pamonVolumeChanged(uint32_t current_volume, void * userdata);
static void pamonError(PamonError error, void * userdata);
static void onSignal(int signum);
static void parseOpts(int argc, char ** argv, Options * result);
static void printUsage();

static Pamon * pamon = NULL;
static GMainLoop * main_loop = NULL;
static char * format = NULL;


int main(int argc, char ** argv)
{
struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = onSignal;
sigaction(SIGINT, &action, NULL);
PamonInitData pamon_init_data;
memset(&pamon_init_data, 0, sizeof(PamonInitData));
{
Options opts;
memset(&opts, 0, sizeof(Options));
parseOpts(argc, argv, &opts);
if(opts.print_help)
{
printUsage();
free(opts.format);
return EXIT_SUCCESS;
}
pamon_init_data.sink_id = opts.sink;
format = opts.format;
}
pamon_init_data.volume_callback = pamonVolumeChanged;
pamon_init_data.error_callback = pamonError;
main_loop = g_main_loop_new(NULL, FALSE);
pamon = pamonInit(&pamon_init_data);
g_main_loop_run(main_loop);
return EXIT_SUCCESS;
}

void onSignal(int signum)
{
pamonClose(pamon);
g_main_loop_quit(main_loop);
g_main_loop_unref(main_loop);
free(format);
printf("\nquit\n");
exit(EXIT_SUCCESS);
}

void pamonVolumeChanged(uint32_t current_volume, void * userdata)
{
PAMON_UNUSED(userdata);
printf(format ? format : DEFAULT_FORMAT, current_volume);
printf("\n");
fflush(stdout);
}

void pamonError(PamonError error, void * userdata)
{
PAMON_UNUSED(userdata);
fprintf(stderr, "Error: %s\n", pamonTranslateError(error));
}

void parseOpts(int argc, char ** argv, Options * result)
{
for(;;)
{
switch(getopt(argc, argv, "f:s:"))
{
case -1:
return;
case '?':
result->print_help = 1;
break;
case 'f':
result->format = (char *)malloc(strlen(optarg) + 1);
strcpy(result->format, optarg);
break;
case 's':
sscanf(optarg, "%i", &result->sink);
break;
default:
break;
}
}
}

void printUsage()
{
printf(
"Usage: %s [-s sink] [-f format]\n"
" sink - pulseaudio sink id\n"
" format - print format. Use %%i to print current volume value. Use %%%% to print the %% mark\n"
"\n"
"Use Ctrl+C to exit program\n",
APP_NAME);
fflush(stdout);
}
Здесь мы получаем из опций приложения идентификатор sink`а и формат сообщения. Создаём основной цикл GLib и запускам pamon, передав функции pamonVolumeChanged и pamonError в качестве колбеков. Программа завершается по нажатию Ctrl+C. Обработчик сигнала SIGINT позволит освободить занятые ресурсы перед выходом, хотя это и не обязательно.

Перейдём к самому интересному, к реализации интерфейса взаимодействия с pulseaudio.
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <pulse/pulseaudio.h>
#include <pulse/glib-mainloop.h>
#include "pamon.h"

struct Pamon
{
pa_context * pulse_context;
pa_glib_mainloop * pulse_glib_mainloop;
PamonVolumeChangedCallback volume_callback;
PamonErrorCallback error_callback;
int sink_id;
void * userdata;
};

Pamon * pamonInit(PamonInitData * init_data)
{
Pamon * pamon = (Pamon *)malloc(sizeof(Pamon));
memset(pamon, 0, sizeof(Pamon));
if(init_data)
{
pamon->userdata = init_data->userdata;
pamon->volume_callback = init_data->volume_callback;
pamon->error_callback = init_data->error_callback;
pamon->sink_id = init_data->sink_id;
}
pamon->pulse_glib_mainloop = pa_glib_mainloop_new(NULL);
pa_mainloop_api * api = pa_glib_mainloop_get_api(pamon->pulse_glib_mainloop);
pamon->pulse_context = pa_context_new(api, NULL);
pa_context_set_state_callback(pamon->pulse_context,
(pa_context_notify_cb_t)pulseContextStateCallback, pamon);
pa_context_connect(pamon->pulse_context, NULL, 0, NULL);
return pamon;
}
Функция инициализации создаёт и заполняет экземпляр структкры Pamon. Затем создаётся основной цикл из дефолтного контекста GLib и, после получения API основного цикла, инициализируется контекст. pa_mainloop_api служит абстрактным интерфейсом для работы со всеми тремя типами основных циклов. Так как мы используем асинхронные API и соединение с демоном pulseaudio происходит в отдельном потоке, функция pa_context_connect может сообщить о своём результате только в функцию обратного вызова, которую мы и устанавливаем с помощью pa_context_set_state_callback. При установке колбеков мы можем передавать пользовательские данные, которые будут отданы колбеку. Здесь и далее мы будем использовать указатель на структуру Pamon, хранящую всю служебную информацию.
void pulseContextStateCallback(pa_context * context, Pamon * pamon)
{
switch(pa_context_get_state(context))
{
case PA_CONTEXT_READY:
pa_context_set_subscribe_callback(context, (pa_context_subscribe_cb_t)pulseContextCallback, pamon);
pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK, NULL, NULL);
performSinkHandler(context, pamon);
break;
case PA_CONTEXT_FAILED:
performError(pamon, PAMON_ERROR_CONNECTION_FAILED);
break;
default:
break;
}
}
Здесь мы подписываемся на события контекста с помощью функции pa_context_subscribe, устнавив колбек вызовом pa_context_set_subscribe_callback. Так как нам интересны только события от sink`ов, то передаём маску PA_SUBSCRIPTION_MASK_SINK для фильтрации того, что нам будет приходить в колбек.
Если произошла ошибка, о ней сообщаем вызовом performError, который, по сути, зовёт колбек, переданный при инициализации.
void performError(Pamon * pamon, PamonError error)
{
if(pamon->error_callback)
pamon->error_callback(error, pamon->userdata);
}
Кроме подписки на события, мы форсируем получение значения громкости для изначальной инициализации подписчика.
void performSinkHandler(pa_context * context, Pamon * pamon)
{
pa_operation * operation = pa_context_get_sink_info_by_index(context, pamon->sink_id,
(pa_sink_info_cb_t)pulseSinkInfoCallback, pamon);
pa_operation_unref(operation);
}
Как видно, функция performSinkHandler выполняет только одно действие -- просит информацию о sink`е по его идентификатору вызовом функции pa_context_get_sink_info_by_index. Так как информация о ходе выполнения асинхронной операции нам не интересна, то сразу же отпускаем ссылку на экземпляр pa_operation вызвав pa_operation_unref. Итак, информация о sink`е приходит в функцию pulseSinkInfoCallback.
void pulseSinkInfoCallback(pa_context * context, const pa_sink_info * info, int eol, Pamon * pamon)
{
if(eol == -1)
{
performError(pamon, PAMON_ERROR_INVALID_SINK);
}
if(info && pamon && pamon->volume_callback)
{
uint32_t volume = info->mute || !info->volume.channels ? 0 : info->volume.values[0];
uint32_t percents = (uint32_t)round((double)(volume * 100) / PA_VOLUME_NORM);
pamon->volume_callback(percents, pamon->userdata);
}
}
Я не нашёл документации о параметре eol, но методом научного тыка выяснил, что при ошибке этот флаг принимает значение -1.
Структура pa_sink_info содержит всю необходимую нам информацию. Уровень громкости тут содержится не в процентах, а в довольно больших числах. Кроме того, уровень громкости можно получить для каждого канала отдельно. Поле channels содержит количество доступных каналов, а массив values, состоящий из 32-ух целочисленных значений имеет инициализированными ровно channels первых элементов. Здесь я схалтурил и взял первое попавшееся значение. Карту соответствия индексов массива с реальными каналами можно посмотреть в поле channel_map структуры pa_sink_info.
Высчитать текущий уровень громкости в процентах помогает константа PA_VOLUME_NORM, которая содержит значение громкости при 100%, 65536.

На этом наша работа с pulseaudio заканчивается. Осталось только корректно завершить работу.
void pamonClose(Pamon * pamon)
{
if(!pamon) return;
pa_context_disconnect(pamon->pulse_context);
pa_context_unref(pamon->pulse_context);
pa_glib_mainloop_free(pamon->pulse_glib_mainloop);
free(pamon);
}
Для этого отсоединяемся от демона pulseaudio c помощью вызова pa_context_disconnect, отпускаем ссылку на контекст с помощью pa_context_unref и высвобождаем память, занятую основным циклом используя pa_glib_mainloop_free. Естественно не забываем и о нашей структуре, хранящей служебную информацию. Следует обратить внимание, что для завершения работы с другими типами mainloop`ов нужно выполнять действия, соответствующие выбранному типу.

За бортом остались две функции: pamonSetSinkId и pamonTranslateError. Их назначение понятно, а код прост, поэтому приведу его без комментариев.
#define ERSTR_UNKNOWN "Unknown error"
#define ERSTR_INVALID_SINK "Invalid sink"
#define ERSTR_CONNECTION_FAILED "Unable to connect to the pulseaudio daemon"

void pamonSetSinkId(Pamon * pamon, uint32_t id)
{
if(!pamon || pamon->sink_id == id)
return;
pamon->sink_id = id;
performSinkHandler(pamon->pulse_context, pamon);
}

const char * pamonTranslateError(PamonError error)
{
switch(error)
{
case PAMON_ERROR_CONNECTION_FAILED:
return ERSTR_CONNECTION_FAILED;
case PAMON_ERROR_INVALID_SINK:
return ERSTR_INVALID_SINK;
default:
return ERSTR_UNKNOWN;
}
}

lxpanel

Полученное приложение уже можно использовать, например, в conky или dzen2. Так
выглядит dzen2, запущенный командой
$ ./pamon -f " Current volume: %i%%" | dzen2 -bg "#505050" -fg white -ta left
Но наша цель -- встроить монитор в lxpanel, поэтому создаём ещё один файл, lxpamon.c, который будем компилировать в динамическую библиотеку.
Первое, что нам надо сделать -- это подключить необходимые заголовочные файлы.
#include <stdlib.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <lxpanel/plugin.h>
#include "pamon.h"
Файл lxpanel/plugin.h и есть тот самый файл, который не способен переварить компилятор C++.
Документации по написанию плагинов для lxpanel ещё меньше, чем документации по pulseaudio, а точнее -- ровно одна страничка. Даже зачитав её до дыр, Вы не сможете написать полноценный плагин. Для того, чтобы написать свой, мне пришлось посмотреть в исходинки плагинов, поставляемых с lxpanel. Я скачал исходники из репозитория debian по этой ссылке. Файл src/plugins/dclock.c, содержащий исходники плагина с часами, оказался наиболее компактным и полезным.
Итак, для того, чтобы встроить плагин в панель, мы должны собрать библиотеку и положить её в  /usr/lib/lxpanel/plugins/ (или в  /usr/lib/x86_64-linux-gnu/lxpanel/plugins/ для мультиархитектурного debian`а). Есть одна особенность, имя файла не должно содержать префикс lib и иметь имя <имя_плагина>.so. В нашем случае файл будет называться lxpamon.so.
Библиотека с плагином должна экспозить экземпляр структуры PluginClass (к сожалению, ссылок мне давать не на что). Но не просто экспозить, а экспозить с определённым именем. Имя должно иметь вид <имя_плагина>_plugin_class. То есть, наш экземпляр будет иметь имя lxpamon_plugin_class.
PluginClass lxpamon_plugin_class = {
PLUGINCLASS_VERSIONING,
type: "lxpamon",
name: N_("Pulseaudio volume monitor"),
version: "1.0",
description: N_("Pulseaudio volume monitor plugin for LXPanel"),
one_per_system: FALSE,
expand_available: FALSE,
constructor: lxpamonConstructor,
destructor: lxpamonDestructor,
config: lxpamonConfigure,
save: lxpamonSaveConfig,
panel_configuration_changed: lxpamonPanelConfigurationChanged
};
Структура PluginClass описывает наш плагин. Макрос PLUGINCLASS_VERSIONING заполняет поля для определения версии структуры. Поле type должно содержать имя нашего плагина. Поля version и description вполне понятны. Флаг one_per_system позволяет ограничит количество активных экземпляров плагина одной штукой, а флаг expand_available разрешает растягивать виджет на всю свободную область панели (как tasklist или spacer). Макрос N_ объявлен в GLib и предназначен для интернационализации.
Оставшиеся поля контролируют жизненный цикл плагина, это следующие процедуры обратного вызова:
  • constructor -- инициализирует плагин;
  • destructor -- вызывается перед выгрузкой плагина;
  • config -- колбек, который будет зваться для редактирования настроек (например, при нажатии на кнопку Edit в настройках элементов панели);
  • save -- зовётся для сохранения настроек в конфиг;
  • panel_configuration_changed -- колбек, сообщающий, что конфигурация самой панели изменилась.
Для того, чтобы хранить текущее состояние плагина, будем использовать структуру PluginData.
typedef struct
{
Pamon * pamon;
GtkWidget * label;
uint32_t current_volume;
int sink;
char * format;
int bold_font;
} PluginData;
И сразу же приведу код конструктора.
int lxpamonConstructor(Plugin * plugin, char ** config)
{
GtkWidget * label = gtk_label_new(NULL);
plugin->pwid = gtk_event_box_new();
gtk_container_add(GTK_CONTAINER(plugin->pwid), GTK_WIDGET(label));
g_signal_connect(G_OBJECT(plugin->pwid), "button_press_event",
G_CALLBACK(plugin_button_press_event), plugin);
gtk_widget_show_all(plugin->pwid);
PluginData * plug_data = g_new0(PluginData, 1);
plug_data->label = label;
lxpamonInitConfig(config, plug_data);
plugin->priv = plug_data;
PamonInitData pamon_init_data;
memset(&pamon_init_data, 0, sizeof(PamonInitData));
pamon_init_data.volume_callback = (PamonVolumeChangedCallback)pamonVolumeChanged;
pamon_init_data.error_callback = (PamonErrorCallback)pamonError;
pamon_init_data.userdata = plugin;
pamon_init_data.sink_id = plug_data->sink;
Pamon * pamon = pamonInit(&pamon_init_data);
plug_data->pamon = pamon;
return TRUE;
}
Здесь создаются GTK+ контейнер и label, который кладётся в контейнер. Вообще говоря, Вы может создавать любые виджеты. Осонвное, что Вы должны сделать -- положить указатель на основной виджет в поле pwid структуры Plugin, которая передаётся на вход конструктору. Вообще, указатель на структуру Plugin будет передаваться во все колбеки. Эта структура содержит кое-какие полезные поля, с которыми мы познакомимся по ходу дела. Поле priv предназначено для хранения пользовательских данных, поэтому положим туда указатель на объект структуры PluginData.
Вторым параметром в конструктор передаётся конфиг, который мы читаем в функции lxpamonInitConfig. Сохраняется конфиг, как уже было сказано, функцией lxpamonSaveConfig.
#define DEFAULT_FORMAT "%i%%"
#define CONFIG_FORMAT "Format"
#define CONFIG_SINK "Sink"
#define CONFIG_BOLD "BoldFont"

void lxpamonInitConfig(char ** src, PluginData * plug_data)
{
line str;
str.len = 256;
while(lxpanel_get_line(src, &str))
{
if(strcmp(CONFIG_FORMAT, str.t[0]) == 0)
plug_data->format = g_strdup(str.t[1]);
else if(strcmp(CONFIG_SINK, str.t[0]) == 0)
sscanf(str.t[1], "%i", &plug_data->sink);
else if(strcmp(CONFIG_BOLD, str.t[0]) == 0)
plug_data->bold_font = str2num(bool_pair, str.t[1], 0);
}
if(!plug_data->format)
plug_data->format = g_strdup(DEFAULT_FORMAT);
}

void lxpamonSaveConfig(Plugin * plugin, FILE * file)
{
PluginData * plug_data = (PluginData *)plugin->priv;
lxpanel_put_str(file, CONFIG_FORMAT, plug_data->format);
lxpanel_put_int(file, CONFIG_SINK, plug_data->sink);
lxpanel_put_bool(file, CONFIG_BOLD, plug_data->bold_font);
}
Для записи данных в конфиг, используются макросы lxpanel_put_str, lxpanel_put_bool и lxpanel_put_int, кторые принемают файл, имя параметра и значение. Наш конфиг будет выглядеть примерно так
Plugin {
type = lxpamon
Config {
Format=%i%%
Sink=0
BoldFont=1
}
}
Конфиги лежат в ~/.config/lxpanel/default/panels/<имя_панели>. Для последовательного чтения записей из конфига следует использовать функцию lxpanel_get_line. Она записывает в свой второй параметр пару имя-значение из конфига. Как только пары кончатся, функция вернёт ноль. Функция str2num хоть и кажется, что переводит строку в число, но на деле она работает только с предопределёнными соответствиями, которые передаются в первом параметре. Например, bool_pair представлен примерно так { {"0", 0}, {"1", 1 } }.

Для того, чтобы пользователь смог задать настройки, мы должны показать диалог при обработке соответствующего события.
void lxpamonConfigure(Plugin * plugin, GtkWindow * parent)
{
PluginData * data = (PluginData *)plugin->priv;
GtkWidget * dlg = create_generic_config_dlg(_(plugin->class->name), GTK_WIDGET(parent),
(GSourceFunc)lxpamonApplyConfiguration, plugin,
_("Pulseaudio sink"), &data->sink, CONF_TYPE_INT,
_("Volume display format"), &data->format, CONF_TYPE_STR,
_("Use %i to print current volume. Use %% to print the % mark."), NULL, CONF_TYPE_TRIM,
_("Bold font"), &data->bold_font, CONF_TYPE_BOOL,
NULL);
gtk_window_present(GTK_WINDOW(dlg));
}
Здесь используется функция create_generic_config_dlg, показывающая стандартный диалог с настройками.

Вся проблема в том, что этой функции нет в файле lxpanel/plugin.h. Она объявлена в самой панели, но стандартные плагины используют именно её. Поэтому мы добавим себе её объявление.
extern GtkWidget * create_generic_config_dlg(const char * title, GtkWidget * parent,
GSourceFunc apply_func, Plugin * plugin, const char * name, ...);
Эта функция принимает родителя, заголовок и колбек, который будет вызван для применения настроек. В списке неопределённых параметров функция принимает тройки "имя параметра", "указатель на значение" и "тип значения". Завершается список параметров NULL`ом. Макрос _ объявлен в GLib и нужен для интернационализации. При принятии параметров мы устанавливаем новый sink для монитора и перерисовываем виджет
void lxpamonApplyConfiguration(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
pamonSetSinkId(plug_data->pamon, plug_data->sink);
lxpamonDrawVolume(plugin);
}

static void lxpamonDrawVolume(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
char * format = plug_data->format ? plug_data->format : "%%i";
char * buf = (char *)malloc(strlen(format) + 4);
if(INVALID_VOLUME == plug_data->current_volume)
strcpy(buf, "ERROR");
else
sprintf(buf, plug_data->format, plug_data->current_volume);
panel_draw_label_text(plugin->panel, plug_data->label, buf, plug_data->bold_font, TRUE);
free(buf);
}
При отрисовке мы снова используем функцию, которая не объявлена в lxpanel/plugin.h.
extern void panel_draw_label_text(Panel * p, GtkWidget * label, char * text,
gboolean bold, gboolean custom_color);
Функция panel_draw_label_text рисует текст на лейбле цветом, заданным в конфигурации панели. Структура Plugin содержит поле panel, у которого в свою очередь есть поля, хранящие цвета панели, в том числе цвет шрифта. Но есть и флаг usefontcolor, который установлен в 0, а при попытке использовать значение цвета получается что-то красное. Функция panel_draw_label_text также позволяет рисовать полужирным шрифтом, чем мы тоже пользуемся.
Значение INVALID_VOLUME устанавливается при обработке ошибки от pamon. В этом случае наш плагин напишет слово "ERROR".

Функция panel_draw_label_text не последняя из тех, которые забыли положить в файл lxpanel/plugin.h, ещё нам нужен стандартный обработчик клика мыши.
extern gboolean plugin_button_press_event(GtkWidget * widget, GdkEventButton * event, Plugin * plugin);
Эту функцию мы повесели на "button_press_event" в конструкторе. Эта функция добавляет в контекстное меню пункты для редактирования настроек и удаления виджета с панели.

После того, как настройки панели изменены нам нужно перерисовать свой виджет. Именно для этого мы и подписывались на это событие.
void lxpamonPanelConfigurationChanged(Plugin * plugin)
{
lxpamonDrawVolume(plugin);
}

Обработчики событий от pulseaudio тривиальны и не нуждаются в комментариях за одним исключением, ошибка может произойти до того, как поле plugin->priv будет присвоено, поэтому его нужно проверить.
void lxpamonPanelConfigurationChanged(Plugin * plugin)
{
lxpamonDrawVolume(plugin);
}

void pamonVolumeChanged(uint32_t current_volume, Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
plug_data->current_volume = current_volume;
lxpamonDrawVolume(plugin);
}

void pamonError(PamonError error, Plugin * plugin)
{
fprintf(stderr, "Pulseaudio monitor error: %s\n", pamonTranslateError(error));
PluginData * plug_data = (PluginData *)plugin->priv;
if(plug_data)
{
plug_data->current_volume = INVALID_VOLUME;
lxpamonDrawVolume(plugin);
}
}
Последнее, что осталось сделать -- освободить ресурсы при выгрузке плагина. Здесь это гораздо важнее, чем в консольном приложении, так как панель может продолжать работать ещё долгое время и утекшая память не вернётся системе.
void lxpamonDestructor(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
pamonClose(plug_data->pamon);
g_free(plug_data->format);
g_free(plug_data);
}
Поле plugin->pwid осовобождать не нужно, lxpanel это сделает за Вас.

P.S.

Исходники проекта лежат на github.
git clone https://github.com/brainstream/lxpamon.git
Возможно, проект я буду дорабатывать, если кому интересна изначальная версия, она лежит здесь.

Получившийся результат можно посмотреть на видео ниже