• СТАТЬЯ Введение в COM Часть 2
  • Программирование на Visual C++

    Выпуск №29 от 24 декабря 2000 г.

    Здравствуйте, уважаемые подписчики!

    Рад снова приветствовать вас на страницах рассылки. В этом выпуске вас ожидает вторая часть статьи "Введение в COM" и, конечно же, ответы на вопрос из предыдущего выпуска и кое-что еще.

    СТАТЬЯ

    Введение в COM

    Часть 2

    Автор: michael dunn

    Перевод: Илья Простакишин

    Источник: The Code Project

    Базовый интерфейс – IUnknown

    Каждый COM-интерфейс наследуется от интерфейса IUnknown. Имя выбрано не совсем удачно, поскольку этот интерфейс не является "неизвестным" (unknown). Это имя всего лишь означает, что если вы имеете указатель на интерфейс COM-объекта IUnknown, то вы не можете знать, какой объект им владеет (реализует), поскольку интерфейс IUnknown есть в каждом COM-объекте.

    IUnknown включает три метода:

    1. AddRef() – заставляет COM-объект увеличивать (инкрементировать) свой счетчик обращений. Вы должны использовать этот метод, если была сделана копия указателя на интерфейс и нужно обеспечить возможность использования двух указателей – копии и оригинала. Мы не будем использовать метод AddRef() в этой статье, т.к. для рассматриваемых здесь задач он не нужен. 

    2. Release() – сообщает COM-объекту о необходимости уменьшения (декремента) счетчика обращений. Смотрите предыдущий пример, чтобы понять, как нужно использовать Release(). 

    3. QueryInterface() – запрашивает указатель на интерфейс COM-объекта. Используется если CO-класс содержит не один, а несколько интерфейсов.

    Вы уже видели пример использования Release(), но как же действует QueryInterface()? Когда вы создаете COM-объект с помощью CoCreateInstance(), вы получаете указатель на интерфейс. Если COM-объект включает более одного интерфейса (не считая IUnknown), вы должны использовать метод QueryInterface() для получения дополнительных указателей на интерфейсы, которые вам нужны. Посмотрим на прототип QueryInterface():

    HRESULT IUnknown::QueryInterface(REFIID iid, void** ppv);

    Значения параметров:

    iid IID интерфейса, который вам нужен.
    ppv Адрес указателя на интерфейс. QueryInterface() возвращает указатель на интерфейс через этот параметр, если не произошло никаких ошибок.

    Продолжим наш пример с ярлыком. CO-класс для создания ярлыков включает интерфейсы IShellLink и IPersistFile. Если у вас уже есть указатель на IShellLink – pISL, то вы можете запросить интерфейс IPersistFile у COM-объекта с помощью следующего кода:

    HRESULT hr;

    IPersistFile* pIPF;

    hr = pISL->QueryInterface(IID_IPersistFile, (void**)&pIPF);

    Затем вы тестируете hr с помощью макроса SUCCEEDED. Это нужно, чтобы узнать, сработал ли метод QueryInterface(). Если все нормально, то можно использовать новый указатель pIPF, так же как и любой другой интерфейсный указатель. Затем вам нужно вызвать метод pIPF->Release() для сообщения COM-объекту, что вы закончили работу с интерфейсом и он вам больше не нужен.

    Обратите внимание – обработка строк

    Я хочу остановиться на некоторых моментах, касающихся работы со строками при написании программ в COM.

    Всякий раз, когда метод COM возвращает строку, он делает это, используя формат Unicode. Unicode это таблица символов, также как и ASCII, только все символы в ней занимают 2 байта (в ANSI – один байт). Если вы хотите получить строку в более удобном виде, то ее нужно преобразовать в тип TCHAR.

    TCHAR и функции, начинающиеся с _t (например, _tcscpy()) были разработаны для управления строками Unicode и ANSI с использованием одинакового исходного кода. Наверняка, вы раньше писали программы с использованием ANSI-строк и ANSI-функций, поэтому далее в этой статье я буду обращаться к типу char, вместо TCHAR, чтобы лишний раз вас не смущать. Однако, вы должны знать, что есть такой тип – TCHAR, хотя бы для того, чтобы не задавать лишних вопросов, когда встретите его в программах, написанных другими разработчиками.

    Когда вы получаете строку из метода COM, вы можете преобразовать ее в строку char одним из следующих способов:

    1. Вызвать функцию API WideCharToMultiByte(). 

    2. Вызвать функцию CRT wcstombs().

    3. Использовать конструктор CString или оператор присваивания (только в MFC). 

    4. Использовать макрос преобразования ATL.

    Особенности Unicode

    С другой стороны, вы можете лишь хранить строку Unicode, если с ней не требуется делать что-либо еще. Если вы создаете консольное приложение, то вывод на экран строки Unicode можно осуществить с помощью глобальной переменной std::wcout, например:

    wcout << wszSomeString;

    Однако, имейте ввиду, что wcout предполагает, что все "входящие" строки имеют формат Unicode, поэтому если вы имеете любую "нормальную" строку, то для вывода нужно использовать std::cout. Если вы используете строковые литералы, для перевода в Unicode ставьте перед ними символ L, например:

    wcout << L"The Oracle says…" << endl << wszOracleResponse;

    Если вы используете строки Unicode, вы должны знать о следующих ограничениях:

    • С этими строками вы должны использовать функции вида wcsXXX(), например wcslen(). 

    • За редким исключением, вы не должны передавать строки Unicode функциям Windows API в ОС Windows 9x. Чтобы обеспечить переносимость кода между платформами 9x и NT, вы должны использовать типы TCHAR, как это описано в MSDN. Объединим все вместе – Примеры Программ

    Здесь приведены два примера, иллюстрирующие концепции COM, которые обсуждались ранее в этой статье.

    Использование объекта COM с одним интерфейсом

    Первый пример показывает, как можно использвать объект COM, содержащий единственный интерфейс. Это простейший случай из тех, которые вам могут встретиться. Программа использует содержащийся в оболочке CO-класс Active Desktop для получения имени файла "обоев", которые установлены в данный момент. Чтобы этот код был работоспособен, вам может потребоваться установить Active Desktop.

    Мы должны осуществить следующие шаги:

    1. Инициализировать библиотеку COM. 

    2. Создать COM-объект, используемый для взаимодействия с Active Desktop и получить интерфейс IActiveDesktop. 

    3. Вызвать метод COM-объекта GetWallpaper(). 

    4. Если GetWallpaper() завершился успешно, вывести имя файла "обоев" на экран. 

    5. Освободить интерфейс. 

    6. Разинициализировать библиотеку COM.

    WCHAR wszWallpaper[MAX_PATH];

    CString strPath;

    HRESULT hr;

    IActiveDesktop* pIAD;


    // 1. Инициализация библиотеки COM (заставляем Windows загрузить библиотеки DLL). Обычно

    // вам нужно делать это в функции InitInstance() или подобной ей. В MFC-приложениях

    // можно также использовать функцию AfxOleInit().

    CoInitialize(NULL);

    // 2. Создаем COM-объект, используя CO-класс Active Desktop, поставляемый оболочкой.

    // Четвертый параметр сообщает COM какой именно интерфейс нам нужен (IActiveDesktop).

    hr = CoCreateInstance(CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER, IID_IActiveDesktop, (void**)&pIAD);

    if (SUCCEEDED(hr)) {

     // 3. Если COM-объект был создан, то вызываем его метод GetWallpaper().

     hr = pIAD->GetWallpaper(wszWallpaper, MAX_PATH, 0);

     if (SUCCEEDED(hr)) {

      // 4. Если GetWallpaper() завершился успешно, выводим полученное имя файла.

      // Заметьте, что я использую wcout для отображения Unicode-строки wszWallpaper.

      // wcout является Unicode-эквивалентом cout.

      wcout << L"Wallpaper path is:\n " << wszWallpaper << endl << endl;

     } else {

      cout << _T("GetWallpaper() failed.") << endl << endl;

     }

     // 5. Освобождаем интерфейс.

     pIAD->Release();

    } else {

     cout << _T("CoCreateInstance() failed.") << endl << endl;

    }

    // 6. Разинициализируем библиотеку COM. В приложениях MFC этого не требуется –

    // MFC делает это автоматически.

    CoUninitialize();

    В этом примере я использовал std::wcout для отображения строки Unicode wszWallpaper.

    Использование COM-объекта, включающего несколько интерфейсов

    Второй пример показывает, как можно использовать QueryInterface() для получения единственного интерфейса COM-объекта. В этом примере используется CO-класс Shell Link, содержащийся в оболочке, для создания ярлыка для файла "обоев", имя которого мы получили в предыдущем примере.

    Программа состоит из следующих шагов:

    1. Инициализация библиотеки COM. 

    2. Создание объекта COM, используемого для создания ярлыков, и получение интерфейса IShellLink. 

    3. Вызов метода SetPath() интерфейса IShellLink. 

    4. Вызов метода QueryInterface() объекта COM и получение интерфейса IPersistFile. 

    5. Вызов метода Save() интерфейса IPersistFile. 

    6. Освобождение интерфейсов. 

    7. Разинициализация библиотеки COM.

    CString sWallpaper = wszWallpaper; // Конвертация пути к "обоям" в ANSI

    IShellLink* pISL;

    IPersistFile* pIPF;


    // 1. Инициализация библиотеки COM (заставляем Windows загрузить библиотеки DLL). Обычно

    // вам нужно делать это в функции InitInstance() или подобной ей. В MFC-приложениях

    // можно также использовать функцию AfxOleInit().

    CoInitialize(NULL);

    // 2. Создание объекта COM с использованием CO-класса Shell Link, поставляемого оболочкой.

    // 4-й параметр указывает на то, какой интерфейс нам нужен (IShellLink).

    hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (void**)&pISL);

    if (SUCCEEDED(hr)) {

     // 3. Устанавливаем путь, на который будет указывать ярлык (к файлу "обоев").

     hr = pISL->SetPath(sWallpaper);

     if (SUCCEEDED(hr)) {

      // 4. Получение второго интерфейса (IPersistFile) от объекта COM.

      hr = pISL->QueryInterface(IID_IPersistFile, (void**)&pIPF);

      if (SUCCEEDED(hr)) {

       // 5. Вызов метода Save() для сохранения ярлыка в файл. Первый параметр

       // является строкой Unicode.

       hr = pIPF->Save(L"C:\\wallpaper.lnk", FALSE);

       // 6a. Освобождение интерфейса IPersistFile.

       pIPF->Release();

      }

     }

     // 6b. Освобождение интерфейса IShellLink.

     pISL->Release();

    }

    // Где-то здесь должен быть код для обработки ошибок.

    // 7. Разинициализация библиотеки COM. В приложениях MFC этого делать

    // не нужно, т.к. MFC справляется с этим сама.

    CoUninitialize();

    Литература

    Essential COM, Don Box, ISBN 0-201-63446-5.

    MFC Internals, George Shepherd and Scot Wingo, ISBN 0-201-40721-3.

    Beginning ATL 3 COM Programming, Richard Grimes, ISBN 1-861001-20-7.

    ВОПРОС-ОТВЕТ 

    Q. Как в Win9x и WinNT заблокировать клавиши WIN, Alt+Tab, Ctrl+Esc etc.?

    (Mike Krasnik )

    A1 Например так – в конструкторе главного окна приложения зарегистрировать HotKey:

    m_HK = GlobalAddAtom("alttab"); // DWORD m_HK;

    RegisterHotKey(GetSafeHwnd(), m_HK, MOD_ALT, VK_TAB);
     

    а в деструкторе не забыть его разрегистрировать: 

    UnregisterHotKey(GetSafeHwnd(), m_HK);
     

    так как никакого обработчика для этого HotKey мы не делаем, то соответственно и происходить по нажатию Alt-Tab ничего не будет.

    (Алексей Кирюшкин )

    A2 По материалам http://msdn.microsoft.com/msdnmag/issues/0700/Win32/Win320700.asp

    В WinNT (начиная с Windows NTR 4.0 Service Pack 3) существует возможность использовать "low-level" hook на клавиатуру WH_ KEYBOARD_LL для отключения комбинаций Ctrl+Esc, Alt+Tab, Alt+Esc.

    Для данной данной функии установлен лимит времени: Система возвращается в нормальное состояние через промежуток времени определяемый параметром LowLevelHooksTimeout в HKEY_CURRENT_USER\Control Panel\Desktop время указывается в милисекундах.

    (Владимир Згурский )

    A3 Это делается очень по-разному в различных системах от Microsoft.

    В Windows 9x можно использовать трюк, опсанный в MSDN – вызвать функцию SystemParametersInfo с недокументированным параметром. В данном случае им можно пользоваться смело: Микрософт больше не будет вносить изменений в архитектуру Win9x. Чтобы отключить Alt+Tab, Ctrl+Alt+Del и т. д., нужно написать: 

    int prev;

    SystemParametersInfo(SPI_SCREENSAVERRUNNING, TRUE, &prev, 0);
     

    Выполняя этот код, мы сообщаем системе, что работает скринсейвер. А когда он работает, переключение задач запрещено. Чтобы включить стандартные комбинации снова, используйте: 

    int prev;

    SystemParametersInfo(SPI_SCREENSAVERRUNNING, FALSE, &prev, 0);
     

    Внимание: если этого не сделать, переключение задач будет невозможно даже после завершения работы вашего приложения! 

    Перейдём к Windows NT/2000. Там трюк со скрин сейвером не работает, но зато есть низкоуровневые хуки для мыши и клавиатуры (обычные хуки не перехватывают системные комбинации клавиш). Установив глобальный низкоуровневый хук на клавиатуру, можно "съесть" все системные нажатия (кроме Ctrl+Alt+Del). Для этого в ответ на приход таких нажатий функция хука должна вернуть единицу. 

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

    #define _WIN32_WINNT 0x0500

    #include <windows.h>


    static HINSTANCE hInstance;

    static HHOOK     hHook;


    BOOL APIENTRY DllMain(HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved) {

     hInstance = (HINSTANCE)hModule;

     return TRUE;

    }


    LRESULT CALLBACK KeyboardProc(INT nCode, WPARAM wParam, LPARAM lParam);


    extern "C" __declspec(dllexport) void HookKeyboard() {

     hHook = SetWindowsHookEx(WH_KEYBOARD_LL, (HOOKPROC)KeyboardProc, hInstance, 0);

    }


    extern "C" __declspec(dllexport) void UnhookKeyboard() {

     UnhookWindowsHookEx(hHook);

    }


    LRESULT CALLBACK KeyboardProc(INT nCode, WPARAM wParam, LPARAM lParam) {

     KBDLLHOOKSTRUCT *pkbhs = (KBDLLHOOKSTRUCT*)lParam;

     BOOL bControlKeyDown = 0;

     if (nCode == HC_ACTION) {

      bControlKeyDown = GetAsyncKeyState(VK_CONTROL) >> ((sizeof(SHORT) * 8) - 1);

      // Проверяем CTRL+ESC

      if (pkbhs->vkCode == VK_ESCAPE && bControlKeyDown) return 1;

      // Проверяем ALT+TAB

      if (pkbhs->vkCode == VK_TAB && pkbhs->flags & LLKHF_ALTDOWN) return 1;

      // Проверяем ALT+ESC

      if (pkbhs->vkCode == VK_ESCAPE && pkbhs->flags & LLKHF_ALTDOWN) return 1;

     }

     return CallNextHookEx(hHook, nCode, wParam, lParam);

    }

    Чтобы воспользоваться этой DLL, загрузите её любым способом, а затем вызывайте HookKeyboard, чтобы перехватывать комбинации клавиш, и UnhookKeyboard, чтобы прекратить перехват. 

    В ранних версиях NT низкоуровневых хуков не было. В MSDN утверждается, что там от Alt+Tab там можно избавиться с помощью перерегистрации глобального акселератора на ту же комбинацию (посредством RegisterHotKey), но испытать это средство мне не удалось (нет под рукой NT3.51 или NT4.0 с SP 2 и ниже). Ctrl+Esc там не блокируется. 

    Для полноты картины упомяну ещё одно непровереное средство, с помощью которого можно обезвредить Ctrl+Alt+Del под Windows NT/2000. Для этого нужно написать собственную GINA DLL. Если кого-нибудь интересуют подробности, сделайте в MSDN поиск по строке "GINA".

    (Александр Шаргин )
    ОБРАТНАЯ СВЯЗЬ 

    Уважаемый Алекс.

    Читая Вашу статью о DCOM я прочел:

    "Строго говоря, COM не является спецификацией, привязанной к Win32. Теоретически, можно портировать ваши COM-объекты в Unix или любые другие ОС. Однако, я никогда не видел, чтобы COM применялась где-то за пределами сферы влияния Microsoft."

    Могу подсказать ОС использующую COM/DCOM не из семейства Windows. Как ни странно это VxWorks, где COM/DCOM существует в виде одного из компонент ядра и обеспечивает все, что может быть положено на концепцию этой ОС.

    Например из-за ограничений ОС (там по сути только один процесс с общей памятью, но со многими потоками-задачами) серверы могут быть только INPROC. Не поддержан (пока что) IDispatch, массивы в VARIANT. Зато теперь можно использовать DCOM-распределенные системы на основе смеси Windows и VxWorks, что очень удобно для управления realtime системами.

    С уважением

    (Алексей Трошин )

    На вопрос из выпуска №27 о пунктирной рамке вокруг кнопки: 

    Предложенный Александром Шаргиным вариант с тулбаром врядли можно признать удовлетворительным. Диалог не получит сообщение от тулбара да и программное создание кнопки… Можно, конечно, но… :-( . Наиболее приемлемый выход – использование самопрорисовывающихся элементов управления. Достоинство этого метода – нарисовать можно всё, что угодно! :-))). А в вопросе Максима Чучуйко есть ещё подвопрос: А должна ли кнопка вообще получать фокус?.

    В общем, плоскую кнопку, не получающую фокус совсем сделать достаточно просто:

    1) Создаём класс

    CFlatButton: public CButton;

    2) Добавляем переменные:

    protected:

     BOOL bMouseCaptured;

     CWnd* pOldFocus;

    В конструкторе инициализируем:

     bMouseCaptured = FALSE;

     pOldFocus = NULL;

    3) Добавляем методы:

    protected:

     void CFlatButton::SetOldFocus() {

      // Закомментировать тело метода, если кнопка может получать фокус.

      if (pOldFocus) pOldFocus->SetFocus();

      pOldFocus =NULL;

    }

    Добавляем обработчики сообщений:

     void CFlatButton::OnSetFocus(CWnd* pOldWnd) {

      CButton::OnSetFocus(pOldWnd);

      if (!pOldFocus) // Дабы не было проблем с модальными окнами, вызываемыми по нажатию этой кнопки.

       pOldFocus = pOldWnd;

     }


     void CFlatButton::OnLButtonUp(UINT nFlags, CPoint point) {

      CButton::OnLButtonUp(nFlags, point);

      CRect rectBtn;

      GetClientRect(rectBtn);

      if (rectBtn.PtInRect(point) && GetCapture() != this) {

       bMouseCaptured = TRUE;

       SetCapture();

       Invalidate(FALSE);

      }

      SetOldFocus();

     }


     void CFlatButton::OnMouseMove(UINT nFlags, CPoint point) {

      CRect rectBtn;

      GetClientRect(rectBtn);

      if (rectBtn.PtInRect(point)) {

       BOOL bNeedUpdate =FALSE;

       if (!bMouseCaptured) bNeedUpdate = TRUE;

       bMouseCaptured = TRUE;

       SetCapture();

       if (bNeedUpdate) Invalidate(FALSE);

      } else {

       bMouseCaptured = FALSE;

       ReleaseCapture();

       SetOldFocus();

       Invalidate(FALSE);

      }

      CButton::OnMouseMove(nFlags, point);

     }

    И, самое интересное… :-))) Перекрываем виртуальный метод:

    void CFlatButton::DrawItem(LPDRAWITEMSTRUCT lpDIS) {

     // Test WS_TABSTOP

     ASSERT(!(GetStyle() & WS_TABSTOP)); 

     CDC* pDC = CDC::FromHandle(lpDIS->hDC);

     CRect rectAll;

     GetClientRect(rectAll);

     CString text;

     GetWindowText(text);

     int save = pDC->SaveDC();

     CRect rectText(rectAll);

     rectText.DeflateRect(2,2);

     CBrush bkBr(GetSysColor(COLOR_3DFACE));

     pDC->FillRect(rectAll,&bkBr);

     UINT state = lpDIS->itemState;

     if (state & ODS_SELECTED) {

      // Нажатое состояние

      rectText.OffsetRect(1,1);

      pDC->DrawEdge(rectAll, BDR_SUNKENOUTER, BF_RECT);

     } else {

      if (bMouseCaptured) {

       pDC->DrawEdge(rectAll, BDR_RAISEDINNER, BF_RECT);

      }

     }

     pDC->DrawText(text, rectText, DT_SINGLELINE|DT_VCENTER|DT_CENTER|DT_TOP);

     pDC->RestoreDC(save);

    }

    Использование: очень просто. Ставим на шаблоне диалога кнопку, убираем стиль WS_TABSTOP, ставим стиль WS_OWNERDRAW. В ClassWizard'е сопоставляем ей переменную типа CButton, затем тип переменной вручную меняем на CFlatButton. И всё. Далее – как с обычной кнопкой. У меня (VC++ 5.0) – работает.

    (Дмитрий Сулима)
    В ПОИСКАХ ИСТИНЫ

    Q. Как включать в проект незарегистрированный компонент ActiveX? Вернее он на моей машине зарегистрирован, а на другой нет, и в результате этого программа на той машине вообще не запускается.

    (Сергей Лобачев)

    Это все на сегодня. Удачи вам!

    (Алекс Jenter jenter@mail.ru) (Красноярск, 2000.)







    Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх